将正则表达式模式预编译为Pattern对象

2023/06/09

1. 概述

在本教程中,我们将看到预编译正则表达式模式的好处以及Java 8和11中引入的新方法

这将不是一个正则表达式操作方法,但我们为此目的提供了一个出色的Java正则表达式API指南

2. 好处

重用不可避免地会带来性能提升,因为我们不需要一次又一次地创建和重新创建相同对象的实例。因此,我们可以假设重用和性能通常是联系在一起的。

让我们来看看这个原则,因为它与Pattern#compile相关。我们将使用一个简单的基准测试

  1. 我们有一个列表,其中包含从1到5,000,000的5,000,000个数字
  2. 我们的正则表达式将匹配偶数

因此,让我们使用以下Java正则表达式来测试解析这些数字:

  • String.matches(regex)
  • Pattern.matches(regex, charSequence)
  • Pattern.compile(regex).matcher(charSequence).matches()
  • 预编译的正则表达式,多次调用preCompiledPattern.matcher(value).matches()
  • 带有一个Matcher实例和多次调用matcherFromPreCompiledPattern.reset(value).matches()的预编译正则表达式

实际上,如果我们看一下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();
}

然后,我们可以想象前三个表达式的执行方式相似。这是因为第一个表达式调用第二个,第二个调用第三个。

第二点是这些方法不会重用创建的Pattern和Matcher实例。而且,正如我们将在基准测试中看到的那样,这会将性能降低六倍:

@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) {
    Instant start = Instant.now();
    for (String value : values) {
        bh.consume(value.matches(PATTERN));
    }
}

从基准测试结果来看,毫无疑问,预编译的Pattern和重用的Matcher是赢家,结果快了六倍以上:

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

除了性能时间之外,我们还有创建的对象数量

  • 前三种形式:
    • 创建了5,000,000个Pattern实例
    • 创建了5,000,000个Matcher实例
  • preCompiledPattern.matcher(value).matches()
    • 创建了1个Pattern实例
    • 创建了5,000,000个Matcher实例
  • matcherFromPreCompiledPattern.reset(value).matches()
    • 创建了1个Pattern实例
    • 创建了1个Matcher实例

因此,不要将我们的正则表达式委托给String#matches或Pattern#matches,因为它们总是会创建Pattern和Matcher实例。我们应该预编译我们的正则表达式以获得性能并创建更少的对象。

要了解更多关于正则表达式性能的信息,请查看我们的Java正则表达式性能概述

3. 新方法

由于引入了函数式接口和流,重用变得更加容易。

Pattern类已在新的Java版本中发展,以提供与流和lambda的集成。

3.1 Java 8

Java 8引入了两种新方法:splitAsStream和asPredicate

让我们看看splitAsStream的一些代码,该代码围绕模式的匹配从给定的输入序列创建一个流:

@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方法创建一个谓词,其行为就像它从输入序列创建一个匹配器,然后调用find:

string -> matcher(string).find();

让我们创建一个模式来匹配列表中的名称,该列表中的名字和姓氏至少各有三个字母:

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

3.2 Java 11

Java 11引入了asMatchPredicate方法,该方法创建一个谓词,该谓词的行为就像它从输入序列创建一个匹配器然后调用匹配项:

string -> matcher(string).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"));
}

4. 总结

在本教程中,我们看到使用预编译模式为我们带来了卓越的性能

我们还了解了JDK 8和JDK 11中引入的三种新方法,它们使我们的生活更轻松。

这些示例的代码可在GitHub上的java-11-1中获得,用于JDK 11片段,而java-regex-1则用于其他片段。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章