正規表現の基本
このレッスンで分かること
- 正規表現 (
regex) は文字列のパターンを記述するミニ言語String.matches(regex)String.replaceAll(regex, repl)で使うのが基本Pattern.compileでPatternインスタンスにして使い回すと速い- メタ文字は
. * + ? [] {} ()等、リテラルとして使うときは\\.のようにエスケープ
正規表現の基本 とは
Java の正規表現の基本パターンとメタ文字のエスケープを学び、String.matches を使って簡易なメール形式かを判定する関数を書いてみよう。
文字列の「形」をパターンで表現する 正規表現
業務でコードを書いていると、「この文字列はメールアドレスっぽいか?」「電話番号として妥当か?」「ファイル名が .jpg で終わっているか?」といった、文字列の 形 (パターン) を判定したい場面が次々に出てきます。これを if と String.charAt と for ループで真面目に書こうとすると、たった 1 行分のチェックに何十行も使うことになってしまいます。
そこで登場するのが 正規表現 (Regular Expression、略して regex や regexp) です。正規表現は、文字列の「形」を 1 行の式で表現するための小さな言語で、Java だけでなく JavaScript Python Ruby Go などほぼ全ての言語に組み込まれている共通の道具です。一度覚えれば、他の言語でもほぼそのまま使えるので、コスパが非常に高い知識と言えます。
正規表現は最初こそ呪文に見えますが、実は「文字クラス」「繰り返し」「アンカー」のたった 3 つの考え方で大半をカバーできます。少しずつ実例で慣れていくと、ある日突然読めるようになります。
このレッスンでは、まずは String.matches と簡単な文字クラスだけに絞って学んでいきます。Pattern / Matcher を使った本格的な API や性能チューニングの話は次のレッスンで扱うので、ここでは「matches で完全一致を判定する」感覚をつかむことを目標にしましょう。
基本パターンの一覧
まずは語彙を仕入れましょう。正規表現には「特別な意味を持つ文字」と「そのままの文字」があります。よく使うものを下の表にまとめました。
| 記号 | 意味 | 例 |
|---|---|---|
. | 任意の 1 文字 (改行以外) | a.c は abc a1c にマッチ |
* | 直前の要素を 0 回以上繰り返し | ab* は a ab abbb にマッチ |
+ | 直前の要素を 1 回以上繰り返し | ab+ は ab abbb にマッチ (a は NG) |
? | 直前の要素を 0 または 1 回 | colou?r は color と colour にマッチ |
[abc] | a または b または c のいずれか 1 文字 | [aeiou] で母音 1 文字 |
[^abc] | a / b / c 以外 の 1 文字 | [^0-9] で数字以外 |
\d | 数字 1 文字 ([0-9] と同じ) | \d\d は 2 桁の数字 |
\w | 英数字 + _ 1 文字 | \w+ は単語っぽい塊 |
\s | 空白文字 (半角スペース、\t、改行など) | a\sb は a b にマッチ |
^ $ | 行頭 / 行末 (アンカー) | ^abc$ は文字列全体が abc |
たとえば [A-Za-z0-9]+ と書けば「英大文字、英小文字、数字のどれかが 1 文字以上連続」という意味になります。A-Z のように - を使うと範囲指定ができ、[A-Za-z0-9] で英数字全体を表せる、と覚えてください。
\d\w\sの大文字版\D\W\Sはそれぞれ「否定」を意味します。たとえば\Dは「数字 以外 の 1 文字」です。否定が必要な場面では[^0-9]よりこちらの方が短く書けて便利です。
Java 特有の罠 \\ で書くエスケープ
ここで Java 特有の最大の罠を紹介します。正規表現で \d を書きたいとき、Java のソースコードでは "\\d" のように バックスラッシュを 2 つ 並べる必要があります。
Java
// 正しい (正規表現としては \d)
String digit = "\\d";
// 間違い: コンパイルエラーになる (\d は Java 文字列の正しいエスケープではない)
String wrong = "\d";これは、Java の文字列リテラル自体が \ をエスケープ文字として使うためです。"\\d" と書くと「文字列としては \d という 2 文字」になり、それを正規表現エンジンが「数字 1 文字」と解釈する、という二段構えになっています。同じ理由で、正規表現で . (任意の 1 文字ではなくドットそのもの) にマッチさせたい場合は "\\." と書きます。
Java
// ドットそのものにマッチさせたい
String dot = "\\.";
// 数字 3 桁 + ドット + 数字 2 桁
String price = "\\d{3}\\.\\d{2}"; // 例: 100.50String.matches で完全一致を判定する
Java の String クラスには、正規表現を直接受け取る matches メソッドが用意されています。Pattern や Matcher をわざわざ作らなくても、簡単な判定ならこれ 1 つで足ります。
s.matches(regex)—s全体が正規表現とマッチするかをbooleanで返す。部分一致ではなく完全一致 な点に注意
実例で動きを見てみます。
Java
public class RegexDemo {
public static void main(String[] args) {
// matches: 完全一致を boolean で返す
System.out.println("abc123".matches("[a-z]+\\d+")); // true
System.out.println("abc123".matches("[a-z]+")); // false (全体が一致しない)
System.out.println("hello".matches("[a-z]+")); // true
}
}matches が「完全一致」になっているのは Java 特有の親切設計です。同じことを Python でやろうとすると re.fullmatch を使うか、自分で ^ と $ を付ける必要がありますが、Java の matches は最初から ^ と $ で囲まれているのと同じ振る舞いをします。
より高度な使い方 (
splitやreplaceAll、Pattern.compileでのコンパイル再利用、性能の話、グループ抽出) は次のレッスンでまとめて扱います。本レッスンではmatchesだけに集中しましょう。
よくある間違い
初学者がつまずきやすいポイントを 3 つ整理しておきます。
\\のエスケープ忘れ —Javaの文字列で\d\w\sを書きたいときは必ず\\d\\w\\sのようにバックスラッシュ 2 つです。1 つしか書かないとコンパイルエラー (illegal escape character) になりますmatchesを部分一致だと思い込む —String.matchesは 完全一致 です。"foo bar".matches("foo")はfalseを返します。部分的に含まれているかを調べたいならs.contains("foo")を使うのが手軽ですgreedyとlazyの混同 —*+?はデフォルトでgreedy(できるだけ長くマッチ) です。最小マッチにしたいときは*?+?のように?を後ろに付けます。今回の課題のような単純な完全一致では気にしなくて構いません
String.matchesは内部で毎回正規表現をコンパイルするので、同じパターンを大量の文字列に対して使う場面では遅くなります。性能対策が必要な場面では、次のレッスンで扱うPattern.compileを使い回すのが鉄則です。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- バリデーション ─ メアド
^[\w.-]+@[\w.-]+\.\w+$、電話番号^\d{2,4}-\d{2,4}-\d{4}$ - ログ解析 ─
\[ERROR\].*で ERROR 行を抽出 - HTML / Markdown のラフな置換 ─ ただし複雑な構造は専用パーサーが本筋
- 設定値からの抽出 ─
host:portを^(.+?):(\d+)$でキャプチャ
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
- 毎回
Pattern.compile─ ループ内で同じ regex を compile し直すと遅い。static finalに - 正規表現でメアドを完全に検証しようとしている ─ RFC 準拠の regex は超複雑、現実的にはざっくり + 認証メール送信が定番
String.split(".")で空配列 ─.は「任意の 1 文字」、\\.でエスケープが必要- バックトラック爆発 (ReDoS) ─
(a+)+のような regex は攻撃で CPU を食いつぶす。タイムアウト付き Matcher を検討
パフォーマンス考慮事項
Pattern.compileは重い ─ 同じ regex は使い回す、static final Pattern P = ...String.matchesは毎回 compile ─ ホットパスではPattern.matcherを使い回すreplaceAllも内部で compile ─ 大量置換はMatcher.replaceAllでPattern.compile(regex, Pattern.MULTILINE)─ 改行を意識した正規表現はフラグ指定が必要
ここまでの要点
regex は String.matches / Pattern.compile で使う。メタ文字はエスケープ、Pattern は static で使い回す、ReDoS に注意。
やってみよう
それでは課題に挑戦しましょう。Solution.isEmail(String s) を実装して、s が 簡易なメール形式 (英数 + @ + 英数 + . + 英数) かどうかを boolean で返します。仕様は次のとおりです。
- 文字列全体が「英数字 1 文字以上 +
@+ 英数字 1 文字以上 +.+ 英数字 1 文字以上」の形ならばtrue - それ以外 (空文字列、
@がない、ドットがない、英数字以外を含むなど) はfalse - ヒント:
String.matchesを使うと、完全一致を 1 行で書けます
具体的にはこんな結果になります。
isEmail("test@example.com")→trueisEmail("a@b.c")→trueisEmail("invalid")→falseisEmail("@example.com")→falseisEmail("")→false
正規表現は [A-Za-z0-9]+@[A-Za-z0-9]+\\.[A-Za-z0-9]+ を使うと、上記の仕様をぴったり満たせます。\\. がドットそのものを表していること、+ が「1 文字以上の繰り返し」であることを思い出しながら書いてみてください。
実務のメール検証はもっと複雑 (+ や - を含めるか、国際化ドメインに対応するかなど) ですが、まずはこの「簡易版」で正規表現の感覚をつかみましょう。次のレッスンでは Pattern と Matcher で本格的に扱う書き方や、split / replaceAll の活用例に進みます。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は split と join で、Java の正規表現の基本パターンとメタ文字のエスケープを学び、String を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- 正規表現 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. 正規表現 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
String.matchesを使って正規表現で完全一致を判定すること- 正規表現は
[A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z0-9]+の形式で、@の前後とドットの前後にそれぞれ 1 文字以上の英数字があることを保証すること - ドット
.は\\.のように Java 文字列内ではバックスラッシュ 2 つでエスケープすること
入出力例
test-cases.txt
isEmail("test@example.com") → true
isEmail("a@b.c") → true
isEmail("invalid") → false
isEmail("@example.com") → false
isEmail("") → false
isEmail("test@examplecom") → false
isEmail("test@.com") → false