- JAVA
Java List contains・indexOfの使い方とequals
JavaのListに要素があるかどうかは contains(戻り値は true か false)、どこにあるかは indexOf(戻り値は位置、見つからなければ -1)で調べます。 どちらも要素の equals メソッドで「同じかどうか」を判定するため、自作クラスのリストでは equals の書き方が結果を左右します。
この記事では、contains・indexOf・lastIndexOf・containsAll・anyMatch・Collections.disjoint を「戻り値/見つからない時/条件指定の可否」で比べる早見表を最初に示します。続いて、equals を書いていないクラスで contains が false になる様子を実行結果で再現し、equals の実装と record の2通りで直します。 型違いで見つからないケース、null の扱い、10万件での List と HashSet の実測時間まで、すべて1ファイルで動くコードと出力を載せています。
確認環境:Ubuntu 24.04、OpenJDK 21.0.10(java ファイル名.java で実行)。
存在チェックのメソッド早見表
| メソッド | 戻り値 | 見つからない時 | 条件指定の可否 | 使いどころ |
|---|---|---|---|---|
list.contains(値) |
boolean | false | 不可(equals で一致するものだけ) | あるかないかだけ知りたい |
list.indexOf(値) |
int(最初の位置、0始まり) | -1 | 不可 | 位置がほしい・先頭から探す |
list.lastIndexOf(値) |
int(最後の位置) | -1 | 不可 | 同じ値が複数あり、最後の位置がほしい |
list.containsAll(コレクション) |
boolean | false(1つでも欠けたら) | 不可 | 複数の値がすべてあるか |
Collections.disjoint(a, b) |
boolean | true(共通の要素がない時に true) | 不可 | 2つのリストに共通の値が1つもないか |
list.stream().anyMatch(条件) |
boolean | false | 可(ラムダ式で自由に書ける) | 前方一致・大文字小文字を無視・一部の項目で探す |
list.stream().filter(条件).findFirst() |
Optional |
Optional.empty | 可 | 見つかった要素そのものがほしい |
set.contains(値)(HashSet) |
boolean | false | 不可 | 件数が多く、何度も調べる |
| つまずきやすい点 | 起きること | 直し方 |
|---|---|---|
| 自作クラスに equals がない | 中身が同じでも contains が false | equals と hashCode を書く、または record にする |
| Long のリストに int を渡す | contains(1) が false |
contains(1L) と型をそろえる |
Integer のリストで remove(1) |
値 1 ではなく位置 1 が消える | remove(Integer.valueOf(1)) |
List.of のリストに contains(null) |
NullPointerException | null を渡さない・先に null を確かめる |
| 大文字と小文字が違う | contains("java") が false |
anyMatch(s -> s.equalsIgnoreCase("java")) |
| 10万件のリストを何度も探す | 件数に比例して時間がかかる | HashSet に入れ替えてから探す |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java List contains・indexOfの使い方とequals」
基本の使い方:contains・indexOf・lastIndexOf・containsAll
まずは文字列のリストで、6つの書き方の戻り値を並べます。
import java.util.*;
public class Basic {
public static void main(String[] args) {
List<String> tags = List.of("java", "sql", "git", "java", "html");
System.out.println("contains(\"sql\") : " + tags.contains("sql"));
System.out.println("contains(\"go\") : " + tags.contains("go"));
System.out.println("indexOf(\"java\") : " + tags.indexOf("java"));
System.out.println("lastIndexOf(\"java\") : " + tags.lastIndexOf("java"));
System.out.println("indexOf(\"go\") : " + tags.indexOf("go"));
System.out.println("containsAll([git,sql]): " + tags.containsAll(List.of("git", "sql")));
System.out.println("containsAll([git,go]) : " + tags.containsAll(List.of("git", "go")));
System.out.println("containsAll([]) : " + tags.containsAll(List.of()));
System.out.println("anyMatch(jで始まる) : " + tags.stream().anyMatch(t -> t.startsWith("j")));
System.out.println("disjoint([go,rust]) : " + Collections.disjoint(tags, List.of("go", "rust")));
System.out.println("disjoint([go,git]) : " + Collections.disjoint(tags, List.of("go", "git")));
}
}
実行結果:
contains("sql") : true
contains("go") : false
indexOf("java") : 0
lastIndexOf("java") : 3
indexOf("go") : -1
containsAll([git,sql]): true
containsAll([git,go]) : false
containsAll([]) : true
anyMatch(jで始まる) : true
disjoint([go,rust]) : true
disjoint([go,git]) : false
- indexOf は先頭から探して最初に見つかった位置、lastIndexOf は末尾から探して最初に見つかった位置を返します。 "java" は位置0と位置3にあるので、それぞれ 0 と 3 になりました。位置は 0 から数えます。
- 見つからないとき、indexOf と lastIndexOf は -1 を返します。 例外にはなりません。そのため、
if (list.indexOf(x) >= 0)と書けば contains と同じ判定になります。 - containsAll は、引数のすべての値がリストに入っていれば true です。空のコレクションを渡すと true になります(調べる対象が1つもないため)。入力が空の可能性があるときは、先に isEmpty で分けると意図が伝わります。
- Collections.disjoint は「共通の要素が1つもなければ true」です。「どれか1つでも含まれるか」を知りたいときは、結果を反転して
!Collections.disjoint(a, b)と書きます。
containsとindexOfのどちらを使うか
| 知りたいこと | 書き方 |
|---|---|
| あるかないか | list.contains(x) |
| 何番目にあるか | list.indexOf(x) |
| あれば、その位置の要素を置き換えたい | int i = list.indexOf(x); if (i >= 0) list.set(i, y); |
| 2回以上出てくるか | list.indexOf(x) != list.lastIndexOf(x) |
| 複数の値がすべてあるか | list.containsAll(List.of(a, b)) |
| 複数の値のどれかがあるか | !Collections.disjoint(list, List.of(a, b)) |
equalsがないクラスではcontainsがfalseになる
公式ドキュメントでは、contains は「Objects.equals(o, e) が成り立つ要素 e が1つ以上あるとき true」と定義されています。indexOf と lastIndexOf も同じ判定です。つまり、要素の equals が「同じ」と答えるかどうかがすべてです。
equals を書いていないクラスで試します。
import java.util.*;
public class NoEquals {
// equalsを書いていないクラス
static class Book {
final String isbn;
final String title;
Book(String isbn, String title) { this.isbn = isbn; this.title = title; }
}
public static void main(String[] args) {
Book first = new Book("978-1", "Java入門");
List<Book> shelf = new ArrayList<>();
shelf.add(first);
Book same = new Book("978-1", "Java入門"); // 中身は同じ、別のインスタンス
System.out.println("contains(same) : " + shelf.contains(same));
System.out.println("indexOf(same) : " + shelf.indexOf(same));
System.out.println("contains(first) : " + shelf.contains(first));
System.out.println("first.equals(same): " + first.equals(same));
System.out.println("表示 : " + same);
}
}
実行結果:
contains(same) : false
indexOf(same) : -1
contains(first) : true
first.equals(same): false
表示 : NoEquals$Book@3b938003
ISBNもタイトルも同じ本なのに、contains は false、indexOf は -1 になりました。equals を書いていないクラスは、Object クラスの equals をそのまま使います。これは「同じインスタンスかどうか」だけを比べるため、new で別に作ったものは、中身が同じでも「違う」と判定されます。 リストに入れたのと同じインスタンス(first)を渡した場合だけ true です。
最後の行の NoEquals$Book@3b938003 のような表示は、toString も書いていないときの既定の形です(@ の後ろの値は実行のたびに変わることがあります)。この形の表示を見かけたら、equals も未定義の可能性が高いと考えると見つけやすくなります。
直し方1:equalsとhashCodeを実装する
import java.util.*;
public class WithEquals {
static class Book {
final String isbn;
final String title;
Book(String isbn, String title) { this.isbn = isbn; this.title = title; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book other)) return false;
return Objects.equals(isbn, other.isbn) && Objects.equals(title, other.title);
}
@Override
public int hashCode() {
return Objects.hash(isbn, title);
}
@Override
public String toString() {
return "Book(" + isbn + ", " + title + ")";
}
}
public static void main(String[] args) {
List<Book> shelf = new ArrayList<>();
shelf.add(new Book("978-1", "Java入門"));
shelf.add(new Book("978-2", "SQL入門"));
Book same = new Book("978-2", "SQL入門");
System.out.println("contains(same): " + shelf.contains(same));
System.out.println("indexOf(same) : " + shelf.indexOf(same));
System.out.println("HashSetでも : " + new HashSet<>(shelf).contains(same));
}
}
実行結果:
contains(same): true
indexOf(same) : 1
HashSetでも : true
equals の書き方の要点は次の3つです。
| 行 | 役割 |
|---|---|
if (this == o) return true; |
同じインスタンスならすぐ true |
if (!(o instanceof Book other)) return false; |
型が違う・null のときは false(Java 16以降のパターンマッチ) |
Objects.equals(isbn, other.isbn) && ... |
項目どうしを比べる(項目が null でも安全) |
equals を書いたら hashCode も必ずセットで書きます。 List の contains は equals だけで判定しますが、HashSet や HashMap は先に hashCode で絞り込むためです。equals だけ書いた場合にどうなるかを見てみます。
import java.util.*;
public class HashOnly {
// equalsだけ書いて、hashCodeを書いていないクラス
static class Book {
final String isbn;
Book(String isbn) { this.isbn = isbn; }
@Override
public boolean equals(Object o) {
return o instanceof Book other && isbn.equals(other.isbn);
}
}
public static void main(String[] args) {
List<Book> list = List.of(new Book("978-1"));
Set<Book> set = new HashSet<>(list);
Book same = new Book("978-1");
System.out.println("List.contains : " + list.contains(same));
System.out.println("HashSet.contains: " + set.contains(same));
}
}
実行結果:
List.contains : true
HashSet.contains: false
List では見つかるのに、HashSet では見つかりません。後でリストを HashSet に入れ替えたときに結果が変わってしまうので、最初からセットで書いておくと安心です。
直し方2:recordにする
Java 16 以降なら、record を使うと equals・hashCode・toString が自動で用意されます。すべての項目が等しければ「同じ」と判定されます。
import java.util.*;
public class WithRecord {
record Book(String isbn, String title) {}
public static void main(String[] args) {
List<Book> shelf = List.of(
new Book("978-1", "Java入門"),
new Book("978-2", "SQL入門"));
Book same = new Book("978-2", "SQL入門");
System.out.println("contains(same): " + shelf.contains(same));
System.out.println("indexOf(same) : " + shelf.indexOf(same));
System.out.println("HashSetでも : " + new HashSet<>(shelf).contains(same));
// 一部の項目だけで探す(ISBNだけ一致すればよい)
boolean hasIsbn = shelf.stream().anyMatch(b -> b.isbn().equals("978-1"));
System.out.println("ISBNだけで探す: " + hasIsbn);
// 見つかった要素そのものがほしい
Optional<Book> found = shelf.stream()
.filter(b -> b.title().startsWith("SQL")).findFirst();
System.out.println("findFirst : " + found);
System.out.println("見つからない時: " + shelf.stream()
.filter(b -> b.title().startsWith("Go")).findFirst());
// 条件に合う位置(添字)がほしい
int idx = java.util.stream.IntStream.range(0, shelf.size())
.filter(i -> shelf.get(i).title().startsWith("SQL"))
.findFirst().orElse(-1);
System.out.println("条件で位置 : " + idx);
}
}
実行結果:
contains(same): true
indexOf(same) : 1
HashSetでも : true
ISBNだけで探す: true
findFirst : Optional[Book[isbn=978-2, title=SQL入門]]
見つからない時: Optional.empty
条件で位置 : 1
record にするだけで contains も indexOf も HashSet も期待どおりに動きました。データを入れるだけのクラスは record にするのが、もっとも短く書ける直し方です。
このコードの後半は、「全項目の一致」ではなく「一部の項目で探す」書き方です。
| やりたいこと | 書き方 | 結果 |
|---|---|---|
| 一部の項目が一致するものがあるか | anyMatch(b -> b.isbn().equals("978-1")) |
true |
| 見つかった要素そのものがほしい | filter(条件).findFirst() |
Optional[Book[...]]、なければ Optional.empty |
| 条件に合う位置がほしい | IntStream.range(0, size).filter(i -> 条件).findFirst().orElse(-1) |
1 |
equals を変えずに「ISBNだけで探す」ことができるのが anyMatch の利点です。 探し方が場面ごとに違うとき(ISBNで探す、タイトルの先頭で探す)は、equals に手を入れず、条件をラムダ式で渡すほうが影響が小さくなります。
equals実装・record・anyMatchの選び方
| 状況 | 選ぶもの |
|---|---|
| 新しく作るデータ用のクラス(Java 16以降) | record |
| 既存のクラスで、項目を変更する必要がある | equals と hashCode を実装 |
| 一部の項目だけで探したい・探し方が複数ある | anyMatch/filter と findFirst |
| 既存クラスの equals に手を入れられない | anyMatch |
型違いで見つからないケース:LongとInteger、removeとの混同
コンパイルは通るのに結果が期待と違う、というケースをまとめて実行します。contains や indexOf の引数は Object 型なので、どんな型を渡してもコンパイルエラーになりません。
import java.util.*;
public class Types {
public static void main(String[] args) {
// 1. List<Long> に int を渡す
List<Long> ids = List.of(1L, 2L, 3L);
System.out.println("ids.contains(1) : " + ids.contains(1));
System.out.println("ids.contains(1L) : " + ids.contains(1L));
System.out.println("ids.indexOf(2) : " + ids.indexOf(2));
System.out.println("ids.indexOf(2L) : " + ids.indexOf(2L));
int n = 3;
System.out.println("contains((long) n): " + ids.contains((long) n));
System.out.println("Integer.equals(Long): " + Integer.valueOf(1).equals(Long.valueOf(1L)));
// 2. List<Integer> の remove は「位置」と「値」で別のメソッド
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30, 1));
System.out.println("indexOf(1) : " + nums.indexOf(1));
nums.remove(1); // remove(int index) → 位置1の「20」が消える
System.out.println("remove(1) の後 : " + nums);
nums.remove(Integer.valueOf(1)); // remove(Object o) → 値「1」が消える
System.out.println("remove(Integer.valueOf(1)) の後: " + nums);
// 3. 文字列と数値
List<String> codes = List.of("1", "2", "3");
System.out.println("codes.contains(1) : " + codes.contains(1));
System.out.println("codes.contains(\"1\") : " + codes.contains("1"));
// 4. 大文字と小文字
List<String> langs = List.of("Java", "Go", "Rust");
System.out.println("contains(\"java\") : " + langs.contains("java"));
System.out.println("equalsIgnoreCase : "
+ langs.stream().anyMatch(s -> s.equalsIgnoreCase("java")));
// 5. 前後の空白
System.out.println("contains(\" Go\") : " + langs.contains(" Go"));
System.out.println("strip してから : " + langs.contains(" Go".strip()));
// 6. 配列は Arrays.asList / List.of で包む
String[] arr = {"a", "b"};
System.out.println("Arrays.asList(arr).contains(\"b\"): " + Arrays.asList(arr).contains("b"));
int[] prim = {1, 2, 3};
System.out.println("Arrays.asList(int[]).contains(1): " + Arrays.asList(prim).contains(1));
System.out.println("Arrays.stream(int[]).anyMatch : "
+ Arrays.stream(prim).anyMatch(x -> x == 1));
}
}
実行結果:
ids.contains(1) : false
ids.contains(1L) : true
ids.indexOf(2) : -1
ids.indexOf(2L) : 1
contains((long) n): true
Integer.equals(Long): false
indexOf(1) : 3
remove(1) の後 : [10, 30, 1]
remove(Integer.valueOf(1)) の後: [10, 30]
codes.contains(1) : false
codes.contains("1") : true
contains("java") : false
equalsIgnoreCase : true
contains(" Go") : false
strip してから : true
Arrays.asList(arr).contains("b"): true
Arrays.asList(int[]).contains(1): false
Arrays.stream(int[]).anyMatch : true
Longのリストにintを渡すとfalseになる
ids.contains(1) の 1 は int なので、自動で Integer に変換されます。リストの中身は Long です。Integer と Long は、値が同じ 1 でも equals が false を返します(実行結果の Integer.equals(Long): false)。そのため contains も false、indexOf も -1 になります。
| 書き方 | 結果 | 理由 |
|---|---|---|
ids.contains(1) |
false | Integer の 1 と Long の 1 は別の型 |
ids.contains(1L) |
true | Long どうし |
ids.contains((long) n) |
true | int の変数は long に変換してから渡す |
データベースのIDを Long で持っているリストに、画面から受け取った int の値を渡す、といった場面で起きやすい動きです。変数の型をリストの要素の型にそろえるのが確実な直し方です。
Integerのリストでremoveを呼ぶときは位置か値かを確かめる
indexOf で位置を調べたあと、remove で消す流れはよく使います。ここで List には2つの remove があります。
| メソッド | 引数 | 動き |
|---|---|---|
remove(int index) |
位置 | その位置の要素を取り除く |
remove(Object o) |
値 | equals で一致する最初の要素を取り除く |
nums.remove(1) は int を渡しているので、位置 1 の要素(20)が消えました。値の 1 を消したいときは nums.remove(Integer.valueOf(1)) と書きます。contains と indexOf は常に「値」で探しますが、remove は int を渡すと「位置」になる、という違いを覚えておくと混同を防げます。
そのほかの型・表記の違い
| ケース | 結果 | 直し方 |
|---|---|---|
文字列のリストに数値を渡す codes.contains(1) |
false | contains("1") または String.valueOf(n) |
大文字と小文字 contains("java") |
false | anyMatch(s -> s.equalsIgnoreCase("java")) |
前後の空白 contains(" Go") |
false | strip で空白を取り除いてから渡す |
int の配列を Arrays.asList に渡す |
false(要素が配列1つのリストになる) | Arrays.stream(配列).anyMatch(x -> x == 1) |
nullを探す・nullが混ざるとき
null の扱いは、リストの作り方で変わります。
import java.util.*;
public class Nulls {
public static void main(String[] args) {
// ArrayList は null を入れられ、null を探せる
List<String> a = new ArrayList<>(Arrays.asList("x", null, "y"));
System.out.println("ArrayList contains(null): " + a.contains(null));
System.out.println("ArrayList indexOf(null) : " + a.indexOf(null));
// List.of で作ったリストは contains(null) で止まる
List<String> b = List.of("x", "y");
try {
b.contains(null);
} catch (NullPointerException e) {
System.out.println("List.of contains(null) : " + e);
}
try {
b.indexOf(null);
} catch (NullPointerException e) {
System.out.println("List.of indexOf(null) : " + e);
}
// Arrays.asList と Collections.unmodifiableList は null を探せる
System.out.println("Arrays.asList : " + Arrays.asList("x", "y").contains(null));
System.out.println("unmodifiableList : "
+ Collections.unmodifiableList(new ArrayList<>(List.of("x"))).contains(null));
// stream().toList() は null を探せる(List.copyOf は止まる)
System.out.println("stream().toList() : " + a.stream().toList().contains(null));
try {
List.copyOf(a);
} catch (NullPointerException e) {
System.out.println("List.copyOf(nullあり) : " + e);
}
// リストそのものが null のとき
List<String> none = null;
try {
none.contains("x");
} catch (NullPointerException e) {
System.out.println("リストがnull : " + e.getClass().getName());
}
System.out.println("nullを先に確かめる : " + (none != null && none.contains("x")));
// anyMatch のラムダで null 要素のメソッドを呼ぶと止まる
try {
a.stream().anyMatch(s -> s.equals("y"));
} catch (NullPointerException e) {
System.out.println("anyMatch(s.equals) : " + e.getClass().getName());
}
System.out.println("anyMatch(\"y\"::equals) : " + a.stream().anyMatch("y"::equals));
}
}
実行結果:
ArrayList contains(null): true
ArrayList indexOf(null) : 1
List.of contains(null) : java.lang.NullPointerException
List.of indexOf(null) : java.lang.NullPointerException
Arrays.asList : false
unmodifiableList : false
stream().toList() : true
List.copyOf(nullあり) : java.lang.NullPointerException
リストがnull : java.lang.NullPointerException
nullを先に確かめる : false
anyMatch(s.equals) : java.lang.NullPointerException
anyMatch("y"::equals) : true
| リストの作り方 | null の要素 | contains(null) |
|---|---|---|
new ArrayList<>() |
入れられる | true/false を返す |
Arrays.asList(...) |
入れられる | true/false を返す |
Collections.unmodifiableList(...) |
元のリストしだい | true/false を返す |
stream().toList() |
入れられる | true/false を返す |
List.of(...)/List.copyOf(...) |
入れられない | NullPointerException |
List.of で作ったリストは、null を入れられないだけでなく、contains(null) や indexOf(null) を呼ぶだけでも NullPointerException になります。 公式ドキュメントは、List.of と List.copyOf で作る変更できないリストについて「null の要素を受け付けず、null を含めて作ろうとすると NullPointerException になる」と説明しています。探す値が null になりうるときは、x != null && list.contains(x) のように先に確かめます。
List.of に null を渡した場合のスタックトレースは次のとおりです。
import java.util.List;
public class NullTrace {
public static void main(String[] args) {
List<String> tags = List.of("java", null, "sql");
System.out.println(tags.contains("java"));
}
}
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at java.base/java.util.ImmutableCollections.listFromTrustedArray(ImmutableCollections.java:215)
at java.base/java.util.List.of(List.java:972)
at NullTrace.main(NullTrace.java:5)
List.of の行と Objects.requireNonNull が並んでいれば、「List.of に null が入った」と読み取れます。contains を呼ぶ前、リストを作る時点で止まっている点に注目してください。
もう2つ、実行結果から読み取れる点があります。
- リストの変数そのものが null のときも NullPointerException になります。
none != null && none.contains("x")のように先に確かめるか、空のリスト(List.of())を初期値にしておくと、確かめる手間が減ります。 - anyMatch のラムダ式で
s.equals("y")と書くと、要素に null が混ざっているときに止まります。"y"::equalsのように、null でないとわかっている側を左に置くと、null が混ざっていても安全に比べられます。
10万件でのListとHashSetの実測
contains は、ArrayList では先頭から順に1件ずつ比べます。件数が多く、何度も調べる場合は時間が気になってきます。10万件のリストに対して1万回の検索(約半分は存在しない値)を行い、時間を測りました。
import java.util.*;
public class Bench {
public static void main(String[] args) {
int n = 100_000;
List<Integer> list = new ArrayList<>(n);
for (int i = 0; i < n; i++) list.add(i);
List<Integer> linked = new LinkedList<>(list);
Set<Integer> set = new HashSet<>(list);
int[] keys = new Random(42).ints(10_000, 0, n * 2).toArray(); // 約半分は存在しない値
for (int round = 1; round <= 3; round++) {
long t0 = System.nanoTime();
int hit1 = 0;
for (int k : keys) if (list.contains(k)) hit1++;
long t1 = System.nanoTime();
int hit2 = 0;
for (int k : keys) if (set.contains(k)) hit2++;
long t2 = System.nanoTime();
int hit3 = 0;
for (int k : keys) { final int key = k; if (list.stream().anyMatch(x -> x == key)) hit3++; }
long t3 = System.nanoTime();
int hit4 = 0;
for (int k : keys) if (linked.contains(k)) hit4++;
long t4 = System.nanoTime();
int hit5 = 0;
for (int k : keys) if (Collections.binarySearch(list, k) >= 0) hit5++;
long t5 = System.nanoTime();
System.out.printf("%d回目 ArrayList.contains %5d ms / HashSet.contains %6.2f ms / anyMatch %5d ms / LinkedList.contains %5d ms / binarySearch %6.2f ms (hit %d,%d,%d,%d,%d)%n",
round, (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000.0, (t3 - t2) / 1_000_000,
(t4 - t3) / 1_000_000, (t5 - t4) / 1_000_000.0, hit1, hit2, hit3, hit4, hit5);
}
long s0 = System.nanoTime();
Set<Integer> built = new HashSet<>(list);
long s1 = System.nanoTime();
System.out.printf("HashSetを作る時間 %.2f ms (%d件)%n", (s1 - s0) / 1_000_000.0, built.size());
}
}
実行結果(2コアの仮想マシン、同じ処理を3回くり返し):
1回目 ArrayList.contains 821 ms / HashSet.contains 7.41 ms / anyMatch 2299 ms / LinkedList.contains 2287 ms / binarySearch 10.45 ms (hit 4901,4901,4901,4901,4901)
2回目 ArrayList.contains 752 ms / HashSet.contains 5.69 ms / anyMatch 3514 ms / LinkedList.contains 3866 ms / binarySearch 3.39 ms (hit 4901,4901,4901,4901,4901)
3回目 ArrayList.contains 1646 ms / HashSet.contains 0.42 ms / anyMatch 4614 ms / LinkedList.contains 4515 ms / binarySearch 1.21 ms (hit 4901,4901,4901,4901,4901)
HashSetを作る時間 90.80 ms (100000件)
| 方法(1万回の検索) | 今回の計測 | しくみ |
|---|---|---|
ArrayList.contains |
752〜1,646 ms | 先頭から順に比べる(件数に比例) |
HashSet.contains |
0.42〜7.41 ms | ハッシュ値で場所を直接引く |
stream().anyMatch |
2,299〜4,614 ms | 先頭から順に条件を評価 |
LinkedList.contains |
2,287〜4,515 ms | 先頭から順にたどる |
Collections.binarySearch |
1.21〜10.45 ms | 並べ替え済みのリストを半分ずつ絞り込む |
| HashSet を作る時間(10万件) | 90.80 ms | 1回だけかかる |
数値は実行するマシンや回によって変わりますが、ArrayList.contains と HashSet.contains には100倍以上の開きがありました。 HashSet を作る時間(今回は約91 ms)を足しても、1万回探すなら HashSet のほうがずっと短く済みます。
ただし、どんな場合でも HashSet にすればよいわけではありません。
| 状況 | 向いているもの |
|---|---|
| 件数が数十〜数百件、調べるのは数回 | List の contains で十分 |
| 件数が多く、ループの中で何度も調べる | HashSet に入れ替えてから contains |
| 位置(何番目か)が必要 | List の indexOf |
| 同じ値を複数持ちたい・順序が大事 | List のまま(検索用に HashSet を別に持つ) |
| すでに並べ替え済みで、位置もほしい | Collections.binarySearch |
入れ替えは new HashSet<>(リスト) の1行で書けます(上の計測コードの変数 set がその例です)。ループに入る前に1回だけ作り、ループの中では HashSet の contains を呼びます。
「ループの中で list.contains を呼んでいる」箇所を見つけたら、HashSet への入れ替えを検討する目安になります。 なお、HashSet を使うときは要素のクラスに hashCode が必要です(前述のとおり、equals だけでは見つかりません)。Collections.binarySearch は、リストが昇順に並んでいることが前提です。
チェックリスト:containsがfalseになるときに確かめること
| 確かめること | 見るポイント |
|---|---|
| 要素のクラスに equals があるか | 表示が クラス名@16進数 なら equals も未定義の可能性 |
| 渡した値の型はリストの要素と同じか | Long と Integer、String と数値 |
| 大文字と小文字、前後の空白は一致しているか | equalsIgnoreCase、strip |
| HashSet で見つからないなら hashCode はあるか | equals と hashCode はセット |
| リストや値が null になっていないか | List.of は null を受け付けない |
| remove に int を渡していないか | 位置で消える。値なら Integer.valueOf |
よくある質問
Q. contains と anyMatch はどちらを使えばよいですか? A. 値そのものが一致するかを調べるなら contains が短く、今回の計測でも anyMatch より短い時間で済みました。前方一致や大文字小文字を無視した比較、一部の項目だけでの検索など、equals 以外の条件で探したいときに anyMatch を使います。
Q. indexOf で見つからないとき、例外になりますか?
A. なりません。-1 が返ります。そのまま list.get(-1) のように使うと範囲外の例外になるため、if (i >= 0) で確かめてから使います。
Q. 文字列のリストで部分一致を調べるには?
A. contains は要素全体が一致するかを見るので、部分一致には使えません。list.stream().anyMatch(s -> s.contains("ava")) のように、ラムダ式の中で String の contains を呼びます。List の contains と String の contains は別のメソッドです。
Q. equals を書けば hashCode は不要ですか? A. List の contains と indexOf だけなら equals で動きますが、HashSet や HashMap に入れたときに見つからなくなります。今回の実行結果でも、equals だけのクラスは List.contains が true、HashSet.contains が false でした。セットで書くか、record を使います。
まとめ
- あるかないかは contains、位置は indexOf と lastIndexOf。見つからないときは false と -1 で、例外にはならない。
- 判定は要素の equals で行われる。equals のない自作クラスは、中身が同じでも false になる。
- 直し方は equals と hashCode の実装、または record。一部の項目で探すなら anyMatch。
- Long のリストに int を渡すと false。型をそろえて
contains(1L)と書く。 - Integer のリストの remove(1) は位置で消える。値で消すなら
remove(Integer.valueOf(1))。 - List.of のリストは contains(null) でも NullPointerException。null になりうる値は先に確かめる。
- 件数が多く何度も探すなら HashSet。今回の計測では ArrayList.contains と100倍以上の開きがあった。
参考・出典
- Java SE 21 API ドキュメント List(contains、indexOf、remove、Unmodifiable Lists): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html
- Java SE 21 API ドキュメント Stream(anyMatch): https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java List contains・indexOfの使い方とequals」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。