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:splitAsStreamasPredicate

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"));
}

asMatchPredicateasPredicate 的关键区别:

  • asPredicatefind() → 部分匹配
  • asMatchPredicatematches() → 全字符串匹配

这个方法在做数据校验时非常实用,避免了手动写 pattern.matcher(str).matches() 的样板代码。

4. 总结

  • 性能为王:高频正则匹配务必预编译 Pattern,必要时复用 Matcher 实例。
  • Java 8 引入 splitAsStreamasPredicate,让正则与 Stream 集成更自然。
  • Java 11 新增 asMatchPredicate,提供类型安全的全匹配断言,代码更简洁。
  • ⚠️ 避免在循环中使用 String#matches,这是性能“杀手”。

示例代码已托管至 GitHub:


原始标题:Pre-compile Regex Patterns Into Pattern Objects