概要

メールアドレス、電話番号、郵便番号など、業務システムではフォーマットの検証が日常的に発生します。Java の正規表現は Pattern と Matcher のペアで扱いますが、Pattern.compile のコストを意識せずにメソッド内で毎回コンパイルしているコードや、matches と find の違いを把握しないまま意図しない判定をしているコードは実務でもよく見かけます。この記事では、Pattern を static final で保持する基本的な設計から、名前付きキャプチャグループによる可読性向上、asMatchPredicate を使った Stream 連携まで、業務で即使える正規表現パターンを整理します。Java 8 の基本的な書き方から Java 21 の sealed interface を使ったバリデーション結果の型安全な表現まで、段階的に扱います。

使いどころ

会員登録フォームでメールアドレス・電話番号・郵便番号の形式を正規表現で即時バリデーションする

帳票データや自由入力テキストから日付文字列(yyyy/mm/dd)を抽出し、名前付きキャプチャグループで年月日に分解する

CSV 一括取込時に不正な形式のメールアドレスを Stream + asMatchPredicate でフィルタリングし、エラー行を特定する

コード例

RegexValidator.java
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexValidator {

    // Pattern は static final で保持し、使い回す
    private static final Pattern EMAIL_PATTERN =
            Pattern.compile("^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$");

    private static final Pattern PHONE_PATTERN =
            Pattern.compile("^0\\d{1,4}-\\d{1,4}-\\d{4}$");

    private static final Pattern ZIP_PATTERN =
            Pattern.compile("^\\d{3}-\\d{4}$");

    private static final Pattern DATE_PATTERN =
            Pattern.compile("(?<year>\\d{4})/(?<month>\\d{2})/(?<day>\\d{2})");

    /** メールアドレスの形式チェック */
    public static boolean isValidEmail(String email) {
        return email != null && EMAIL_PATTERN.matcher(email).matches();
    }

    /** 電話番号の形式チェック(ハイフン区切り) */
    public static boolean isValidPhone(String phone) {
        return phone != null && PHONE_PATTERN.matcher(phone).matches();
    }

    /** 郵便番号の形式チェック */
    public static boolean isValidZip(String zip) {
        return zip != null && ZIP_PATTERN.matcher(zip).matches();
    }

    /** テキストから最初の日付を抽出(名前付きキャプチャグループ) */
    public static String extractFirstDate(String text) {
        if (text == null) return null;
        Matcher m = DATE_PATTERN.matcher(text);
        if (m.find()) {
            return m.group("year") + "年"
                 + m.group("month") + "月"
                 + m.group("day") + "日";
        }
        return null;
    }

    /** テキストからすべての日付を抽出 */
    public static List<String> extractAllDates(String text) {
        List<String> results = new ArrayList<>();
        if (text == null) return results;
        Matcher m = DATE_PATTERN.matcher(text);
        while (m.find()) {
            results.add(m.group("year") + "/"
                      + m.group("month") + "/"
                      + m.group("day"));
        }
        return results;
    }

    /** Stream 連携: 有効なメールアドレスだけを抽出(Java 11+) */
    public static List<String> filterValidEmails(List<String> emails) {
        return emails.stream()
                .filter(EMAIL_PATTERN.asMatchPredicate())
                .toList();
    }

    public static void main(String[] args) {
        System.out.println(isValidEmail("[email protected]"));   // true
        System.out.println(isValidEmail("invalid@"));           // false
        System.out.println(isValidPhone("03-1234-5678"));       // true
        System.out.println(isValidZip("123-4567"));             // true

        String text = "納期は2024/04/01から2024/06/30までです。";
        System.out.println(extractFirstDate(text));             // 2024年04月01日
        System.out.println(extractAllDates(text));              // [2024/04/01, 2024/06/30]

        var emails = List.of("[email protected]", "invalid", "[email protected]");
        System.out.println(filterValidEmails(emails));          // [[email protected], [email protected]]
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

Pattern.asMatchPredicate() で Stream との連携が簡潔になる。var による型推論で Matcher の宣言も短くなる。

Java 17
// Java 17: 名前付きグループ + asMatchPredicate
Pattern p = Pattern.compile(
    "(?<year>\\d{4})/(?<month>\\d{2})/(?<day>\\d{2})");
var m = p.matcher(text);
if (m.find()) {
    String year = m.group("year");  // 名前でアクセス
}
// asMatchPredicate で Stream 連携
List<String> valid = emails.stream()
        .filter(EMAIL_PATTERN.asMatchPredicate())
        .toList();

Library Comparison

標準 API(Pattern / Matcher)基本的なフォーマット検証や文字列抽出。依存なしで対応したいとき。複雑なパターンを自前で管理する必要がある。テストケースを十分に書いて正規表現の正しさを担保する。
Apache Commons Validatorメールアドレスや URL の検証を既製のバリデータに任せたいとき。カスタマイズ性は低い。日本固有のフォーマット(電話番号・郵便番号)は結局自前の正規表現が必要。
Hibernate Validator(Bean Validation)@Email / @Pattern アノテーションで DTO のフィールドに宣言的にバリデーションをかけたいとき。フレームワーク依存が前提。単体の文字列検証だけなら導入コストが高い。

注意点

Pattern.compile はスレッドセーフだが、Matcher はスレッドセーフではない。Pattern は static final で共有し、Matcher はメソッド内で毎回生成すること

matches() は文字列全体がパターンに一致するかを判定する。部分一致を調べたい場合は find() を使う。この違いを間違えると、検証が意図通りに動かない

メールアドレスの正規表現は完全な RFC 準拠にすると非常に複雑になる。業務用途では実用的な範囲に絞り、厳密な検証はサーバー側やメール送信での到達確認に委ねる

日本語の電話番号パターンは市外局番の桁数が地域によって異なる(03- は2桁、045- は3桁など)。単一の正規表現で全パターンをカバーしようとすると保守が難しくなる

FAQ

matches() と find() はどう使い分けますか。

matches() は文字列全体がパターンに合致するかを調べます。find() は部分一致を探します。入力値の形式チェックには matches()、テキストからの抽出には find() を使います。

Pattern.compile を毎回呼んでも問題ないですか。

動作はしますが、compile はコストの高い処理です。同じパターンを繰り返し使うなら static final フィールドに保持するのが定石です。ループ内での compile は避けてください。

正規表現のエスケープはどう書きますか。

Java の文字列リテラルでは \\ が正規表現の \\ に対応します。\\d は数字、\\. はリテラルのドットです。テキストブロック(Java 15+)を使うとエスケープが若干読みやすくなります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます