[Kotlin] Regex の使い方完全ガイド — containsMatchIn / find / findAll / replace の違い

Kotlinで正規表現を使うとき、「あれ、findfindAllって何が違うんだっけ?」「マッチしたかどうか調べたいだけなのに、どのメソッドを呼べばいいの?」と迷ったことはありませんか?

KotlinのRegexクラスには似たようなメソッドがいくつかあって、名前が似ているぶん最初は使い分けに戸惑うんですよね。

この記事では、Regexの主要メソッドであるcontainsMatchIn / matches / find / findAll / replaceの違いを、実際のコードを交えながら整理していきます。読み終わる頃には「この場面ではこのメソッド」とすぐに判断できるようになるはずです。

ちなみに、パターンを試しながら書きたい方は、ブラウザ上で正規表現をテストできる正規表現テスター(Kotlin Regex対応)も用意しているので、合わせて使ってみてください。

スポンサーリンク

前提条件

  • Kotlin(新しい目のバージョンであれば特に気にしなくてOK)
  • Android Studio または任意のKotlin実行環境

まずは Regex インスタンスの作り方

メソッドを見ていく前に、Regexそのものの作り方を軽く押さえておきます。

// 基本的な作り方
val regex = Regex("\\d{3}-\\d{4}")
 
// オプション付き(大文字小文字を区別しない)
val regex2 = Regex("hello", RegexOption.IGNORE_CASE)
 
// 複数オプションを指定
val regex3 = Regex(
    "^start.*end$",
    setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)
)
 
// Stringから拡張関数で変換することも可能
val regex4 = "\\d+".toRegex()

パターン文字列の中で\d\sを書くとき、バックスラッシュを2つ\\d\\s)にしないといけない点は要注意です。これはKotlinの通常の文字列リテラルでは\自体がエスケープ文字だからですね。

エスケープが面倒なときは、生文字列リテラル("""...""")を使うとそのまま書けます。

// 生文字列リテラルならエスケープ不要
val regex = Regex("""\d{3}-\d{4}""")

長いパターンを書くときは生文字列リテラルのほうが読みやすいことが多いので、覚えておくと便利です。

メソッドの全体像

先に全体像をざっくり把握しておきましょう。用途別に見るとこんな感じです。

  • マッチするかどうかだけ知りたいcontainsMatchIn / matches
  • マッチした箇所の中身を取り出したいfind / findAll
  • マッチした部分を別の文字列に置き換えたいreplace / replaceFirst

ここから1つずつ見ていきます。

1. containsMatchIn — マッチがあるかどうかだけ知りたい

文字列の中にパターンにマッチする部分が1つでもあるかBooleanで返してくれるメソッドです。「含まれているか?」をサクッと判定したいときに使います。

val text = "ユーザーID: user_123 でログインしました"
val regex = Regex("""\d+""")
 
val found = regex.containsMatchIn(text)
println(found) // true

ログの中に数字があるかチェックしたい、エラーコードが含まれているか調べたい、といったユースケースにぴったりです。

ちなみに、こちらはStringcontainsの拡張関数もあるので、そちらのほうが書きやすい場合も多いです。

val text = "ユーザーID: user_123"
val found = text.contains(Regex("""\d+""")) // 同じ結果

2. matches — 文字列全体がパターンに一致するか

containsMatchInと紛らわしいのがこのmatchesです。違いは「部分一致」か「完全一致」か

  • containsMatchIn:文字列のどこかにマッチがあればtrue
  • matches:文字列全体がパターンと一致する必要がある
val regex = Regex("""\d+""")
 
regex.containsMatchIn("abc123xyz") // true(一部に数字がある)
regex.matches("abc123xyz")          // false(全体が数字じゃない)
regex.matches("123")                // true(全体が数字)

入力バリデーションで「この文字列は完全に電話番号の形式か?」のようにチェックしたいときはmatches、「文字列内に数字が含まれているか?」のような場面ではcontainsMatchIn、と使い分けましょう。

なお、matchesの挙動は、パターンを^...$で囲んだときのcontainsMatchInと同じです。実質的にはどちらで書いても動きますが、「完全一致がしたい」という意図はメソッド名で伝わるmatchesのほうが読みやすいですね。

3. find — 最初のマッチを取り出す

「マッチした部分の文字列そのものが欲しい」という場面で使うのがfindです。最初に見つかったマッチだけをMatchResult?として返します。

val text = "注文番号: 20260420-AB123"
val regex = Regex("""\d{8}""")
 
val match = regex.find(text)
println(match?.value) // "20260420"
println(match?.range) // 6..13(マッチした位置)

戻り値がMatchResult?(nullable)なのがポイントです。マッチしなかった場合はnullが返るので、?.?:で扱ってあげる必要があります。

val result = regex.find(text)?.value ?: "マッチなし"

キャプチャグループを取り出す

MatchResultからはキャプチャグループ(()で囲んだ部分)も取り出せます。

val text = "価格: 1500円"
val regex = Regex("""(\d+)円""")
 
val match = regex.find(text)
if (match != null) {
    println(match.value)          // "1500円"(マッチ全体)
    println(match.groupValues[1]) // "1500"(1番目のグループ)
}

groupValues[0]はマッチ全体、[1]以降が順番にキャプチャグループの値になります。0番目から始まるのはちょっと紛らわしいですが、慣れれば自然です。

名前付きキャプチャを使う

グループが増えてくると「1番目のグループって何だっけ?」と迷いがちです。そんなときは名前付きキャプチャが便利です。

val text = "日付: 2026-04-20"
val regex = Regex("""(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})""")
 
val match = regex.find(text)
if (match != null) {
    val year = match.groups["year"]?.value   // "2026"
    val month = match.groups["month"]?.value // "04"
    val day = match.groups["day"]?.value     // "20"
    println("$year/$month/$day")
}

名前で取り出せると、コードを後から読み返したときに意図が伝わりやすくなります。

分割代入で複数のグループを一気に受け取る

ちょっとした小技ですが、destructuredプロパティを使うと分割代入で一度に変数へ展開できます。

val regex = Regex("""(\d{4})-(\d{2})-(\d{2})""")
val match = regex.find("日付: 2026-04-20")
 
if (match != null) {
    val (year, month, day) = match.destructured
    println("$year年$month月$day日") // "2026年04月20日"
}

すっきり書けていい感じですね。

4. findAll — すべてのマッチを取り出す

findは最初の1つだけでしたが、findAllすべてのマッチSequence<MatchResult>として返します。

val text = "連絡先: 090-1234-5678, 080-9876-5432"
val regex = Regex("""\d{3}-\d{4}-\d{4}""")
 
regex.findAll(text).forEach { match ->
    println("${match.value} (${match.range})")
}
// 出力:
// 090-1234-5678 (5..17)
// 080-9876-5432 (19..31)

戻り値がSequenceになっている点がポイントです。Listに変換したい場合はtoList()すればOK。

val matches = regex.findAll(text).toList()
val values = regex.findAll(text).map { it.value }.toList()

マッチした文字列だけのList<String>が欲しいという場面は結構多いので、map { it.value }.toList()のパターンは覚えておくと便利です。

5. replace — マッチした部分を置き換える

replaceはマッチした箇所を別の文字列に置き換えるメソッドです。すべてのマッチが対象になります(最初の1つだけ置き換えるreplaceFirstもあります)。

val text = "電話番号: 090-1234-5678"
val regex = Regex("""\d""")
 
val masked = regex.replace(text, "*")
println(masked) // "電話番号: ***-****-****"

ラムダで動的に置換する

置換後の文字列をマッチ内容に応じて変えたいときは、第2引数にラムダを渡します。

val text = "価格: 1500円, 送料: 800円"
val regex = Regex("""(\d+)円""")
 
val taxed = regex.replace(text) { match ->
    val price = match.groupValues[1].toInt()
    "${(price * 1.1).toInt()}円"
}
println(taxed) // "価格: 1650円, 送料: 880円"

マッチごとに計算結果を使いたい、キャプチャグループの値を加工したい、といった場面で活躍します。

キャプチャグループを置換文字列から参照する

シンプルな置換なら、$1, $2といった後方参照記法も使えます。

val text = "山田 太郎"
val regex = Regex("""(\S+)\s+(\S+)""")
 
// 姓と名を入れ替える
val swapped = regex.replace(text, "$2 $1")
println(swapped) // "太郎 山田"

迷ったときの判断フロー

ここまで5つのメソッドを見てきました。最後に「どれを使えばいい?」と迷ったときの判断フローをまとめます。

  1. マッチするかどうかだけ知りたい?
    • 部分一致でOK → containsMatchIn
    • 文字列全体がパターンと一致してほしい → matches
  2. マッチした内容を取り出したい?
    • 最初の1つだけでよい → find
    • すべての出現を処理したい → findAll
  3. マッチした部分を書き換えたい?
    • すべて置換する → replace
    • 最初だけ置換する → replaceFirst

このフローを頭に入れておくだけで、ほとんどの場面で迷わなくなります。

よくあるハマりポイント

ハマり1:バックスラッシュのエスケープ忘れ

冒頭でも触れましたが、これは本当によくやります。

// NG: \d が改行文字のようにKotlinに解釈されるわけではないが、
//     コンパイラが警告を出すか、パターンとしてうまく動かないことがある
val regex = Regex("\d+")
 
// OK: バックスラッシュを2つ
val regex = Regex("\\d+")
 
// OK: 生文字列リテラルならそのまま書ける
val regex = Regex("""\d+""")

迷ったら生文字列リテラルを使うのが安全です。

ハマり2:findAll の結果が空になる?

findAllSequenceを返すので、1回イテレートすると次回は空になることがあります(実装によりますが)。念のため、使い回したいならtoList()しておくのが安全です。

// 使い回すなら toList() しておく
val matches = regex.findAll(text).toList()
println(matches.size)
matches.forEach { /* ... */ }

ハマり3:matches と containsMatchIn の取り違え

バリデーションを書くときにcontainsMatchInを使ってしまうと、「部分的にでもマッチすればOK」になってしまって意図しないデータが通ってしまうことがあります。完全一致で確認したい場面では必ずmatchesを使いましょう。

val emailRegex = Regex("""[\w.]+@[\w.]+""")
 
// NG: 不正なメールでも部分マッチすれば通ってしまう
emailRegex.containsMatchIn("abc abc@test.com xyz") // true
 
// OK: 完全一致でチェック
emailRegex.matches("abc@test.com") // true
emailRegex.matches("abc abc@test.com xyz") // false

実践:ブラウザで試しながら書くのがおすすめ

正規表現は、頭の中だけで組み立てようとすると意外とハマりがちです。書いたパターンが本当に意図通り動くか、ブラウザ上でサッと試してから実装に反映するほうが、結果的にバグも減って早いです。

当ブログの正規表現テスター(Kotlin Regex対応)では、パターンを入力するとリアルタイムでマッチ結果とKotlinコードが生成されます。そのままコピペしてプロジェクトに貼れるので、試行錯誤しながら書きたいときに使ってみてください。すべての処理はブラウザ内で完結するので、機密データを含む文字列のテストでも安心です。

まとめ

KotlinのRegexメソッドの使い分けを整理しました。ポイントをおさらいします。

  • containsMatchIn:部分一致で真偽値を返す
  • matches:完全一致で真偽値を返す
  • find:最初のマッチをMatchResult?で返す
  • findAll:すべてのマッチをSequenceで返す
  • replace / replaceFirst:マッチ部分を置換する

名前が似ていて最初は混乱しがちですが、「真偽値か、マッチ結果か、置換か」のどれをしたいかで考えると選びやすくなります。手元に置いておくリファレンスとして使ってもらえたら嬉しいです。

コメント

タイトルとURLをコピーしました