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期連続の黒字決算。

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

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