Javaで「文字列が含まれるか」だけを知りたいときは contains、見つかった位置まで知りたいときは indexOf を使います。 contains は true / false を返し、indexOf は見つかった位置(0始まり)を、見つからなければ -1 を返します。先頭だけ・末尾だけを調べるなら startsWith / endsWith、正規表現で文字列全体の形を確かめるなら matches、完全に同じかどうかは equals と、目的ごとにメソッドが分かれています。

この記事では、7つのメソッドを「戻り値・一致の種類・正規表現の可否」で比べる早見表を最初に置き、そのあとで matches が部分一致ではなく全体一致になる点、null を渡したときの NullPointerException と安全な書き方、大文字小文字を無視する判定、複数キーワードの AND / OR 判定を順に説明します。掲載したコードはすべて OpenJDK 21.0.10 で実行し、出力をそのまま載せています。コードは1ファイルで完結しているので、貼り付けて java ファイル名.java で動かせます。

文字列検索メソッドの早見表

対象の文字列を s、探す語を key として整理します。

メソッド 戻り値 一致の種類 正規表現 見つからないとき 主な用途
s.contains(key) boolean 部分一致 使えない false 含むかどうかだけ知りたい
s.indexOf(key) int 部分一致(先頭から) 使えない -1 最初に出てくる位置を知りたい
s.lastIndexOf(key) int 部分一致(末尾から) 使えない -1 最後に出てくる位置を知りたい
s.startsWith(key) boolean 前方一致 使えない false 接頭辞の判定
s.endsWith(key) boolean 後方一致 使えない false 拡張子・接尾辞の判定
s.matches(regex) boolean 全体一致 使える false 形式(桁数・文字種)の確認
s.equals(key) boolean 完全一致 使えない false 同じ文字列かどうか

やりたいことから選ぶ場合は、次の逆引き表が便利です。

やりたいこと 書き方
含むかどうか s.contains("abc")
含まないかどうか !s.contains("abc")
何文字目にあるか s.indexOf("abc")
2つ目以降を探す s.indexOf("abc", 前回の位置 + 1)
範囲を区切って探す(Java 21以降) s.indexOf("abc", begin, end)
1文字を探す s.indexOf('a') >= 0
先頭が一致するか s.startsWith("abc")
末尾が一致するか s.endsWith(".csv")
正規表現で部分一致 Pattern.compile("\\d+").matcher(s).find()
正規表現で全体一致 s.matches("\\d{4}")
大文字小文字を無視して含むか s.toLowerCase(Locale.ROOT).contains(key.toLowerCase(Locale.ROOT))
大文字小文字を無視して同じか s.equalsIgnoreCase(key)
どれか1つでも含むか(OR) keys.stream().anyMatch(s::contains)
すべて含むか(AND) keys.stream().allMatch(s::contains)
どれも含まないか keys.stream().noneMatch(s::contains)
null かもしれない文字列を調べる s != null && s.contains("abc")

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java文字列検索 containsとindexOfの違い 含むか判定の早見表」

7つのメソッドを同じ文字列で動かして比べる

まずは同じ文字列に対して7つのメソッドを呼び、戻り値の違いを確かめます。

public class Basic {
    public static void main(String[] args) {
        String s = "report_2026_final.csv";

        System.out.println("contains(\"2026\")      : " + s.contains("2026"));
        System.out.println("indexOf(\"_\")          : " + s.indexOf("_"));
        System.out.println("lastIndexOf(\"_\")      : " + s.lastIndexOf("_"));
        System.out.println("indexOf(\"xyz\")        : " + s.indexOf("xyz"));
        System.out.println("startsWith(\"report\")  : " + s.startsWith("report"));
        System.out.println("endsWith(\".csv\")      : " + s.endsWith(".csv"));
        System.out.println("matches(\".*\\\\d{4}.*\")  : " + s.matches(".*\\d{4}.*"));
        System.out.println("equals(\"report\")      : " + s.equals("report"));
    }
}

実行結果(java Basic.java、OpenJDK 21.0.10):

contains("2026")      : true
indexOf("_")          : 6
lastIndexOf("_")      : 11
indexOf("xyz")        : -1
startsWith("report")  : true
endsWith(".csv")      : true
matches(".*\\d{4}.*")  : true
equals("report")      : false

読み取れることは3つです。

  • contains・startsWith・endsWith・matches・equals は boolean、indexOf・lastIndexOf は int(位置) を返します。
  • 位置は 0始まり です。report_ の _ は7文字目ですが、戻り値は 6 です。
  • 見つからないときの indexOf は -1 です。0 は「先頭で見つかった」という意味なので、if (s.indexOf(key) > 0) と書くと先頭一致を取りこぼします。含むかどうかの判定は >= 0 か != -1 で書きます。

containsとindexOfのどちらを選ぶか

位置を substring に渡したい、2回目以降の出現も調べたい、1文字(char)を探したい、出現回数を数えたい、といった場合は indexOf を選びます。contains は内部で indexOf を呼び、結果が 0 以上かどうかを返しているだけなので、判定結果は常に同じになります。「含むか」だけなら contains、位置を使うなら indexOf と覚えておけば迷いません。

matchesは部分一致ではなく全体一致

検索メソッドのなかで、もっとも取り違えやすいのが matches です。名前からは「パターンに合う部分があれば true」と読めますが、matches は文字列全体が正規表現に一致したときだけ true を返します。

import java.util.regex.Pattern;

public class MatchesTrap {
    public static void main(String[] args) {
        String s = "order-123-tokyo";

        // 1. matches は「全体一致」。部分的に合うだけでは false
        System.out.println("matches(\"\\\\d+\")       : " + s.matches("\\d+"));
        System.out.println("matches(\".*\\\\d+.*\")   : " + s.matches(".*\\d+.*"));

        // 2. 部分一致で正規表現を使うなら Matcher.find()
        Pattern p = Pattern.compile("\\d+");
        System.out.println("find()               : " + p.matcher(s).find());

        // 3. contains は正規表現を解釈しない(記号もただの文字)
        System.out.println("contains(\"\\\\d+\")      : " + s.contains("\\d+"));
        System.out.println("\"a.c\".contains(\".\")   : " + "a.c".contains("."));
        System.out.println("\"abc\".matches(\"a.c\")  : " + "abc".matches("a.c"));

        // 4. 記号を文字として正規表現に渡すなら Pattern.quote
        String key = "1+1";
        System.out.println("matches(\".*1+1.*\")    : " + "1+1=2".matches(".*" + key + ".*"));
        System.out.println("matches(quote)       : " + "1+1=2".matches(".*" + Pattern.quote(key) + ".*"));
    }
}

実行結果:

matches("\\d+")       : false
matches(".*\\d+.*")   : true
find()               : true
contains("\\d+")      : false
"a.c".contains(".")   : true
"abc".matches("a.c")  : true
matches(".*1+1.*")    : false
matches(quote)       : true

order-123-tokyo には数字が含まれていますが、matches("\\d+") は false です。正規表現 \d+ が文字列の最初から最後までを覆えないためです。

直す前と後

目的 直す前(意図と違う結果) 直した後
数字を含むか s.matches("\\d+") Pattern.compile("\\d+").matcher(s).find()
数字を含むか(1行で書く) s.matches("\\d+") s.matches(".*\\d+.*")
特定の語を含むか s.matches("tokyo") s.contains("tokyo")
記号を含む語を正規表現で探す s.matches(".*" + key + ".*") s.matches(".*" + Pattern.quote(key) + ".*")

ポイントを整理します。

  • 決まった語を探すだけなら contains で十分です。正規表現を使う必要はありません。
  • 正規表現で「含むか」を調べるなら Matcher の find() を使います。find は部分一致、matches は全体一致という関係です。
  • . + * ( [ \ などは正規表現で特別な意味を持ちます。利用者が入力した語をそのまま正規表現に混ぜると、上の 1+1 のように意図と違う判定になります。Pattern.quote で包むと、記号もただの文字として扱われます。
  • 逆に contains は正規表現を解釈しません。contains("\\d+") は「バックスラッシュ・d・プラス」という3文字を探すだけです。

nullで起きるNullPointerExceptionを再現する

文字列検索でいちばん多く出会う例外が NullPointerException(以下 NPE)です。検索される側が null の場合と、検索する語が null の場合の両方で発生します。メソッドごとに実際のメッセージを出してみます。

import java.util.Objects;

public class NullDemo {
    public static void main(String[] args) {
        String s = null;      // 検索される側が null
        String key = null;    // 検索する語が null

        try {
            System.out.println(s.contains("abc"));
        } catch (NullPointerException e) {
            System.out.println("[1] " + e.getMessage());
        }
        try {
            System.out.println("abcdef".contains(key));
        } catch (NullPointerException e) {
            System.out.println("[2] " + e.getMessage());
        }
        try {
            System.out.println("abcdef".indexOf(key));
        } catch (NullPointerException e) {
            System.out.println("[3] " + e.getMessage());
        }
        try {
            System.out.println("abcdef".startsWith(key));
        } catch (NullPointerException e) {
            System.out.println("[4] " + e.getMessage());
        }
        try {
            System.out.println("abcdef".matches(key));
        } catch (NullPointerException e) {
            System.out.println("[5] " + e.getMessage());
        }
        // equals だけは引数が null でも例外にならず false
        System.out.println("[6] \"abcdef\".equals(null) = " + "abcdef".equals(key));
        System.out.println("[7] Objects.equals(null, null) = " + Objects.equals(s, key));
    }
}

実行結果(java NullDemo.java):

[1] Cannot invoke "String.contains(java.lang.CharSequence)" because "<local1>" is null
[2] Cannot invoke "java.lang.CharSequence.toString()" because "s" is null
[3] Cannot invoke "String.coder()" because "str" is null
[4] Cannot invoke "String.length()" because "prefix" is null
[5] Cannot invoke "String.isEmpty()" because "this.pattern" is null
[6] "abcdef".equals(null) = false
[7] Objects.equals(null, null) = true

[1] は呼び出し元の変数が null、[2]〜[5] は引数に渡した語が null の場合です。equals だけは引数が null でも例外にならず false を返します([6])。

[1] の <local1> は「1番目のローカル変数」という意味です。java NullDemo.java のようにソースを直接実行すると変数名の情報が残らないため、この表示になります。javac -g でコンパイルしてから実行すると、同じ箇所が because "s" is null と変数名つきで表示されることを確認しました。

[2]〜[5] は、自分のコードではなく String クラスの内部で止まっている点に注目してください。メッセージに出てくる s や prefix は自分の変数名ではなく、呼び出したメソッドの引数名です。「引数に渡した値が null だった」と読み替えると原因にたどり着けます。

nullに強い書き方6つの型

null が来る可能性があるときの書き方を、コピペできる形でまとめます。

import java.util.Objects;
import java.util.Optional;

public class NullSafe {
    // 型1: null なら false を返す小さなメソッドにまとめる
    static boolean containsSafe(String text, String keyword) {
        return text != null && keyword != null && text.contains(keyword);
    }

    public static void main(String[] args) {
        String s = null;

        // 型1: 自作メソッド
        System.out.println("containsSafe(null, \"abc\")  : " + containsSafe(s, "abc"));
        System.out.println("containsSafe(\"xabcx\", \"abc\"): " + containsSafe("xabcx", "abc"));

        // 型2: 定数を左に置く(equals / equalsIgnoreCase)
        System.out.println("\"admin\".equals(s)         : " + "admin".equals(s));
        System.out.println("\"admin\".equalsIgnoreCase(s): " + "admin".equalsIgnoreCase(s));

        // 型3: Objects.equals(両方 null かもしれないとき)
        System.out.println("Objects.equals(s, \"admin\"): " + Objects.equals(s, "admin"));

        // 型4: null を空文字に寄せてから調べる
        String t = Objects.requireNonNullElse(s, "");
        System.out.println("requireNonNullElse        : " + t.contains("abc"));

        // 型5: Optional でつなぐ
        boolean hit = Optional.ofNullable(s).map(v -> v.contains("abc")).orElse(false);
        System.out.println("Optional                  : " + hit);

        // 型6: 入口で検査して、原因のわかるメッセージで止める
        try {
            Objects.requireNonNull(s, "text は必須です");
        } catch (NullPointerException e) {
            System.out.println("requireNonNull            : " + e.getMessage());
        }
    }
}

実行結果(java -Dstdout.encoding=UTF-8 NullSafe.java):

containsSafe(null, "abc")  : false
containsSafe("xabcx", "abc"): true
"admin".equals(s)         : false
"admin".equalsIgnoreCase(s): false
Objects.equals(s, "admin"): false
requireNonNullElse        : false
Optional                  : false
requireNonNull            : text は必須です
型 書き方 向いている場面
1 text != null && text.contains(key) もっとも基本。その場で1回だけ調べる
2 "定数".equals(s) 決まった値と同じかどうかを調べる
3 Objects.equals(a, b) 両方が null になりうる比較
4 Objects.requireNonNullElse(s, "") null と空文字を同じ扱いにしてよいとき
5 Optional.ofNullable(s).map(...) 変換と判定をつなげて書きたいとき
6 Objects.requireNonNull(s, "説明") null を受け付けない入口で早めに知らせる

&& は左側が false なら右側を評価しないので、型1の text != null && text.contains(key) は null のときに contains が呼ばれません。順番を逆にして text.contains(key) && text != null と書くと NPE になるため、null の確認を必ず左に置きます。

なお、日本語を含む出力が ? に置き換わる場合は、上の実行コマンドのように -Dstdout.encoding=UTF-8 を付けると解消します。標準出力の文字コードが実行環境のロケールに合わせて決まるためで、今回の検証環境でもこの指定を付けて実行しました。

大文字小文字を無視して含むかを判定する

contains は大文字と小文字を区別します。"Hello Java World".contains("JAVA") は false です。区別せずに判定する方法は4つあります。

import java.util.Locale;
import java.util.regex.Pattern;

public class IgnoreCase {
    // 方法2: regionMatches で1文字ずつずらして調べる(新しい文字列を作らない)
    static boolean containsIgnoreCase(String text, String keyword) {
        if (text == null || keyword == null) return false;
        int len = keyword.length();
        for (int i = 0; i <= text.length() - len; i++) {
            if (text.regionMatches(true, i, keyword, 0, len)) return true;
        }
        return false;
    }

    public static void main(String[] args) {
        String s = "Hello Java World";
        String key = "JAVA";

        System.out.println("contains そのまま      : " + s.contains(key));

        // 方法1: 両方を小文字にそろえる
        System.out.println("toLowerCase            : "
                + s.toLowerCase(Locale.ROOT).contains(key.toLowerCase(Locale.ROOT)));

        // 方法2: regionMatches(true, ...)
        System.out.println("regionMatches          : " + containsIgnoreCase(s, key));

        // 方法3: Pattern.CASE_INSENSITIVE + find()
        Pattern p = Pattern.compile(Pattern.quote(key), Pattern.CASE_INSENSITIVE);
        System.out.println("Pattern.find           : " + p.matcher(s).find());

        // 方法4: 正規表現の中に (?i) を書く(matches は全体一致なので前後に .* が必要)
        System.out.println("matches (?i)           : " + s.matches("(?i).*" + Pattern.quote(key) + ".*"));

        // 完全一致・前方一致・後方一致の大文字小文字無視
        System.out.println("equalsIgnoreCase       : " + "java".equalsIgnoreCase("JAVA"));
        System.out.println("startsWith 相当        : " + s.regionMatches(true, 0, "HELLO", 0, 5));
        String suffix = "WORLD";
        System.out.println("endsWith 相当          : "
                + s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length()));

        // 全角英字は CASE_INSENSITIVE だけでは対象外。UNICODE_CASE を足す
        String z = "JAVA入門";
        System.out.println("全角 CASE_INSENSITIVE  : "
                + Pattern.compile("java", Pattern.CASE_INSENSITIVE).matcher(z).find());
        System.out.println("全角 + UNICODE_CASE    : "
                + Pattern.compile("java", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher(z).find());
        System.out.println("全角 regionMatches     : " + containsIgnoreCase(z, "java"));

        // ロケールの影響: トルコ語ロケールでは I の小文字が「ı」になる
        Locale tr = Locale.forLanguageTag("tr");
        System.out.println("\"TITLE\" tr 小文字      : " + "TITLE".toLowerCase(tr));
        System.out.println("tr で contains(title)  : " + "TITLE".toLowerCase(tr).contains("title"));
        System.out.println("ROOT で contains(title): " + "TITLE".toLowerCase(Locale.ROOT).contains("title"));
    }
}

実行結果(java -Dstdout.encoding=UTF-8 IgnoreCase.java):

contains そのまま      : false
toLowerCase            : true
regionMatches          : true
Pattern.find           : true
matches (?i)           : true
equalsIgnoreCase       : true
startsWith 相当        : true
endsWith 相当          : true
全角 CASE_INSENSITIVE  : false
全角 + UNICODE_CASE    : true
全角 regionMatches     : true
"TITLE" tr 小文字      : tıtle
tr で contains(title)  : false
ROOT で contains(title): true

4つの方法の比較

方法 書き方 特徴 向いている場面
1. 小文字にそろえる s.toLowerCase(Locale.ROOT).contains(k.toLowerCase(Locale.ROOT)) 短く書ける。変換後の文字列が新しく作られる まずはこれで十分
2. regionMatches s.regionMatches(true, i, k, 0, len) をずらしながら呼ぶ 新しい文字列を作らない。全角英字にも対応 長い文字列を何度も調べる処理
3. Pattern + find Pattern.compile(Pattern.quote(k), CASE_INSENSITIVE) Pattern を使い回せる。正規表現と組み合わせやすい 同じ語で大量の行を調べる
4. (?i) + matches s.matches("(?i).*" + Pattern.quote(k) + ".*") 1行で書ける。呼ぶたびに正規表現を組み立てる 回数の少ない一度きりの判定

結果から押さえておきたい点は次のとおりです。

  • toLowerCase には Locale.ROOT を渡すと、実行環境の言語設定に左右されません。引数なしの toLowerCase は既定のロケールを使うため、トルコ語ロケールでは TITLE が tıtle(点のない ı)になり、contains("title") が false になりました。
  • Pattern.CASE_INSENSITIVE だけでは ASCII の範囲が対象です。全角英字やアクセントつきの文字も区別なく扱いたい場合は Pattern.UNICODE_CASE を一緒に指定します。
  • regionMatches と equalsIgnoreCase はロケールを考慮しないと API ドキュメントに明記されており、環境による差が出ません。
  • 前方一致・後方一致の大文字小文字無視は regionMatches で書けます。s.regionMatches(true, 0, prefix, 0, prefix.length()) が startsWith 相当です。

複数キーワードのAND・OR判定はStreamで書く

「この3語のどれかを含むか」「すべて含むか」は、contains を || や && で並べても書けますが、語の数が増えると読みにくくなります。キーワードをリストにして Stream の anyMatch / allMatch / noneMatch に渡すと、語が増えてもコードが変わりません。

import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class MultiKeyword {
    public static void main(String[] args) {
        String text = "Java 21 の新機能と Stream API の使い方";
        List<String> keys = List.of("Java", "Stream", "Kotlin");

        // OR: どれか1つでも含む
        boolean any = keys.stream().anyMatch(text::contains);
        // AND: すべて含む
        boolean all = keys.stream().allMatch(text::contains);
        // NOT: どれも含まない
        boolean none = keys.stream().noneMatch(text::contains);
        System.out.println("anyMatch (OR)   : " + any);
        System.out.println("allMatch (AND)  : " + all);
        System.out.println("noneMatch (NOT) : " + none);

        // どの語が当たったかを取り出す
        List<String> hits = keys.stream().filter(text::contains).toList();
        System.out.println("含まれていた語  : " + hits);

        // 当たった語の数(2つ以上含む、などの条件に使える)
        long count = keys.stream().filter(text::contains).count();
        System.out.println("当たった数      : " + count);

        // 大文字小文字を無視した OR 判定(正規表現にまとめる)
        String regex = keys.stream().map(Pattern::quote).collect(Collectors.joining("|"));
        Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        System.out.println("正規表現         : " + regex);
        System.out.println("find (OR, 無視)  : " + p.matcher("i like JAVA").find());

        // 複数の文を、除外語リストでふるい分ける
        List<String> ng = List.of("広告", "PR");
        List<String> lines = List.of("新着のお知らせ", "【PR】セール情報", "広告:新商品", "メンテナンス予定");
        List<String> ok = lines.stream()
                .filter(line -> ng.stream().noneMatch(line::contains))
                .toList();
        System.out.println("残った行         : " + ok);

        // 空のリストに注意: allMatch は true、anyMatch は false
        List<String> empty = List.of();
        System.out.println("空リスト allMatch : " + empty.stream().allMatch(text::contains));
        System.out.println("空リスト anyMatch : " + empty.stream().anyMatch(text::contains));
    }
}

実行結果(java -Dstdout.encoding=UTF-8 MultiKeyword.java):

anyMatch (OR)   : true
allMatch (AND)  : false
noneMatch (NOT) : false
含まれていた語  : [Java, Stream]
当たった数      : 2
正規表現         : \QJava\E|\QStream\E|\QKotlin\E
find (OR, 無視)  : true
残った行         : [新着のお知らせ, メンテナンス予定]
空リスト allMatch : true
空リスト anyMatch : false
判定 書き方 意味 キーワードが空のとき
OR keys.stream().anyMatch(text::contains) 1つでも含めば true false
AND keys.stream().allMatch(text::contains) すべて含めば true true
NOT keys.stream().noneMatch(text::contains) 1つも含まなければ true true
当たった語の一覧 keys.stream().filter(text::contains).toList() 含まれていた語のリスト 空のリスト
N個以上含む keys.stream().filter(text::contains).count() >= n 当たった数で判定 0

text::contains はメソッド参照で、k -> text.contains(k) と同じ意味です。anyMatch は true が確定した時点、allMatch は false が確定した時点で残りの語を調べずに終わるので、語が多くても余分な検索は走りません。

キーワードのリストが空のとき、allMatch は true を返します。「条件が1つもないので、すべて満たしている」という扱いです。検索画面でキーワード未入力のときに全件を当たりにしたくない場合は、!keys.isEmpty() && keys.stream().allMatch(...) のように空かどうかを先に確かめます。

大文字小文字を無視した OR 判定は、キーワードを | でつないで1つの正規表現にまとめる方法が簡潔です。出力にある \Q...\E は Pattern.quote が付けた「ここからここまではただの文字」という印です。toList() は Java 16 以降で使えます。それより前の版では collect(Collectors.toList()) に置き換えてください。

indexOfで位置を調べて切り出す

indexOf と lastIndexOf は、見つかった位置を substring に渡して文字列を切り出す用途でよく使います。出現回数を数える、すべての位置を集める、といった処理も同じ考え方で書けます。

import java.util.ArrayList;
import java.util.List;

public class IndexOfDemo {
    public static void main(String[] args) {
        String s = "apple,banana,apple,cherry,apple";

        // 位置を調べる
        System.out.println("indexOf(\"apple\")         : " + s.indexOf("apple"));
        System.out.println("indexOf(\"apple\", 1)      : " + s.indexOf("apple", 1));
        System.out.println("lastIndexOf(\"apple\")     : " + s.lastIndexOf("apple"));
        System.out.println("lastIndexOf(\"apple\", 25) : " + s.lastIndexOf("apple", 25));
        System.out.println("indexOf('b')             : " + s.indexOf('b'));
        System.out.println("indexOf(\"grape\")         : " + s.indexOf("grape"));

        // Java 21 から: 範囲を指定して探す(beginIndex 以上 endIndex 未満)
        System.out.println("indexOf(\"apple\", 1, 18)  : " + s.indexOf("apple", 1, 18));
        System.out.println("indexOf(\"apple\", 1, 17)  : " + s.indexOf("apple", 1, 17));

        // すべての出現位置を集める
        List<Integer> positions = new ArrayList<>();
        for (int i = s.indexOf("apple"); i >= 0; i = s.indexOf("apple", i + 1)) {
            positions.add(i);
        }
        System.out.println("すべての位置             : " + positions);
        System.out.println("出現回数                 : " + positions.size());

        // 1回だけ出てくるか(最初と最後の位置が同じ)
        System.out.println("banana は1回だけか       : "
                + (s.indexOf("banana") >= 0 && s.indexOf("banana") == s.lastIndexOf("banana")));

        // 見つかった位置で切り出す
        String file = "/home/taro/docs/memo.final.txt";
        System.out.println("ファイル名               : " + file.substring(file.lastIndexOf('/') + 1));
        System.out.println("拡張子                   : " + file.substring(file.lastIndexOf('.') + 1));
        String kv = "name=Taro=Yamada";
        int eq = kv.indexOf('=');
        System.out.println("キー / 値                : " + kv.substring(0, eq) + " / " + kv.substring(eq + 1));

        // -1 のまま substring に渡すと止まる
        try {
            String noDot = "README";
            System.out.println(noDot.substring(0, noDot.indexOf('.')));
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println("例外                     : " + e.getMessage());
        }
    }
}

実行結果(java -Dstdout.encoding=UTF-8 IndexOfDemo.java):

indexOf("apple")         : 0
indexOf("apple", 1)      : 13
lastIndexOf("apple")     : 26
lastIndexOf("apple", 25) : 13
indexOf('b')             : 6
indexOf("grape")         : -1
indexOf("apple", 1, 18)  : 13
indexOf("apple", 1, 17)  : -1
すべての位置             : [0, 13, 26]
出現回数                 : 3
banana は1回だけか       : true
ファイル名               : memo.final.txt
拡張子                   : txt
キー / 値                : name / Taro=Yamada
例外                     : Range [0, -1) out of bounds for length 6

indexOf・lastIndexOfの引数の一覧

書き方 探す範囲 補足
indexOf(str) 先頭から末尾へ 最初に見つかった位置
indexOf(str, fromIndex) fromIndex から末尾へ fromIndex が長さを超えても例外にならず -1
indexOf(str, beginIndex, endIndex) beginIndex 以上 endIndex 未満 Java 21 で追加。範囲が不正だと StringIndexOutOfBoundsException
indexOf(ch) 先頭から末尾へ 1文字(char / コードポイント)を探す
lastIndexOf(str) 末尾から先頭へ 最後に見つかった位置
lastIndexOf(str, fromIndex) fromIndex から先頭へ fromIndex 以前で最後に見つかった位置

範囲つきの indexOf(3引数)は、Java 21 の API ドキュメントに「Since: 21」と記載されていることを確認しました。上の実行結果では、範囲の終わりを 18 にすると 13 が返り、17 にすると語の末尾が範囲に収まらないため -1 になっています。Java 17 以前で同じことをしたい場合は、s.substring(begin, end).indexOf(str) の結果に begin を足します(-1 のときは足さずにそのまま扱います)。

切り出しで気をつけたいのは、見つからなかったときの -1 をそのまま substring に渡さないことです。上の例では Range [0, -1) out of bounds for length 6 というメッセージの StringIndexOutOfBoundsException になりました。次のように、位置を変数に取ってから分岐すると落ち着いて扱えます。

int dot = name.lastIndexOf('.');
String ext = (dot >= 0) ? name.substring(dot + 1) : "";

空文字・全角・絵文字での結果を確かめる

細かな挙動も一度実行して確かめておくと、思い込みによる取り違えを減らせます。

対象の文字列は String s = "Java"; です。左側に書いた式をそのまま System.out.println で出力しています(最後の3行だけは String e = "A😀B"; に対する結果で、codePointCount は先頭から B の手前までを数えています)。

実行結果(EdgeCases.java として -Dstdout.encoding=UTF-8 つきで実行):

contains("")        : true
indexOf("")         : 0
lastIndexOf("")     : 4
startsWith("")      : true
endsWith("")        : true
matches("")         : false
equals("")          : false
contains("java")    : false
contains("Java"): false
" Java ".equals    : false
strip 後 equals     : true
startsWith("va", 2) : true
contains(sb)        : true
indexOf('v') >= 0   : true
length              : 4
indexOf("B")        : 3
codePointCount      : 2
入力 結果 覚えておきたいこと
空文字を探す contains・startsWith・endsWith は true、indexOf は 0 入力欄が空のまま検索すると全件が当たりになる。必要なら isEmpty で先に確かめる
大文字小文字の違い 別の文字として扱われる 前の章の方法で無視できる
全角と半角 別の文字として扱われる java.text.Normalizer の NFKC でそろえてから調べる方法がある
前後の空白 equals では一致しない strip(Java 11以降)で取り除いてから比べる
1文字(char)を探す contains には渡せない indexOf('v') >= 0 か contains("v")
絵文字を含む文字列 位置は char 単位(絵文字は2つ分) 見た目の文字数が必要なら codePointCount で数える

用途別のコピペ用スニペット集

よく出てくる判定を、短い形でまとめます。すべて上の検証で使ったメソッドの組み合わせです。

// 拡張子の判定(大文字の .CSV も対象にする)
boolean isCsv = fileName.toLowerCase(Locale.ROOT).endsWith(".csv");

// 複数の拡張子のどれかで終わるか
boolean isImage = Stream.of(".png", ".jpg", ".gif")
        .anyMatch(fileName.toLowerCase(Locale.ROOT)::endsWith);

// URL の種類を見分ける
boolean isHttps = url.startsWith("https://");

// コメント行と空行を読み飛ばす
boolean skip = line.isBlank() || line.strip().startsWith("#");

// 数字だけで構成されているか(全体一致なので matches が合う)
boolean digitsOnly = code.matches("\\d+");

// 半角英数字4〜8文字か
boolean valid = id.matches("[0-9A-Za-z]{4,8}");

// 区切り文字の前後で分ける(見つからないときは全体を前半とする)
int at = text.indexOf(':');
String head = (at >= 0) ? text.substring(0, at) : text;
String tail = (at >= 0) ? text.substring(at + 1) : "";

// リストの中から、キーワードを含む要素だけを取り出す
List<String> found = list.stream().filter(v -> v != null && v.contains(key)).toList();

上のスニペットは断片なので、java.util.Locale、java.util.List、java.util.stream.Stream の import と変数の定義を添えて使ってください。形式の確認(数字だけ、英数字だけ)は「文字列全体がその形か」を見たいので、全体一致の matches がそのまま合います。部分一致は contains、全体の形は matches と使い分けると、どちらも短く書けます。

速度の目安と選び方

同じ「含むか」の判定でも、メソッドによって処理にかかる時間が変わります。約400文字の文字列(中央に needle という語を置いたもの)に対して判定を繰り返し、かかった時間を測りました。

計測は、4つの書き方をそれぞれ200万回のループで回し、System.nanoTime() の差を取る単純な方法です(1周目はウォームアップとして捨て、2周目の値を採用)。

実行結果(OpenJDK 21.0.10、1回の計測値):

contains          :   101 ms
indexOf >= 0      :    67 ms
matches(.*x.*)    :  3211 ms
Pattern 再利用find:   598 ms

この数値は検証用のコンテナで1回測ったもので、機械や文字列の長さによって変わります。専用の計測ツールを使った厳密な比較ではないので、傾向として読んでください。

  • contains と indexOf はほぼ同じ速さで、今回の条件では正規表現を使う方法より短い時間で終わりました。
  • String.matches は呼ぶたびに正規表現を組み立てるため、繰り返しの中で使うと時間がかかります。同じパターンを何度も使うなら、Pattern.compile を1回だけ行い、Matcher を作って find する形にすると今回は5分の1ほどになりました。
  • 決まった語を探すだけなら、読みやすさの面でも速さの面でも contains が第一候補です。

つまずきやすい点のチェックリスト

症状 原因 直し方
含まれているのに matches が false matches は全体一致 contains か find を使う。matches なら前後に .* を付ける
先頭にあるのに「含まない」と判定される indexOf(key) > 0 と書いている >= 0 に直す
NullPointerException で止まる 対象か引数が null s != null && を左に置く。定数は左に書く
大文字のキーワードが当たらない contains は大文字小文字を区別する toLowerCase(Locale.ROOT) でそろえる
記号を含む語で判定がずれる 正規表現の記号として解釈されている Pattern.quote で包む、または contains を使う
空のキーワードで全件が当たる 空文字はどの文字列にも含まれる isEmpty / isBlank で先に確かめる
substring で例外になる indexOf の -1 をそのまま渡している 位置を変数に取り、0 以上か確かめてから切り出す
== で比べると結果が安定しない == は同じオブジェクトかどうかの比較 内容の比較は equals を使う

よくある質問

Q. contains と indexOf はどちらが速いですか? A. ほぼ同じです。contains は内部で indexOf を呼び、結果が 0 以上かを返しています。今回の計測でも 101 ms と 67 ms で、体感できる差ではありませんでした。含むかどうかだけなら読みやすい contains、位置が必要なら indexOf と、目的で選ぶのがおすすめです。

Q. 「含まない」ことを判定するメソッドはありますか? A. String に専用のメソッドはないので、!s.contains(key) と否定を付けます。複数の語のどれも含まないことを確かめるなら keys.stream().noneMatch(s::contains) が使えます。

Q. contains で正規表現やワイルドカードは使えますか? A. 使えません。contains は渡された文字の並びをそのまま探します。パターンで探したいときは Pattern.compile(正規表現).matcher(s).find() を使います。String.matches は全体一致なので、部分一致のつもりで使うと false になります。

Q. List や配列に要素が含まれるかも contains で調べられますか? A. List には list.contains(要素) があり、要素と equals で一致するものがあるかを返します。こちらは「リストの中に同じ値があるか」の判定で、文字列の部分一致ではありません。「キーワードを含む要素があるか」を調べたいときは list.stream().anyMatch(v -> v.contains(key)) と書きます。配列は Arrays.asList(配列).contains(要素) で同じように調べられます。

まとめ

  • 含むかどうかは contains、位置が必要なら indexOf / lastIndexOf。indexOf の「見つからない」は -1 なので、判定は >= 0 で書く
  • 先頭は startsWith、末尾は endsWith、完全一致は equals。いずれも正規表現は解釈しない
  • matches は全体一致。正規表現で部分一致を調べるなら Matcher の find()、決まった語なら contains
  • null には s != null && を左に置く。定数との比較は "定数".equals(s)、両方 null になりうるなら Objects.equals
  • 大文字小文字の無視は toLowerCase(Locale.ROOT)・regionMatches・Pattern.CASE_INSENSITIVE の3通り。全角英字まで含めるなら UNICODE_CASE を足す
  • 複数キーワードは Stream の anyMatch(OR)・allMatch(AND)・noneMatch(NOT)。空のリストでは allMatch が true になる
  • 繰り返し使う正規表現は Pattern.compile を1回だけ行って使い回す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java文字列検索 containsとindexOfの違い 含むか判定の早見表」

株式会社RJCでは、Javaの文字列検索のような基礎を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?