この記事で得られる知識
この記事を通じて、以下のスキルや知識を習得できます。
- Javaにおける正規表現の基本的な使い方と考え方
- 正規表現処理の核となる
Pattern
クラスとMatcher
クラスの役割と連携方法 - メールアドレス形式のチェックや特定の文字列の抽出など、実用的な正規表現パターンの書き方
- 文字列の検索、置換、分割といった具体的な操作方法
- 正規表現を扱う上で重要なパフォーマンス最適化のヒントと注意点
はじめに:正規表現とは?
プログラミングにおける文字列処理は、避けては通れないタスクの一つです。ユーザー入力の検証、ログファイルの解析、特定のフォーマットのデータ抽出など、様々な場面で文字列を操作する必要があります。こうした状況で絶大なパワーを発揮するのが「正規表現(Regular Expression)」です。
正規表現とは、特定のルール(パターン)を持つ文字列の集合を表現するための形式言語です。例えば、「アルファベットで始まり、数字で終わる文字列」や「有効なメールアドレスの形式」といった複雑な条件を、短いパターン文字列で簡潔に表現できます。
Javaでは、標準ライブラリとしてjava.util.regex
パッケージが提供されており、これを用いることで強力な正規表現処理を実装できます。このガイドでは、このパッケージの核心であるPattern
クラスとMatcher
クラスを中心に、Javaでの正規表現の扱い方を基礎から応用まで、豊富なコード例とともに徹底的に解説していきます。
java.util.regex
の中心的存在:PatternとMatcher
Javaで正規表現を扱う際、中心となるのがPattern
クラスとMatcher
クラスです。 これら2つのクラスは密接に連携して動作します。まずはそれぞれの役割を正確に理解しましょう。
Patternクラス:正規表現をコンパイルする
Pattern
クラスは、文字列で記述された正規表現パターンを、Javaが効率的に処理できる形式に「コンパイル」する役割を担います。 正規表現をプログラム内で使用するには、まずそのパターンをPattern
オブジェクトに変換する必要があります。
コンパイルは、Pattern.compile()
というstaticメソッドを使って行います。
import java.util.regex.Pattern;
// 「1個以上の数字」という正規表現パターンをコンパイル
Pattern pattern = Pattern.compile("\\d+");
パフォーマンスの鍵:Patternオブジェクトの再利用
Pattern.compile()
の処理は、それなりにコストがかかります。そのため、同じ正規表現パターンを繰り返し使用する場合は、毎回コンパイルするのではなく、一度生成したPattern
オブジェクトを再利用することがパフォーマンス向上の鍵となります。 一般的には、クラスのstatic finalフィールドとして保持しておくと良いでしょう。
public class MyValidator { // static finalフィールドとして一度だけコンパイルし、再利用する private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); public boolean isValidEmail(String email) { if (email == null) { return false; } // 再利用したPatternオブジェクトからMatcherを生成 return EMAIL_PATTERN.matcher(email).matches(); }
}
Matcherクラス:パターンを適用し、結果を得る
Matcher
クラスは、コンパイルされたPattern
オブジェクトを、特定の入力文字列に適用(マッチング)するためのエンジンです。 実際に文字列がパターンに一致するかどうかを調べたり、一致した部分を抽出したりするのは、このMatcher
クラスの役割です。
Matcher
オブジェクトは、Pattern
オブジェクトのmatcher()
メソッドに、検査対象の文字列を渡すことで生成されます。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
String text = "私の電話番号は080-1234-5678です。";
Pattern pattern = Pattern.compile("\\d{2,4}-\\d{2,4}-\\d{4}");
// PatternオブジェクトからMatcherオブジェクトを生成
Matcher matcher = pattern.matcher(text);
このmatcher
オブジェクトを使って、文字列がパターンに一致するかどうかを確認したり、一致箇所を操作したりします。
まとめると、Patternで「型紙」を作り、Matcherでその型紙を文字列に「当ててみる」、というイメージを持つと分かりやすいでしょう。
基本的な使い方:3つのマッチングメソッド
Matcher
クラスには、文字列とパターンのマッチングを行うための主要なメソッドが3つあります。 これらは似ているようで挙動が異なるため、目的に応じて正しく使い分けることが重要です。
1. matches()
– 完全一致
matches()
メソッドは、入力文字列全体が正規表現パターンに完全に一致するかを判定します。 文字列の一部だけが一致してもfalse
を返します。入力値全体のフォーマット検証など、厳密なチェックが必要な場合に用います。
Pattern p = Pattern.compile("Hello, World!");
Matcher m1 = p.matcher("Hello, World!");
System.out.println("m1.matches(): " + m1.matches()); // true
Matcher m2 = p.matcher("Hello, World! Extra");
System.out.println("m2.matches(): " + m2.matches()); // false (余分な文字列があるため)
Matcher m3 = p.matcher("Hello,");
System.out.println("m3.matches(): " + m3.matches()); // false (文字列が足りないため)
2. lookingAt()
– 前方一致
lookingAt()
メソッドは、入力文字列の先頭からパターンに一致するかを判定します。 文字列の末尾に余分な文字があっても、先頭部分が一致していればtrue
を返します。
Pattern p = Pattern.compile("Hello");
Matcher m1 = p.matcher("Hello, World!");
System.out.println("m1.lookingAt(): " + m1.lookingAt()); // true
Matcher m2 = p.matcher("Good morning, Hello!");
System.out.println("m2.lookingAt(): " + m2.lookingAt()); // false (先頭が一致しないため)
3. find()
– 部分一致(検索)
find()
メソッドは、入力文字列の中にパターンに一致する部分があるかを検索します。 matches()
やlookingAt()
と異なり、文字列のどこにあっても一致箇所を見つけ出します。
このメソッドの最大の特徴は、繰り返し呼び出すことで、次の一致箇所を順番に検索できる点です。 while
ループと組み合わせて、一致するすべての箇所を処理する、といった使い方が一般的です。
String text = "リンゴはapple、オレンジもorange、バナナもbananaです。";
Pattern p = Pattern.compile("[a-z]+"); // 1文字以上の英小文字
Matcher m = p.matcher(text);
while (m.find()) { // find()がtrueを返す限り、一致箇所が見つかったことを意味する System.out.println("一致した部分: " + m.group()); System.out.println(" 開始位置: " + m.start()); System.out.println(" 終了位置: " + m.end());
}
// 実行結果:
// 一致した部分: apple
// 開始位置: 5
// 終了位置: 10
// 一致した部分: orange
// 開始位置: 18
// 終了位置: 24
// 一致した部分: banana
// 開始位置: 31
// 終了位置: 37
find()
で見つかった一致文字列はgroup()
メソッドで取得できます。また、start()
とend()
でその位置(インデックス)を取得できます。
正規表現の構文をマスターしよう
正規表現の力を最大限に引き出すには、その構文を理解することが不可欠です。ここでは、Javaの正規表現でよく使われる主要な構文要素を表形式で紹介します。
注意: Javaの文字列リテラル内でバックスラッシュ \
を記述する場合、エスケープ文字として扱われるため、\\
のように2つ重ねて記述する必要があります。例えば、正規表現で数字1文字を表す \d
は、Javaコード内では "\\d"
と書きます。
文字とメタ文字
構文 | 説明 | 例 |
---|---|---|
. | 改行文字を除く任意の1文字にマッチします。 (DOTALL フラグ指定時を除く) | a.c は “abc”, “a3c”, “a@c” などにマッチ |
^ | 行または文字列の先頭にマッチします。 (MULTILINE フラグの影響を受ける) | ^Java は “Java is great” の先頭の “Java” にマッチ |
$ | 行または文字列の末尾にマッチします。 (MULTILINE フラグの影響を受ける) | end$ は “This is the end” の末尾の “end” にマッチ |
[abc] | 角括弧内のいずれか1文字にマッチします(文字クラス)。 | [bcr]at は “bat”, “cat”, “rat” にマッチ |
[^abc] | 角括弧内の文字以外のいずれか1文字にマッチします(否定文字クラス)。 | [^bcr]at は “hat”, “mat” にはマッチするが “cat” にはマッチしない |
[a-zA-Z] | ハイフン(-)で範囲を指定できます。この例では任意のアルファベット1文字にマッチします。 | [0-9] は任意の数字1文字にマッチ |
A|B | AまたはBのいずれかにマッチします(OR条件)。 | cat|dog は “cat” または “dog” にマッチ |
(...) | パターンをグループ化します。量指定子を適用したり、後方参照のために部分文字列をキャプチャします。 | (dog)+ は “dog”, “dogdog”, “dogdogdog” などにマッチ |
定義済みの文字クラス
構文 | 説明 |
---|---|
\d | 任意の数字1文字にマッチします。[0-9] と等価です。 |
\D | 数字以外の任意の1文字にマッチします。[^0-9] と等価です。 |
\s | 空白文字(スペース、タブ、改行など)にマッチします。 |
\S | 非空白文字にマッチします。 |
\w | 単語構成文字(英数字とアンダースコア)にマッチします。[a-zA-Z_0-9] と等価です。 |
\W | 非単語構成文字にマッチします。 |
量指定子
量指定子は、直前の文字やグループが何回繰り返すかを指定します。
構文 | 説明 | 種類 |
---|---|---|
X* | Xが0回以上繰り返す場合にマッチ(最長一致) | 貪欲 (Greedy) |
X+ | Xが1回以上繰り返す場合にマッチ(最長一致) | |
X? | Xが0回または1回の場合にマッチ | |
X{n} | Xがちょうどn回繰り返す場合にマッチ | |
X{n,} | Xがn回以上繰り返す場合にマッチ(最長一致) | |
X{n,m} | Xがn回以上m回以下繰り返す場合にマッチ(最長一致) | |
X*? | Xが0回以上繰り返す場合にマッチ(最短一致) | 非貪欲 (Reluctant) |
X+? | Xが1回以上繰り返す場合にマッチ(最短一致) | |
X?? | Xが0回または1回の場合にマッチ(最短一致) |
デフォルトの量指定子(*
, +
, {n,}
など)は「貪欲(Greedy)」あるいは「最長一致」と呼ばれ、可能な限り長い文字列にマッチしようとします。 一方、量指定子の後ろに ?
をつけると「非貪欲(Reluctant)」あるいは「最短一致」となり、可能な限り短い文字列にマッチしようとします。
String text = "<p>first</p><p>second</p>";
Pattern greedy = Pattern.compile("<p>.*</p>"); // 最長一致
Matcher mGreedy = greedy.matcher(text);
if(mGreedy.find()){ System.out.println("最長一致: " + mGreedy.group(0));
}
// 出力: 最長一致: <p>first</p><p>second</p>
Pattern reluctant = Pattern.compile("<p>.*?</p>"); // 最短一致
Matcher mReluctant = reluctant.matcher(text);
while(mReluctant.find()){ System.out.println("最短一致: " + mReluctant.group(0));
}
// 出力:
// 最短一致: <p>first</p>
// 最短一致: <p>second</p>
グループ化と後方参照
正規表現の一部を丸括弧 ()
で囲むと、「キャプチャグループ」が作成されます。 これにより、マッチした文字列全体だけでなく、グループにマッチした部分文字列を後から参照(取得)することができます。
グループは、正規表現中の左括弧の出現順に1から番号が付けられます。 group(0)
は常にマッチした文字列全体を返します。
String text = "2025-07-12";
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher(text);
if (m.find()) { System.out.println("マッチ全体 (group 0): " + m.group(0)); // 2025-07-12 System.out.println("年 (group 1) : " + m.group(1)); // 2025 System.out.println("月 (group 2) : " + m.group(2)); // 07 System.out.println("日 (group 3) : " + m.group(3)); // 12
}
置換処理では、$n
という形式でn番目のキャプチャグループを参照できます。これを後方参照と呼びます。
String text = "John Smith";
// 姓と名を入れ替える
String result = text.replaceAll("(\\w+)\\s(\\w+)", "$2, $1");
System.out.println(result); // Smith, John
名前付きキャプチャグループ
Java 7からは、グループに名前を付けることができるようになりました。 (?<name>...)
という構文でグループに名前を定義し、matcher.group("name")
でそのグループがキャプチャした文字列を取得できます。 これにより、グループの番号を数える必要がなくなり、コードの可読性が大幅に向上します。
String text = "Date: 2025-07-12";
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher(text);
if (m.find()) { System.out.println("年: " + m.group("year")); // 2025 System.out.println("月: " + m.group("month")); // 07 System.out.println("日: " + m.group("day")); // 12
}
コンパイルフラグ
Pattern.compile()
メソッドの第2引数にフラグを指定することで、正規表現の挙動を調整できます。複数のフラグは |
で連結して指定します。
フラグ | 埋め込みフラグ | 説明 |
---|---|---|
Pattern.CASE_INSENSITIVE | (?i) | 大文字・小文字を区別せずにマッチングを行います。 |
Pattern.MULTILINE | (?m) | 複数行モード。^ と$ が、文字列全体の先頭・末尾だけでなく、各行の行頭・行末にもマッチするようになります。 |
Pattern.DOTALL | (?s) | ドット(. )が改行文字を含むすべての文字にマッチするようになります。 |
Pattern.COMMENTS | (?x) | パターン内の空白文字が無視され、# 以降行末までがコメントとして扱われます。複雑な正規表現を読みやすく記述できます。 |
Pattern.UNICODE_CASE | (?u) | CASE_INSENSITIVE と併用し、Unicode全体を考慮した大文字・小文字の区別を無視します。 |
// 大文字・小文字を区別せず、複数行モードで検索する例
String text = "Java is a programming language.\njava is powerful.";
Pattern p = Pattern.compile("^java", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher(text);
while (m.find()) { System.out.println("Found: '" + m.group() + "' at index " + m.start());
}
// 実行結果:
// Found: 'Java' at index 0
// Found: 'java' at index 31
応用的な使い方:文字列の置換と分割
正規表現は文字列のマッチングだけでなく、置換や分割といった操作にも非常に強力です。
文字列の置換
Matcher
クラスのreplaceAll()
とreplaceFirst()
メソッドを使用することで、パターンにマッチした部分を別の文字列に置換できます。
replaceAll(String replacement)
: マッチしたすべての部分を置換します。replaceFirst(String replacement)
: マッチした最初の部分のみを置換します。
String text = "電話番号は03-1111-2222、携帯は090-3333-4444です。";
Pattern p = Pattern.compile("\\d{2,4}-\\d{4}-\\d{4}");
Matcher m = p.matcher(text);
// すべての電話番号をマスクする
String allReplaced = m.replaceAll("XXX-XXXX-XXXX");
System.out.println(allReplaced);
// 出力: 電話番号はXXX-XXXX-XXXX、携帯はXXX-XXXX-XXXXです。
// 最初の電話番号だけをマスクする
// Matcherは内部状態を持つため、新しいMatcherを生成するかreset()を呼び出す
m.reset();
String firstReplaced = m.replaceFirst("YYY-YYYY-YYYY");
System.out.println(firstReplaced);
// 出力: 電話番号はYYY-YYYY-YYYY、携帯は090-3333-4444です。
なお、よりシンプルな置換であれば、String
クラス自身のreplaceAll()
メソッドも利用できます。これは内部でPattern
とMatcher
を使用しています。
文字列の分割
Pattern.split()
メソッドやString.split()
メソッドを使うと、正規表現をデリミタ(区切り文字)として文字列を分割し、文字列の配列を得ることができます。
String text = "apple,orange;banana mango";
// カンマ、セミコロン、または1つ以上の空白で分割する
Pattern p = Pattern.compile("[,;\\s]+");
String[] fruits = p.split(text);
for (String fruit : fruits) { System.out.println(fruit);
}
// 実行結果:
// apple
// orange
// banana
// mango
String.split()
と Pattern.split()
の違いString.split()
は内部で毎回正規表現をコンパイルするため、ループ内で繰り返し呼び出すとパフォーマンスが低下する可能性があります。 一方、Pattern.split()
は事前にコンパイルしたPattern
オブジェクトを使用するため、繰り返し実行する場合に効率的です。 パフォーマンスとベストプラクティス
正規表現は非常に強力ですが、使い方を誤ると深刻なパフォーマンス問題を引き起こす可能性があります。特に、Webアプリケーションなど外部からの入力を扱う場合は注意が必要です。
ReDoS (Regular expression Denial of Service) 攻撃
正規表現の脆弱性の一つに、ReDoS(正規表現によるサービス拒否)攻撃があります。 これは、特定の「悪意のある」入力文字列を処理させると、正規表現エンジンの計算量が指数関数的に増大し、CPUリソースを食いつぶしてシステムを停止(DoS)させてしまう攻撃です。
ReDoSは、特に「入れ子になった量指定子」と「選択(OR)」が組み合わさったような、正規表現エンジンが多くのバックトラック(探索のやり直し)を強いられるパターンで発生しやすくなります。
// ReDoS脆弱性を持つ可能性のあるパターンの例
// (a+)+ や (a|aa)+ のようなパターンは危険
PatternredosPattern = Pattern.compile("^(([a-zA-Z0-9])+)+$");
対策:
- 複雑な入れ子構造を避ける: 可能な限りシンプルなパターンを心がけます。
- 入力長の制限: 正規表現で処理する前に入力文字列の長さを妥当な範囲に制限します。
- タイムアウトの設定: 万が一の処理暴走に備え、処理全体にタイムアウトを設けることを検討します。
- 既知の安全なパターンを利用する: メールアドレスやURLの検証などでは、OWASP (Open Web Application Security Project) などで推奨されている、検証済みの正規表現パターンを使用することが望ましいです。
Java 9以降の新機能
Java 9ではMatcher
クラスにresults()
メソッドが追加されました。これにより、find()
で見つかった全てのマッチ結果をStream<MatchResult>
として取得できるようになりました。Stream APIと組み合わせることで、より宣言的でモダンなコードを記述できます。
// Java 9以降のコード
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.List;
String text = "Product: A-101, B-202, C-303";
Pattern pattern = Pattern.compile("[A-Z]-\\d{3}");
List<String> productCodes = pattern.matcher(text) .results() // Stream<MatchResult> を取得 .map(mr -> mr.group()) // 各MatchResultからマッチした文字列を取得 .collect(Collectors.toList());
System.out.println(productCodes); // [A-101, B-202, C-303]
まとめ
本記事では、Javaのjava.util.regex
パッケージを用いた正規表現の扱い方について、基本から応用、そしてパフォーマンスに関する注意点までを網羅的に解説しました。
正規表現は、一見すると複雑で難解に見えるかもしれません。しかし、その構文とPattern
・Matcher
クラスの役割を正しく理解すれば、文字列処理の効率を劇的に向上させることができる、非常に強力なツールとなります。
ここで紹介した知識を土台として、ぜひ実際の開発で正規表現を活用してみてください。文字列操作の可能性が大きく広がるはずです。