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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?