- JAVA
Java正規表現の使い方|Pattern・Matcher・replaceAll
Javaの正規表現とは、java.util.regex パッケージの Pattern(コンパイル済みの正規表現)と Matcher(文字列に当てはめて検索・置換する道具)で文字列を扱う仕組みです。 String.matches・replaceAll・split も内部ではこの2つを使っています。
この記事では、matches/find/lookingAt の違い、名前付きグループ、Java 9 の results() と replaceAll(Function)、フラグ一覧、そして「置換文字列の $ で例外」「split(".") が空配列」といったつまずきを、すべて OpenJDK 21.0.10 で実行した出力つきで解説します。Pattern を毎回コンパイルした場合の速度差も計測しました。
正規表現の記号の一覧は既存記事「正規表現 早見表|記号一覧・言語別の違い・実用パターン」、日本語の文字種判定は「正規表現でひらがな・カタカナ・漢字を判定する方法【言語別】」で扱っています。本記事は Java の API の使い方 に絞ります。
Java正規表現の早見表
「やりたいこと → 書き方」の逆引きです。
| やりたいこと | 書き方 | ポイント |
|---|---|---|
| 文字列全体が形式に合うか | s.matches("\\d+") |
全体一致 |
| 同じ判定を何度も行う | static final Pattern P = Pattern.compile(...) → P.matcher(s).matches() |
使い回すと速い |
| 文字列に含まれるか | P.matcher(s).find() |
部分一致 |
| 先頭が一致するか | P.matcher(s).lookingAt() |
末尾は自由 |
| 一致をすべて取り出す | P.matcher(s).results().map(MatchResult::group).toList() |
Java 9 以降 |
| 一致を1つずつ処理 | while (m.find()) { m.group(1); } |
位置は start()/end() |
| 名前で取り出す | (?<year>\\d{4}) → m.group("year") |
置換では ${year} |
| すべて置換 | s.replaceAll("\\d+", "#") |
$ \ は特別扱い |
| 一致部分を加工して置換 | P.matcher(s).replaceAll(r -> ...) |
Java 9 以降 |
| 文字通りに置換 | s.replace("{d}", value) |
正規表現を使わない全件置換 |
| 置換文字列をそのまま使う | Matcher.quoteReplacement(value) |
$ \ をエスケープ |
| 区切り文字で分割 | s.split("\\s*,\\s*") |
末尾の空要素は消える |
| 記号を文字通りに扱う | Pattern.quote(".") |
\Q.\E になる |
| Stream で部分一致フィルタ | P.asPredicate() |
find と同じ |
| Stream で全体一致フィルタ | P.asMatchPredicate() |
Java 11 以降 |
| 大文字小文字を無視 | Pattern.CASE_INSENSITIVE または (?i) |
全角も含めるなら (?iu) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java正規表現の使い方|Pattern・Matcher・replaceAll」
Pattern と Matcher の基本
Pattern は不変でスレッドセーフなので static final で1回作って使い回し、Matcher は検索位置などの状態を持つので文字列ごとに pattern.matcher(s) で作ります。
// Basics.java(実行: java Basics.java)
import java.util.*;
import java.util.regex.*;
public class Basics {
public static void main(String[] args) {
Pattern p = Pattern.compile("(\\d{3})-(\\d{4})");
Matcher m = p.matcher("郵便番号は 123-4567 と 765-4321 です");
while (m.find()) {
System.out.println("全体=" + m.group() + " 1=" + m.group(1) + " 2=" + m.group(2)
+ " 位置=" + m.start() + "-" + m.end());
}
System.out.println("matches: " + "ABC-001".matches("[A-Z]+-\\d+"));
System.out.println("replaceAll: " + "a1b22c333".replaceAll("\\d+", "#"));
System.out.println("replaceFirst: " + "a1b22c333".replaceFirst("\\d+", "#"));
System.out.println("split: " + Arrays.toString("りんご, みかん ,ぶどう".split("\\s*,\\s*")));
System.out.println("split(limit=2): " + Arrays.toString("a:b:c:d".split(":", 2)));
System.out.println("split 末尾の空: " + Arrays.toString("a,b,,".split(",")) + " / limit=-1: " + Arrays.toString("a,b,,".split(",", -1)));
}
}
実行結果(OpenJDK 21.0.10):
全体=123-4567 1=123 2=4567 位置=6-14
全体=765-4321 1=765 2=4321 位置=17-25
matches: true
replaceAll: a#b#c#
replaceFirst: a#b22c333
split: [りんご, みかん, ぶどう]
split(limit=2): [a, b:c:d]
split 末尾の空: [a, b] / limit=-1: [a, b, , ]
group() が一致全体、group(1) 以降が丸かっこで囲んだ部分です。end() は終了位置の「次」を返します。Java の文字列リテラルでは \ 自体をエスケープするため、正規表現の \d は "\\d" と書きます。
split は既定(limit=0)で末尾の空文字列を削除します。 末尾の空欄も要素として必要なら split(",", -1) とします。
String のメソッドとの関係
String の正規表現メソッドは、呼ぶたびに内部で Pattern.compile を行う「1回きり用のショートカット」です(String の Javadoc に同等の式が記載されています)。
| String のメソッド | 同じ意味の書き方 |
|---|---|
s.matches(regex) |
Pattern.matches(regex, s) |
s.replaceAll(regex, rep) |
Pattern.compile(regex).matcher(s).replaceAll(rep) |
s.replaceFirst(regex, rep) |
Pattern.compile(regex).matcher(s).replaceFirst(rep) |
s.split(regex, limit) |
Pattern.compile(regex).split(s, limit) |
s.replace(target, rep) |
正規表現を使わない(文字通り・全件) |
ループ内や頻繁に呼ばれる処理では static final Pattern を使い回すのが基本です(速度差は後半で計測します)。
matches・find・lookingAt の違い(実行結果つき)
違いは「どこからどこまで一致していればよいか」です。 matches は全体、lookingAt は先頭から、find はどこか一部です。
// MatchKinds.java
import java.util.regex.*;
public class MatchKinds {
public static void main(String[] args) {
String[][] cases = {
{"\\d+", "2026"}, {"\\d+", "2026年"}, {"\\d+", "令和8年は2026年"},
{"年", "2026年"}, {"[a-z]+", "ABC"},
};
System.out.println("正規表現 | 入力 | matches | lookingAt | find");
for (String[] c : cases) {
Pattern p = Pattern.compile(c[0]);
boolean mt = p.matcher(c[1]).matches();
boolean la = p.matcher(c[1]).lookingAt();
Matcher f = p.matcher(c[1]);
boolean fd = f.find();
System.out.println(c[0] + " | " + c[1] + " | " + mt + " | " + la + " | " + fd + (fd ? "(" + f.group() + ")" : ""));
}
}
}
実行結果(OpenJDK 21.0.10):
正規表現 | 入力 | matches | lookingAt | find
\d+ | 2026 | true | true | true(2026)
\d+ | 2026年 | false | true | true(2026)
\d+ | 令和8年は2026年 | false | false | true(8)
年 | 2026年 | false | false | true(年)
[a-z]+ | ABC | false | false | false
| 正規表現 | 入力 | matches(全体) | lookingAt(先頭) | find(どこか) |
|---|---|---|---|---|
\d+ |
2026 |
true | true | true(2026) |
\d+ |
2026年 |
false | true | true(2026) |
\d+ |
令和8年は2026年 |
false | false | true(最初の 8) |
年 |
2026年 |
false | false | true(年) |
[a-z]+ |
ABC |
false | false | false |
matchesは暗黙に^...$を付けたのと同じです。部分一致ならfind()を使います。findは左から探して最初の1つで止まります。 3行目は2026ではなく8です。すべて取るならwhile (m.find())かresults()。lookingAtは接頭辞の判定向きです(ORD-で始まる注文番号か、など)。
| 目的 | 選ぶメソッド |
|---|---|
| 入力欄の形式チェック(郵便番号、会員ID) | matches |
| 文章に特定の語が含まれるか | find |
| 先頭の書式だけ確認 | lookingAt |
| 一致箇所をすべて抽出 | results() / while (find()) |
グループ・results()・replaceAll(Function)
丸かっこ ( ) は「キャプチャグループ」になり、番号(左かっこの出現順)または名前 (?<name>...) で取り出せます。 Java 9 の results() と replaceAll(Function) を組み合わせると、抽出と加工つき置換が短く書けます。
// Groups.java
import java.util.*;
import java.util.regex.*;
public class Groups {
public static void main(String[] args) {
Pattern date = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = date.matcher("締切は 2026-10-02 です");
if (m.find()) {
System.out.println("year=" + m.group("year") + " month=" + m.group("month") + " day=" + m.group("day"));
System.out.println("namedGroups=" + new TreeMap<>(m.namedGroups()));
}
System.out.println("2026-10-02".replaceAll("(?<y>\\d{4})-(?<m>\\d{2})-(?<d>\\d{2})", "${y}年${m}月${d}日"));
System.out.println("2026-10-02".replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1"));
Matcher nc = Pattern.compile("(?:ab)+(c)").matcher("ababc");
if (nc.matches()) System.out.println("groupCount=" + nc.groupCount() + " group(1)=" + nc.group(1));
// Matcher.results() (Java 9)
List<String> tags = Pattern.compile("#(\\w+)").matcher("#java と #regex と #stream")
.results().map(r -> r.group(1)).toList();
System.out.println("results=" + tags);
System.out.println("o の数=" + Pattern.compile("o").matcher("foo boo").results().count());
// replaceAll(Function) (Java 9)
System.out.println("replaceAll(Function)=" + Pattern.compile("\\d+").matcher("在庫 3 個、予約 12 件")
.replaceAll(r -> String.valueOf(Integer.parseInt(r.group()) * 2)));
System.out.println("camel=" + Pattern.compile("_([a-z])").matcher("order_item_count")
.replaceAll(r -> r.group(1).toUpperCase()));
// asPredicate / asMatchPredicate
List<String> codes = List.of("A-100", "A-100x", "xA-100", "B-7");
Pattern code = Pattern.compile("[A-Z]-\\d+");
System.out.println("asPredicate(find)=" + codes.stream().filter(code.asPredicate()).toList());
System.out.println("asMatchPredicate(matches)=" + codes.stream().filter(code.asMatchPredicate()).toList());
System.out.println("splitAsStream=" + Pattern.compile("\\s*;\\s*").splitAsStream("a ; b;c").toList());
System.out.println("splitWithDelimiters=" + Arrays.toString("1+2-3".splitWithDelimiters("[+-]", 0)));
}
}
実行結果(OpenJDK 21.0.10):
year=2026 month=10 day=02
namedGroups={day=3, month=2, year=1}
2026年10月02日
02/10/2026
groupCount=1 group(1)=c
results=[java, regex, stream]
o の数=4
replaceAll(Function)=在庫 6 個、予約 24 件
camel=orderItemCount
asPredicate(find)=[A-100, A-100x, xA-100, B-7]
asMatchPredicate(matches)=[A-100, B-7]
splitAsStream=[a, b, c]
splitWithDelimiters=[1, +, 2, -, 3]
| 書き方 | 意味 | 取り出し/置換での参照 |
|---|---|---|
(...) |
キャプチャグループ | m.group(1)/$1 |
(?<name>...) |
名前付きグループ | m.group("name")/${name} |
(?:...) |
非キャプチャ(まとめるだけ) | 番号が振られない |
\1、\k<name> |
後方参照 | パターン内で使う |
m.namedGroups() |
名前→番号の Map | Java 20 以降 |
m.hasMatch() |
直前の検索で一致したか | Java 20 以降 |
(?:...)はまとめたいだけのかっこに番号を振りません。 上の例ではgroupCount=1で、(c)が1番です。results()は一致ごとのMatchResultを Stream で返します。 ループを書かずにmap/countで処理できます(Stream の基本は既存記事「Java Stream APIの使い方」を参照)。replaceAll(Function)はラムダの戻り値で置き換えます。 ただし戻り値も置換文字列として扱われ$\が特別な意味を持つので、外部の値を含めるならMatcher.quoteReplacement(...)で包みます。asPredicate()は find(部分一致)、asMatchPredicate()は matches(全体一致)。xA-100は前者では残り、後者では除外されました。形式チェックならasMatchPredicate()です。
辞書から値を差し込むテンプレート処理では、appendReplacement/appendTail(Java 9 から StringBuilder 版あり)も読みやすい書き方です。
// Template.java
import java.util.*;
import java.util.regex.*;
public class Template {
public static void main(String[] args) {
Matcher m = Pattern.compile("\\{(\\w+)\\}").matcher("こんにちは{name}さん、注文番号は{id}です");
Map<String, String> vars = Map.of("name", "山田", "id", "A-$100");
StringBuilder sb = new StringBuilder();
while (m.find()) {
m.appendReplacement(sb, Matcher.quoteReplacement(vars.getOrDefault(m.group(1), "?")));
}
m.appendTail(sb);
System.out.println(sb);
}
}
実行結果(OpenJDK 21.0.10):
こんにちは山田さん、注文番号はA-$100です
フラグ一覧(CASE_INSENSITIVE・MULTILINE・DOTALL など)
フラグは Pattern.compile(regex, フラグ) の第2引数か、正規表現先頭の (?i) のような埋め込み形式で指定します。 複数は | でつなぎます。埋め込み形式は String.matches などでも使えます。
| 定数 | 埋め込み | 効果 |
|---|---|---|
CASE_INSENSITIVE |
(?i) |
大文字小文字を無視(既定は US-ASCII のみ) |
UNICODE_CASE |
(?u) |
上記を Unicode 全体に広げる |
MULTILINE |
(?m) |
^ $ が各行の先頭・末尾に一致 |
DOTALL |
(?s) |
. が改行にも一致 |
UNICODE_CHARACTER_CLASS |
(?U) |
\w \d \s を Unicode 基準に(UNICODE_CASE も有効) |
COMMENTS |
(?x) |
空白を無視し # 以降をコメントに |
LITERAL |
なし | 記号を文字通りに扱う |
UNIX_LINES |
(?d) |
行末を \n だけと認識 |
CANON_EQ |
なし | 合成文字と分解文字を同一視 |
// Flags.java
import java.util.regex.*;
public class Flags {
static void show(String label, Pattern p, String s) {
Matcher m = p.matcher(s);
StringBuilder sb = new StringBuilder();
while (m.find()) sb.append("[").append(m.group().replace("\n", "\\n")).append("]");
System.out.println(label + " -> " + (sb.length() == 0 ? "(一致なし)" : sb));
}
public static void main(String[] args) {
show("フラグなし java", Pattern.compile("java"), "Java JAVA java");
show("CASE_INSENSITIVE", Pattern.compile("java", Pattern.CASE_INSENSITIVE), "Java JAVA java");
show("ABC (?i)のみ", Pattern.compile("(?i)abc"), "ABC");
show("ABC (?iu)", Pattern.compile("(?iu)abc"), "ABC");
String text = "line1\nline2\nline3";
show("^line\\d フラグなし", Pattern.compile("^line\\d"), text);
show("^line\\d MULTILINE", Pattern.compile("^line\\d", Pattern.MULTILINE), text);
show("a.b フラグなし", Pattern.compile("a.b"), "a\nb");
show("a.b DOTALL", Pattern.compile("a.b", Pattern.DOTALL), "a\nb");
show("\\w+ フラグなし", Pattern.compile("\\w+"), "café 日本語 abc");
show("\\w+ UNICODE_CHARACTER_CLASS", Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS), "café 日本語 abc");
show("\\d フラグなし", Pattern.compile("\\d+"), "123 456");
show("\\d UNICODE_CHARACTER_CLASS", Pattern.compile("\\d+", Pattern.UNICODE_CHARACTER_CLASS), "123 456");
Pattern commented = Pattern.compile("""
(?<year>\\d{4}) # 年
- # 区切り
(?<month>\\d{2}) # 月
""", Pattern.COMMENTS);
show("COMMENTS", commented, "2026-10");
show("LITERAL", Pattern.compile("a.b", Pattern.LITERAL), "axb a.b");
show("CASE_INSENSITIVE|MULTILINE", Pattern.compile("^error", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE), "Error: x\nERROR: y\nok");
}
}
実行結果(OpenJDK 21.0.10):
フラグなし java -> [java]
CASE_INSENSITIVE -> [Java][JAVA][java]
ABC (?i)のみ -> (一致なし)
ABC (?iu) -> [ABC]
^line\d フラグなし -> [line1]
^line\d MULTILINE -> [line1][line2][line3]
a.b フラグなし -> (一致なし)
a.b DOTALL -> [a\nb]
\w+ フラグなし -> [caf][abc]
\w+ UNICODE_CHARACTER_CLASS -> [café][日本語][abc]
\d フラグなし -> [456]
\d UNICODE_CHARACTER_CLASS -> [123][456]
COMMENTS -> [2026-10]
LITERAL -> [a.b]
CASE_INSENSITIVE|MULTILINE -> [Error][ERROR]
CASE_INSENSITIVEだけでは全角英字の大小は同一視されません。(?iu)で一致しました。- Java の
\wと\dは既定で ASCII だけです。éや「日本語」、全角「123」は含まれず、UNICODE_CHARACTER_CLASSで含まれました。半角数字だけを許可したい形式チェックでは[0-9]と明示すると意図が伝わります。 MULTILINEなしの^は文字列全体の先頭だけ、.は既定で改行に一致しません(DOTALLで一致)。COMMENTSとテキストブロックで、長い正規表現にコメントを書けます。
よくある失敗と直し方(実行して再現)
// Pitfalls.java
import java.util.*;
import java.util.regex.*;
public class Pitfalls {
public static void main(String[] args) {
String price = "$100";
try {
System.out.println("価格: {p}".replaceAll("\\{p\\}", price));
} catch (Exception e) {
System.out.println("NG: " + e.getClass().getName() + ": " + e.getMessage());
}
System.out.println("OK: " + "価格: {p}".replaceAll("\\{p\\}", Matcher.quoteReplacement(price)));
String path = "C:\\temp";
System.out.println("保存先: {d}".replaceAll("\\{d\\}", path));
System.out.println("OK: " + "保存先: {d}".replaceAll("\\{d\\}", Matcher.quoteReplacement(path)));
System.out.println("OK(replace): " + "保存先: {d}".replace("{d}", path));
System.out.println("split(\".\") = " + Arrays.toString("192.168.0.1".split(".")) + " 要素数=" + "192.168.0.1".split(".").length);
System.out.println("split(\"\\\\.\") = " + Arrays.toString("192.168.0.1".split("\\.")));
System.out.println("split(Pattern.quote(\".\")) = " + Arrays.toString("192.168.0.1".split(Pattern.quote("."))));
System.out.println("split(\"|\") = " + Arrays.toString("a|b|c".split("|")));
System.out.println("Pattern.quote(\"1+1=2?\") = " + Pattern.quote("1+1=2?"));
for (String b : new String[]{"(abc", "[a-", "*abc", "a{2,1}"}) {
try {
Pattern.compile(b);
} catch (PatternSyntaxException e) {
System.out.println("---- " + b);
System.out.println(e.getMessage());
System.out.println("getDescription=" + e.getDescription() + " getIndex=" + e.getIndex());
}
}
System.out.println("\"abc123\".matches(\"\\\\d+\") = " + "abc123".matches("\\d+"));
System.out.println("貪欲 <.+> : " + Pattern.compile("<.+>").matcher("<b>太字</b>").results().map(MatchResult::group).toList());
System.out.println("最短 <.+?> : " + Pattern.compile("<.+?>").matcher("<b>太字</b>").results().map(MatchResult::group).toList());
try {
Pattern.compile("\\d").matcher("a1").group();
} catch (IllegalStateException e) {
System.out.println("NG: " + e.getClass().getName() + ": " + e.getMessage());
}
}
}
実行結果(OpenJDK 21.0.10):
NG: java.lang.IndexOutOfBoundsException: No group 1
OK: 価格: $100
保存先: C:temp
OK: 保存先: C:\temp
OK(replace): 保存先: C:\temp
split(".") = [] 要素数=0
split("\\.") = [192, 168, 0, 1]
split(Pattern.quote(".")) = [192, 168, 0, 1]
split("|") = [a, |, b, |, c]
Pattern.quote("1+1=2?") = \Q1+1=2?\E
---- (abc
Unclosed group near index 4
(abc
getDescription=Unclosed group getIndex=4
---- [a-
Illegal character range near index 3
[a-
getDescription=Illegal character range getIndex=3
---- *abc
Dangling meta character '*' near index 0
*abc
^
getDescription=Dangling meta character '*' getIndex=0
---- a{2,1}
Illegal repetition range near index 5
a{2,1}
^
getDescription=Illegal repetition range getIndex=5
"abc123".matches("\\d+") = false
貪欲 <.+> : [<b>太字</b>]
最短 <.+?> : [<b>, </b>]
NG: java.lang.IllegalStateException: No match found
置換文字列の $ と \ で例外・文字が消える
replaceAll の置換文字列では、$1 や ${name} がグループ参照、\ が次の1文字のエスケープとして解釈されます。 そのため外部の値をそのまま渡すと、次のようになりました(後半2行は別途 "$name" と "C:\\temp\\" を置換文字列に渡して確認)。
| 置換文字列 | 結果 |
|---|---|
$100 |
IndexOutOfBoundsException: No group 1 |
$name |
IllegalArgumentException: Illegal group reference |
C:\temp |
例外にならず C:temp(\ が消える) |
C:\temp\(末尾が \) |
IllegalArgumentException: character to be escaped is missing |
直し方は、置換文字列を Matcher.quoteReplacement() で包むか、正規表現が不要なら String.replace を使うことです。 String.replace(CharSequence, CharSequence) は正規表現を使わない全件置換です。
// 直す前
"価格: {p}".replaceAll("\\{p\\}", price); // 例外
// 直した後
"価格: {p}".replaceAll("\\{p\\}", Matcher.quoteReplacement(price)); // 価格: $100
"価格: {p}".replace("{p}", price); // 価格: $100
split(".") が空の配列になる
split の引数は正規表現なので、. は「任意の1文字」です。 全文字が区切りになり、末尾の空要素が削除されて要素数0になりました。split("|") も「空文字列か空文字列」と解釈され、1文字ずつに分かれます。
| 書き方 | "192.168.0.1" の結果 |
|---|---|
split(".") |
[](要素数0) |
split("\\.") |
[192, 168, 0, 1] |
split("[.]") |
[192, 168, 0, 1] |
split(Pattern.quote(".")) |
[192, 168, 0, 1] |
+ * ? ( [ { ^ $ \ も同様です。区切り文字が変数で渡ってくるなら Pattern.quote() で包みます。 \Q〜\E の間はすべて文字通りに扱われます。
PatternSyntaxException の読み方
文法の誤りは Pattern.compile(または matches/replaceAll/split)の実行時に PatternSyntaxException(非チェック例外)になります。 near index N と次行の ^ が位置を示します。
| パターン | getDescription | 直し方 |
|---|---|---|
(abc |
Unclosed group | (abc) または \\(abc |
[a- |
Illegal character range | [a-z] または [a\\-] |
*abc |
Dangling meta character '*' | \\*abc |
a{2,1} |
Illegal repetition range | a{1,2} |
利用者の入力から正規表現を組み立てるときは、Pattern.quote() で包むか、この例外を捕捉して入力エラーとして返します。
その他のつまずきチェックリスト
| 症状 | 原因 | 対処 |
|---|---|---|
matches が false |
全体一致だった | find() に変える |
<.+> が取りすぎる |
+ * は貪欲 |
+? か <[^>]+> |
IllegalStateException: No match found |
一致確認前に group() |
if (m.find()) の中で取る |
日本語が \w に一致しない |
既定は ASCII | UNICODE_CHARACTER_CLASS |
複数行で ^ が1回だけ |
MULTILINE なし |
(?m) |
| 改行をまたがない | . は改行に不一致 |
(?s) |
| 処理が遅い | ループ内で String.matches |
static final Pattern |
Pattern を毎回 compile すると遅い?速度差を計測
String.matches は呼ぶたびに Pattern.compile を実行します。 100万回の判定で、使い回した場合と比べました。
// Bench.java
import java.util.regex.*;
public class Bench {
static final Pattern CODE = Pattern.compile("[A-Z]{2}-\\d{4}");
public static void main(String[] args) {
String[] inputs = {"AB-1234", "xy-0000", "CD-99999", "EF-0001"};
int n = 1_000_000;
for (int round = 1; round <= 3; round++) {
long t0 = System.nanoTime();
int c1 = 0;
for (int i = 0; i < n; i++) if (inputs[i & 3].matches("[A-Z]{2}-\\d{4}")) c1++;
long t1 = System.nanoTime();
int c2 = 0;
for (int i = 0; i < n; i++) if (CODE.matcher(inputs[i & 3]).matches()) c2++;
long t2 = System.nanoTime();
System.out.printf("round %d: String.matches 毎回compile = %d ms (%d件) / static Pattern 使い回し = %d ms (%d件)%n",
round, (t1 - t0) / 1_000_000, c1, (t2 - t1) / 1_000_000, c2);
}
}
}
実行結果(OpenJDK 21.0.10、共有の検証用コンテナで2回実行):
# 1回目
round 1: String.matches 毎回compile = 1262 ms (500000件) / static Pattern 使い回し = 111 ms (500000件)
round 2: String.matches 毎回compile = 567 ms (500000件) / static Pattern 使い回し = 95 ms (500000件)
round 3: String.matches 毎回compile = 951 ms (500000件) / static Pattern 使い回し = 187 ms (500000件)
# 2回目
round 1: String.matches 毎回compile = 1492 ms (500000件) / static Pattern 使い回し = 537 ms (500000件)
round 2: String.matches 毎回compile = 1318 ms (500000件) / static Pattern 使い回し = 183 ms (500000件)
round 3: String.matches 毎回compile = 902 ms (500000件) / static Pattern 使い回し = 75 ms (500000件)
ウォームアップ後(round 2〜3)で、使い回しはおおむね5〜12倍速い結果でした。 共有環境のため数値は目安です(厳密な比較には JMH を使います)。
| 呼び出し回数 | おすすめ |
|---|---|
| 起動時に1回だけ | String.matches で十分 |
| リクエストごと・行ごと・ループ内 | private static final Pattern |
| 正規表現が実行時に変わる | 変わるたびに compile し、同じものは Map で再利用 |
split(",") のように「正規表現の記号を含まない1文字」の区切りは、String.split の内部で正規表現を使わない高速な経路が使われるため、String 版のままで問題ありません。
Java 21 と Java 25 の差分(公式ドキュメントで確認)
本記事のコードはすべて OpenJDK 21.0.10 で実行しました。Java 25 は手元で実行していません。Java 25 の Javadoc(java.util.regex.Matcher)では、Java 9 以降の追加は results()・replaceAll(Function)・replaceFirst(Function)・StringBuilder 版 appendReplacement/appendTail(Java 9)と namedGroups()・hasMatch()(Java 20)で、Java 21 より後に Matcher へ追加されたメソッドはありませんでした。Java 21 では String.splitWithDelimiters/Pattern.splitWithDelimiters が加わっています。Java 21 で動く本記事のコードは、Java 25 でもそのまま使えます。
よくある質問
Q. String.matches と Pattern.matcher().matches() は結果が違いますか?
A. 結果は同じです。String.matches は内部で毎回 Pattern をコンパイルするため、何度も呼ぶ処理では static final Pattern を使い回すと速くなります(本記事の計測で5〜12倍)。
Q. 部分一致を判定するには?
A. Pattern.compile(regex).matcher(s).find() を使います。Stream の filter なら asPredicate() が部分一致、asMatchPredicate() が全体一致です。
Q. replaceAll で $ を含む文字列に置き換えると例外になります。
A. 置換文字列の $ はグループ参照だからです。Matcher.quoteReplacement(value) で包むか、String.replace を使います。\ は例外にならず消えることがあるので、同じく包みます。
Q. 日本語を \w で一致させたいのですが。
A. Java の \w は既定で ASCII のみです。Pattern.UNICODE_CHARACTER_CLASS((?U))を付けるか、\p{IsHiragana} などの Unicode プロパティを使います。
まとめ
- Pattern はコンパイル済みで使い回し、Matcher は文字列ごとに作るのが基本です。
matchesは全体一致、lookingAtは先頭一致、findは部分一致です。- 名前付きグループ・
results()・replaceAll(Function)で抽出と加工つき置換が短く書けます。 \w\dは既定で ASCII のみ。 日本語や全角はUNICODE_CHARACTER_CLASS、全角英字の大小は(?iu)です。- 置換文字列は
Matcher.quoteReplacement、区切り文字はPattern.quoteで文字通りに扱えます。 split(".")は空配列になるので\\.かPattern.quoteを使います。- ループ内の
String.matchesをstatic final Patternに変えると、計測で5〜12倍速くなりました。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java正規表現の使い方|Pattern・Matcher・replaceAll」
株式会社RJCでは、Javaの正規表現の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。