- JAVA
Java文字列検索 containsとindexOfの違い 含むか判定の早見表
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期連続の黒字決算。