正規表現の基本

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 正規表現 (regex) は文字列のパターンを記述するミニ言語
  • String.matches(regex) String.replaceAll(regex, repl) で使うのが基本
  • Pattern.compilePattern インスタンスにして使い回すと速い
  • メタ文字は . * + ? [] {} () 等、リテラルとして使うときは \\. のようにエスケープ

正規表現の基本 とは

Java の正規表現の基本パターンとメタ文字のエスケープを学び、String.matches を使って簡易なメール形式かを判定する関数を書いてみよう。

文字列の「形」をパターンで表現する 正規表現

業務でコードを書いていると、「この文字列はメールアドレスっぽいか?」「電話番号として妥当か?」「ファイル名が .jpg で終わっているか?」といった、文字列の 形 (パターン) を判定したい場面が次々に出てきます。これを ifString.charAtfor ループで真面目に書こうとすると、たった 1 行分のチェックに何十行も使うことになってしまいます。

そこで登場するのが 正規表現 (Regular Expression、略して regexregexp) です。正規表現は、文字列の「形」を 1 行の式で表現するための小さな言語で、Java だけでなく JavaScript Python Ruby Go などほぼ全ての言語に組み込まれている共通の道具です。一度覚えれば、他の言語でもほぼそのまま使えるので、コスパが非常に高い知識と言えます。

正規表現は最初こそ呪文に見えますが、実は「文字クラス」「繰り返し」「アンカー」のたった 3 つの考え方で大半をカバーできます。少しずつ実例で慣れていくと、ある日突然読めるようになります。

このレッスンでは、まずは String.matches と簡単な文字クラスだけに絞って学んでいきます。Pattern / Matcher を使った本格的な API や性能チューニングの話は次のレッスンで扱うので、ここでは「matches で完全一致を判定する」感覚をつかむことを目標にしましょう。

diagram (will load when visible)

基本パターンの一覧

まずは語彙を仕入れましょう。正規表現には「特別な意味を持つ文字」と「そのままの文字」があります。よく使うものを下の表にまとめました。

記号意味
.任意の 1 文字 (改行以外)a.cabc a1c にマッチ
*直前の要素を 0 回以上繰り返しab*a ab abbb にマッチ
+直前の要素を 1 回以上繰り返しab+ab abbb にマッチ (a は NG)
?直前の要素を 0 または 1 回colou?rcolorcolour にマッチ
[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\sba 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.50

String.matches で完全一致を判定する

JavaString クラスには、正規表現を直接受け取る matches メソッドが用意されています。PatternMatcher をわざわざ作らなくても、簡単な判定ならこれ 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 を使うか、自分で ^$ を付ける必要がありますが、Javamatches は最初から ^$ で囲まれているのと同じ振る舞いをします。

より高度な使い方 (splitreplaceAllPattern.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") を使うのが手軽です
  • greedylazy の混同* + ? はデフォルトで 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 文字以上 + . + 英数字 1 文字以上」の形ならば true
  2. それ以外 (空文字列、@ がない、ドットがない、英数字以外を含むなど) は false
  3. ヒント: String.matches を使うと、完全一致を 1 行で書けます

具体的にはこんな結果になります。

  • isEmail("test@example.com")true
  • isEmail("a@b.c")true
  • isEmail("invalid")false
  • isEmail("@example.com")false
  • isEmail("")false

正規表現は [A-Za-z0-9]+@[A-Za-z0-9]+\\.[A-Za-z0-9]+ を使うと、上記の仕様をぴったり満たせます。\\. がドットそのものを表していること、+ が「1 文字以上の繰り返し」であることを思い出しながら書いてみてください。

実務のメール検証はもっと複雑 (+- を含めるか、国際化ドメインに対応するかなど) ですが、まずはこの「簡易版」で正規表現の感覚をつかみましょう。次のレッスンでは PatternMatcher で本格的に扱う書き方や、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 つができることを確認しましょう。

  1. 正規表現 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 正規表現 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. String.matches を使って正規表現で完全一致を判定すること
  2. 正規表現は [A-Za-z0-9]+@[A-Za-z0-9]+\.[A-Za-z0-9]+ の形式で、@ の前後とドットの前後にそれぞれ 1 文字以上の英数字があることを保証すること
  3. ドット .\\. のように 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

ヒント

main.java
main.java
学習モード

メモ

正規表現の基本

⌘S で保存