Pattern 物件


剖析、驗證規則表示式往往是最耗時間的階段,在頻繁使用某規則表示式的場合,若可以將剖析、驗證過後的規則表示式重複使用,對效率將會有所幫助。

java.util.regex.Pattern 實例是規則表示式在 JVM 中的代表物件,Pattern 的建構式被標示為 private,無法用 new 建構 Pattern 實例,必須透過 Pattern 的靜態方法 compile 來建構,在剖析、驗證過規則表示式無誤後,將會傳回 Pattern 實例,之後就可以重複使用這個實例。例如:

Pattern pattern = Pattern.compile(".*foo");

Pattern.compile 方法的另一版本,可以指定旗標,例如想不分大小寫比對 dog 文字,可以如下:

Pattern pattern = Pattern.compile("dog", Pattern.CASE_INSENSITIVE);

也可以在規則表示式中使用嵌入旗標表示法(Embedded Flag Expression)。例如 Pattern.CASE_INSENSITIVE 等效的嵌入旗標表示法為 (?i),以下片段效果等同上例:

Pattern pattern = Pattern.compile("(?i)dog");

並非全剖的常數旗標都有對應的嵌入式表示法,底下列出有對應的旗標:

  • Pattern.CASE_INSENSITIVE(?i)
  • Pattern.COMMENTS(?x)
  • Pattern.MULTILINE(?m)
  • Pattern.DOTALL(?s)
  • Pattern.UNICODE_CASE(?u)
  • Pattern.UNICODE_CHARACTER_CLASS(?U)
  • Pattern.UNIX_LINES(?d)

Pattern.CANON_EQPattern.LITERAL 沒有對應的嵌入式表示法。Pattern.CANON_EQ 會啟用 Canonical equivalence,簡單來說,像 å 字元(U+00E5),也會使用 a 與 ̊ 組合標示(combining mark)(U+030A)來表示,對 Pattern 來說,預設兩個是不等價的,然而啟用了 Pattern.CANON_EQ,兩者會視為相同:

jshell> var regex = Pattern.compile("a\u030A");
regex ==> a?

jshell> regex.matcher("\u00E5").find();
$2 ==> false

jshell> var regex2 = Pattern.compile("a\u030A", Pattern.CANON_EQ);
regex2 ==> a?

jshell> regex2.matcher("\u00E5").find();
$4 ==> true

注意到,"a\u030A""a\\u030A" 的差別,前者的 \u030A 是字串表示,後者是規則表示式。

如果規則表示式寫 a\u030A,就真的是比對 a 之後有個碼點為 030A 的字元(而不是被當成組合標示):

var regex2 = Pattern.compile("a\\u030A", Pattern.CANON_EQ);
out.println(regex2.matcher("\u00E5").find());  // 顯示 false

如果你使用 . 來比對 a 與 ̊ 組合,結果會是 false,在 Java 9 中,新增了一個 \X,可用來直接比對這類具有組合標示的字元:

jshell> "a\u030A".matches(".");
$5 ==> false

jshell> "a\u030A".matches("\\X");
$6 ==> true

可以使用 matcher 方法指定要比對的字串,這會傳回 java.util.regex.Matcher 實例,表示對指定字串的比對器,可以使用 find 方法看看是不是有下一個符合字串,下一篇文件會再細談 Matcher

Pattern.LITERAL 的話,在〈String 與 Regex〉中看過,可用來將全部的詮譯字元當成一般字元來比對。

在設定 Pattern.CASE_INSENSITIVE 時,可以加上 Pattern.UNICODE_CASE 啟用 Unicode 版本的忽略大小寫。例如,比較 Ä 與 ä

jshell> var regex3 = Pattern.compile("\u00C4", Pattern.CASE_INSENSITIVE);
regex3 ==> ?

jshell> regex3.matcher("\u00E4").find();
$8 ==> false

jshell> var regex4 = Pattern.compile("\u00C4", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
regex4 ==> ?

jshell> regex4.matcher("\u00E4").find();
$10 ==> true

Pattern.UNICODE_CHARACTER_CLASS 在〈特性類〉中談過,預定義與 POSIX 字元類,可以藉由設置此旗標,令其與對應的 Unicode 特性具有一致的表示。

規則表示式本身可讀性差、除錯不易,如果因規則表示式有誤而導致 compile 呼叫失敗,會拋出 java.util.regex.PatternSyntaxException,可以使用 getDescription 取得錯誤說明,使用 getIndex 取得錯誤索引,使用 getPattern 取得錯誤的規則表示式,getMessage 會以多行顯示錯誤的索引、描述等綜合訊息。

在取得 Pattern 實例後,可以使用 split 方法將指定字串依規則表示式切割,效果等同於使用 Stringsplit 方法:

jshell> var regex = Pattern.compile("\\+");
regex ==> \+

jshell> regex.split("Justin+Monica+Irene");
$12 ==> String[3] { "Justin", "Monica", "Irene" }

由於 Java 8 之後支援 Stream API,Pattern 也因應而新增了 splitAsStream 靜態方法,它傳回的是 Stream<String>,適用於需要管線化、惰性操作的場合:

jshell> var regex = Pattern.compile("\\+");
regex ==> \+

jshell> var tokens = regex.splitAsStream("Justin+Monica+Irene");
tokens ==> java.util.stream.ReferencePipeline$Head@5a1c0542

jshell> tokens.filter(token -> token.indexOf('i') != -1).map(String::toUpperCase).forEach(out::println);
JUSTIN
MONICA

Pattern 實例可以藉由 asPredicate 轉為 Predicate 物件,在需要 Predicate 作為引數的場合時可以使用:

jshell> var regex = Pattern.compile("\\+");
regex ==> \+

jshell> var tokens = regex.splitAsStream("Justin+Monica+irene");
tokens ==> java.util.stream.ReferencePipeline$Head@2833cc44

jshell> tokens.filter(Pattern.compile("\\p{Upper}+").asPredicate()).forEach(System.out::println);
Justin
Monica