概要
「テストでは通ったのに、本番で正しいはずの入力が弾かれる」――正規表現まわりの不具合は、パターンそのものの誤りより、matches と find の取り違えや、ループ内で毎回 Pattern.compile している性能劣化として現れることが多いものです。matches は文字列全体の一致、find は部分一致。この一行の違いを取り違えると、形式チェックのつもりが部分一致で素通りします。この記事では Pattern を static final で保持する基本設計から、名前付きキャプチャグループによる可読性向上、asMatchPredicate を使った Stream 連携まで、メール・電話番号・郵便番号の検証を題材に整理します。境界値(ハイフンなし・桁ずれ)が正しく弾かれることも実行結果で確認します。
使いどころ
会員登録フォームでメールアドレス・電話番号・郵便番号の形式を正規表現で即時バリデーションする
帳票データや自由入力テキストから日付文字列(yyyy/mm/dd)を抽出し、名前付きキャプチャグループで年月日に分解する
CSV 一括取込時に不正な形式のメールアドレスを Stream + asMatchPredicate でフィルタリングし、エラー行を特定する
コード例
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("user@example.com")); // true
System.out.println(isValidEmail("invalid@")); // false
System.out.println(isValidPhone("03-1234-5678")); // true
System.out.println(isValidPhone("0312345678")); // false(ハイフンなしは別書式として扱う)
System.out.println(isValidZip("123-4567")); // true
System.out.println(isValidZip("1234-567")); // false(桁の区切りが不正)
// matches と find の違い: 形式チェックに find を使うと部分一致で素通りする
var stray = "備考: 03-1234-5678 に連絡";
System.out.println(PHONE_PATTERN.matcher(stray).matches()); // false(全体は電話番号ではない)
System.out.println(PHONE_PATTERN.matcher(stray).find()); // false(^ と $ があるため。アンカーなしなら 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("a@b.com", "invalid", "c@d.org");
System.out.println(filterValidEmails(emails)); // [a@b.com, c@d.org]
}
}Version Coverage
Pattern.asMatchPredicate() で Stream との連携が簡潔になる。var による型推論で Matcher の宣言も短くなる。
// 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
注意点
Pattern.compile はスレッドセーフだが、Matcher はスレッドセーフではない。Pattern は static final で共有し、Matcher はメソッド内で毎回生成すること
matches() は文字列全体がパターンに一致するかを判定する。部分一致を調べたい場合は find() を使う。この違いを間違えると、検証が意図通りに動かない
メールアドレスの正規表現は完全な RFC 準拠にすると非常に複雑になる。業務用途では実用的な範囲に絞り、厳密な検証はサーバー側やメール送信での到達確認に委ねる
日本語の電話番号パターンは市外局番の桁数が地域によって異なる(03- は2桁、045- は3桁など)。単一の正規表現で全パターンをカバーしようとすると保守が難しくなる
実務では電話番号や郵便番号の仕様が変わっていてパターンが古くなっていたケースに遭遇することがある。正規表現にはコメントで意図を明記し、テストデータに桁数の境界値やハイフンあり・なしを含めておくこと。
FAQ
matches() は文字列全体がパターンに合致するかを調べます。find() は部分一致を探します。入力値の形式チェックには matches()、テキストからの抽出には find() を使います。
動作はしますが、compile はコストの高い処理です。同じパターンを繰り返し使うなら static final フィールドに保持するのが定石です。ループ内での compile は避けてください。
Java の文字列リテラルでは \\ が正規表現の \\ に対応します。\\d は数字、\\. はリテラルのドットです。テキストブロック(Java 15+)を使うとエスケープが若干読みやすくなります。