1. 概述
本文将深入探讨在 Java 中预编译正则表达式模式(Pattern)带来的性能优势,并介绍 Java 8 和 Java 11 中新增的便捷方法。
这并不是一篇正则表达式入门教程。如果你需要系统学习 Java 正则 API,推荐阅读我们的《Java 正则表达式指南》。
重点在于:如何避免在高频场景下“踩坑”——反复编译同一个正则,导致性能雪崩。
2. 预编译的性能优势
复用对象是性能优化的经典手段。正则表达式也不例外。每次调用 String.matches()
或 Pattern.matches()
,都会重新编译 Pattern 并创建 Matcher 实例,开销不容小觑。
我们通过一个简单粗暴的基准测试来验证:
- 数据集:500 万个数字字符串("1" 到 "5000000")
- 目标:匹配偶数(正则:
\\d*[02468]$
)
测试以下五种方式的性能:
✅ matcherFromPreCompiledPattern.reset(value).matches()
✅ preCompiledPattern.matcher(value).matches()
❌ Pattern.compile(regex).matcher(value).matches()
❌ Pattern.matches(regex, value)
❌ value.matches(regex)
2.1 为什么前三者性能相近?
先看源码:
// String.matches 的实现
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
// Pattern.matches 的实现
public static boolean matches(String regex, CharSequence input) {
Pattern p = compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
结论一目了然:String#matches
→ 调用 Pattern#matches
→ 调用 Pattern#compile
+ matcher#matches
所以前三者本质上都在重复“编译 + 创建 Matcher”的过程,性能自然差。
2.2 基准测试代码
@Benchmark
public void matcherFromPreCompiledPatternResetMatches(Blackhole bh) {
for (String value : values) {
bh.consume(matcherFromPreCompiledPattern.reset(value).matches());
}
}
@Benchmark
public void preCompiledPatternMatcherMatches(Blackhole bh) {
for (String value : values) {
bh.consume(preCompiledPattern.matcher(value).matches());
}
}
@Benchmark
public void patternCompileMatcherMatches(Blackhole bh) {
for (String value : values) {
bh.consume(Pattern.compile(PATTERN).matcher(value).matches());
}
}
@Benchmark
public void patternMatches(Blackhole bh) {
for (String value : values) {
bh.consume(Pattern.matches(PATTERN, value));
}
}
@Benchmark
public void stringMatchs(Blackhole bh) {
for (String value : values) {
bh.consume(value.matches(PATTERN));
}
}
2.3 性能对比结果
Benchmark Mode Cnt Score Error Units
PatternPerformanceComparison.matcherFromPreCompiledPatternResetMatches avgt 20 278.732 ± 22.960 ms/op
PatternPerformanceComparison.preCompiledPatternMatcherMatches avgt 20 500.393 ± 34.182 ms/op
PatternPerformanceComparison.stringMatchs avgt 20 1433.099 ± 73.687 ms/op
PatternPerformanceComparison.patternCompileMatcherMatches avgt 20 1774.429 ± 174.955 ms/op
PatternPerformanceComparison.patternMatches avgt 20 1792.874 ± 130.213 ms/op
性能差距高达 6 倍以上!
2.4 对象创建数量对比
方式 | Pattern 实例数 | Matcher 实例数 |
---|---|---|
String/Pattern.matches 等 |
5,000,000 | 5,000,000 |
预编译 Pattern + 每次 new Matcher | 1 | 5,000,000 |
预编译 Pattern + 复用 Matcher | 1 | 1 |
⚠️ 结论:
在高频匹配场景下,务必预编译 Pattern。如果匹配频率极高,进一步复用 Matcher 实例(通过 reset()
)能获得最佳性能。
更多正则性能优化技巧,参考:《Java 正则表达式性能概览》。
3. Java 8 与 Java 11 新增方法
随着函数式编程和 Stream 的普及,Pattern 类也与时俱进,新增了与 Lambda 和 Stream 集成的方法。
3.1 Java 8:splitAsStream
和 asPredicate
✅ splitAsStream(CharSequence input)
将匹配结果作为 Stream 返回,适合处理大文本或需要流式处理的场景。
@Test
public void givenPreCompiledPattern_whenCallSplitAsStream_thenReturnArraySplitByThePattern() {
Pattern splitPreCompiledPattern = Pattern.compile("__");
Stream<String> textSplitAsStream = splitPreCompiledPattern.splitAsStream("My_Name__is__Fabio_Silva");
String[] textSplit = textSplitAsStream.toArray(String[]::new);
assertEquals("My_Name", textSplit[0]);
assertEquals("is", textSplit[1]);
assertEquals("Fabio_Silva", textSplit[2]);
}
✅ asPredicate()
返回一个 Predicate<String>
,其逻辑等价于 matcher(input).find()
,即只要能找到匹配项就返回 true
。
@Test
public void givenPreCompiledPattern_whenCallAsPredicate_thenReturnPredicateToFindPatternInTheList() {
List<String> namesToValidate = Arrays.asList("Fabio Silva", "Mr. Silva");
Pattern firstLastNamePreCompiledPattern = Pattern.compile("[a-zA-Z]{3,} [a-zA-Z]{3,}");
Predicate<String> patternsAsPredicate = firstLastNamePreCompiledPattern.asPredicate();
List<String> validNames = namesToValidate.stream()
.filter(patternsAsPredicate)
.collect(Collectors.toList());
assertEquals(1, validNames.size());
assertTrue(validNames.contains("Fabio Silva"));
}
💡 注意:
find()
是查找任意位置的匹配,不要求整个字符串完全匹配。
3.2 Java 11:asMatchPredicate()
✅ asMatchPredicate()
返回一个 Predicate<String>
,其逻辑等价于 matcher(input).matches()
,即要求整个字符串完全匹配该正则。
@Test
public void givenPreCompiledPattern_whenCallAsMatchPredicate_thenReturnMatchPredicateToMatchesPattern() {
List<String> namesToValidate = Arrays.asList("Fabio Silva", "Fabio Luis Silva");
Pattern firstLastNamePreCompiledPattern = Pattern.compile("[a-zA-Z]{3,} [a-zA-Z]{3,}");
Predicate<String> patternAsMatchPredicate = firstLastNamePreCompiledPattern.asMatchPredicate();
List<String> validatedNames = namesToValidate.stream()
.filter(patternAsMatchPredicate)
.collect(Collectors.toList());
assertTrue(validatedNames.contains("Fabio Silva"));
assertFalse(validatedNames.contains("Fabio Luis Silva"));
}
✅
asMatchPredicate
与asPredicate
的关键区别:
asPredicate
→find()
→ 部分匹配asMatchPredicate
→matches()
→ 全字符串匹配
这个方法在做数据校验时非常实用,避免了手动写 pattern.matcher(str).matches()
的样板代码。
4. 总结
- ✅ 性能为王:高频正则匹配务必预编译
Pattern
,必要时复用Matcher
实例。 - ✅ Java 8 引入
splitAsStream
和asPredicate
,让正则与 Stream 集成更自然。 - ✅ Java 11 新增
asMatchPredicate
,提供类型安全的全匹配断言,代码更简洁。 - ⚠️ 避免在循环中使用
String#matches
,这是性能“杀手”。
示例代码已托管至 GitHub:
- Java 11 示例:core-java-11
- 正则表达式示例:core-java-regex