1. 概要

このチュートリアルでは、正規表現パターンをプリコンパイルすることの利点と、Java8および11で導入された新しいメソッドを確認します。

これは正規表現のハウツーではありませんが、その目的のための優れたJava正規表現APIガイドがあります。

2. 利点

同じオブジェクトのインスタンスを何度も作成および再作成する必要がないため、再利用すると必然的にパフォーマンスが向上します。 したがって、再利用とパフォーマンスはしばしば関連していると推測できます。

この原則に関連するものを見てみましょう Pattern#compile。 W 簡単なベンチマークを使用します

  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()への多くの呼び出しを含むプリコンパイルされた正規表現
  • 1つの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();
}

そうすると、最初の3つの式が同じように機能することが想像できます。これは、最初の式が2番目を呼び出し、2番目が3番目を呼び出すためです。

2つ目のポイントは、これらのメソッドは、作成されたPatternおよびMatcherインスタンスを再利用しないことです。 そして、ベンチマークでわかるように、これによりパフォーマンスが6分の1に低下します


@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が勝者であり、結果が6倍以上速いことは間違いありません。

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

パフォーマンス時間を超えて、作成されたオブジェクトの数もあります

  • 最初の3つの形式:
    • 5,000,000パターンインスタンスが作成されました
    • 5,000,000マッチャーインスタンスが作成されました
  • preCompiledPattern.matcher(value).matches()
    • 1つのパターンインスタンスが作成されました
    • 5,000,000マッチャーインスタンスが作成されました
  • matcherFromPreCompiledPattern.reset(value).matches()
    • 1つのパターンインスタンスが作成されました
    • 1つのMatcherインスタンスが作成されました

したがって、正規表現を String#matchesまたはPattern#matches に委任する代わりに、常にPatternおよびMatcherインスタンスを作成します。 パフォーマンスを向上させ、作成されるオブジェクトを少なくするには、正規表現を事前にコンパイルする必要があります。

regexのパフォーマンスの詳細については、Javaでの正規表現のパフォーマンスの概要を確認してください。

3. 新しい方法

機能的なインターフェイスとストリームの導入以来、再利用が容易になりました。

Patternクラスは新しいJavaバージョンで進化し、ストリームとラムダとの統合を提供します。

3.1. Java 8

Java 8では、splitAsStreamasPredicateの2つの新しいメソッドが導入されました。

パターンの一致の周りに指定された入力シーケンスからストリームを作成する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();

リストの名前に一致するパターンを作成してみましょう。名前には、それぞれ少なくとも3文字の名前と名前があります。

@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();

少なくとも3文字の名前と姓のみを持つリストから、名前に一致するパターンを作成しましょう。

@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. 結論

このチュートリアルでは、事前にコンパイルされたパターンを使用すると、はるかに優れたパフォーマンスが得られることを確認しました。

また、JDK8とJDK11で導入された3つの新しいメソッドについて学びました。これにより私たちの生活が楽になります

これらの例のコードは、GitHubの core-java-11でJDK11スニペットに、core-java-regexで入手できます。