概要
メールアドレス、電話番号、郵便番号など、業務システムではフォーマットの検証が日常的に発生します。Java の正規表現は Pattern と Matcher のペアで扱いますが、Pattern.compile のコストを意識せずにメソッド内で毎回コンパイルしているコードや、matches と find の違いを把握しないまま意図しない判定をしているコードは実務でもよく見かけます。この記事では、Pattern を static final で保持する基本的な設計から、名前付きキャプチャグループによる可読性向上、asMatchPredicate を使った Stream 連携まで、業務で即使える正規表現パターンを整理します。Java 8 の基本的な書き方から Java 21 の sealed interface を使ったバリデーション結果の型安全な表現まで、段階的に扱います。
使いどころ
会員登録フォームでメールアドレス・電話番号・郵便番号の形式を正規表現で即時バリデーションする
帳票データや自由入力テキストから日付文字列(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("[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]]
}
}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+)を使うとエスケープが若干読みやすくなります。