要素の削除と検索

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • list.remove(int index)インデックス指定の削除list.remove(Object o) は値指定
  • Integer 引数のときオーバーロード曖昧さがあり、list.remove((Integer) 1) でキャストして区別
  • list.contains(o) で存在確認、list.indexOf(o) で位置取得
  • 走査中の removeConcurrentModificationExceptionIterator.removeremoveIf を使う

要素の削除と検索 とは

ArrayList から要素を消したり、目的のものが入っているかを確かめる方法を学ぼう。本レッスンでは、要素の削除と検索 の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

ArrayList から要素を消す・探す

ArrayList に値を入れただけでは、List を「使いこなしている」とはまだ言えません。実際のプログラムでは「ユーザーがキャンセルしたタスクを消す」「カート内に既に商品があるか確認する」「重複を排除する」のように、追加した後に 取り除く探す という操作が必ず登場します。今回はその中心となる removecontains、そして仲間の indexOf lastIndexOf size isEmpty を一気に整理します。

どのメソッドも java.util.List インタフェースに定義されているので、ArrayList に限らず LinkedList でも同じ感覚で使えます。Java のコレクションは「インタフェースで操作、実装はあとから差し替え可」という設計になっていて、最初に身につけておくと一生使えます。

remove の 2 つの顔

まず一番つまずきやすいのが remove です。ArrayList<String> に対して次の 2 行はどちらもコンパイルが通ります。

Java

list.remove(1); // index = 1 の要素を消す list.remove("banana"); // "banana" という要素を消す

見た目はほぼ同じですが、呼ばれるメソッドが違います。前者は remove(int index)、後者は remove(Object o) で、Java は引数の型を見て自動で振り分けます。int リテラルを渡せば前者、String を渡せば後者です。

厄介なのは List<Integer> のときです。list.remove(1) と書くと インデックス 1 番 が消えてしまい、「整数 1 を消したい」と思って書いたコードがバグります。整数を値として消したいときは list.remove(Integer.valueOf(1)) のように Integer 型に明示的に包む必要があります。

戻り値も少し違います。remove(int) は消した要素 (String など) を返し、remove(Object) は本当に消えたかを boolean で返します。たとえばリストに無いものを消そうとすると後者は false を返すだけで、エラーにはなりません。

contains の挙動

contains(Object o) は「リストに同じ要素があるか」を boolean で返します。比較の物差しは == ではなく、各要素の equals メソッドです。String であれば中身の文字が同じなら trueInteger であれば値が同じなら true になります。

Java

ArrayList<String> fruits = new ArrayList<>(); fruits.add("apple"); fruits.add("banana"); boolean a = fruits.contains("apple"); // true boolean c = fruits.contains("cherry"); // false

自作クラスを contains で扱いたいときは、必ず equalshashCode をオーバーライドしてください。デフォルトの Object.equals は「同じインスタンスか」しか見ないので、別のインスタンスは全部 false 扱いになります。

indexOf と lastIndexOf

「あるか/ないか」だけでなく「何番目にあるか」を知りたいときは indexOf を使います。先頭から探して最初に見つかった位置の int を返し、見つからなければ -1 を返します。最後尾から探したいときは lastIndexOf です。

Java

ArrayList<String> log = new ArrayList<>(); log.add("open"); log.add("edit"); log.add("save"); log.add("edit"); int first = log.indexOf("edit"); // 1 int last = log.lastIndexOf("edit"); // 3 int none = log.indexOf("delete"); // -1

この -1 というセンチネル値は、String.indexOf でも Arrays.asList(...).indexOf でも共通の Java の作法です。if (idx >= 0) で「見つかったとき」を判定するクセを付けておくと、コードが安全になります。

size と isEmpty

要素数は size()、空かどうかは isEmpty() で確認します。

  • size()int を返す。空のリストなら 0
  • isEmpty()boolean を返す。size() == 0 と同じ意味だが、こちらの方が意図が伝わる

if (list.size() > 0) よりも if (!list.isEmpty()) の方が読みやすい、というのがコードレビューで好まれるスタイルです。

動きを図で追ってみる

ここで、本日の課題と同じ流れを図にしてみましょう。apple banana cherry を追加してから banana を消すと、間が詰まって cherry がインデックス 1 番に繰り上がります。

diagram (will load when visible)

中央が空いたままにならず、後ろが繰り上がるのが ArrayList の重要な特徴です。たくさん削除する処理を内側で繰り返すと、毎回後ろの要素をシフトするコストがかかります。先頭/末尾の削除が多い用途なら LinkedList を検討する、というのが定番の判断ポイントです。

Before / After で見るコード例

本日学んだメソッドを組み合わせると、ちょっとした「重複チェック付き追加」が次のように書けます。

Before (重複もそのまま追加してしまう例) は以下のとおりです。

Java

ArrayList<String> tags = new ArrayList<>(); tags.add("java"); tags.add("java"); // tags = [java, java] になってしまう

After (contains で重複を防ぐ例) は以下のとおりです。

Java

ArrayList<String> tags = new ArrayList<>(); String input = "java"; if (!tags.contains(input)) { tags.add(input); } if (!tags.contains(input)) { tags.add(input); } // tags = [java] のままで重複なし

重複を完全に避けたいだけなら、本来は HashSet を使う方がパフォーマンスも意味も明確です。今は ArrayList の理解が目的なので contains を使っていますが、次の章で Set を学ぶときに思い出してみてください。

よくある間違い

初学者がほぼ全員ハマるポイントを 3 つ並べておきます。

  • remove(int)remove(Object) の混同 — 特に List<Integer> で頻発します。位置で消すのか、値で消すのかを意識し、値で消したい場合は Integer.valueOf(...) で包みましょう
  • null を渡したときの挙動contains(null)remove(null) は実行時例外にはならず、null を要素として比較します。null を入れないリストを作りたいなら、追加前に if (value != null) でガードします
  • 拡張 for 文の中で remove を呼ぶfor (String s : list) { list.remove(s); } のように書くと ConcurrentModificationException で落ちます。走査中に消したいときは Iterator.remove を使うか、list.removeIf(s -> 条件) のような書き方に切り替えてください

もうひとつ、remove(Object) は「最初に一致したもの 1 つだけ」を消します。同じ要素が複数入っているなら 1 回呼んだだけでは全部消えません。全部消したいときは removeIf(e -> e.equals("banana")) を使うのが定石です。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • カート機能 ─ 「商品を削除する」ボタンで cart.remove(item)
  • TODO リスト ─ チェックを付けたタスクを removeIf(t -> t.done) で一括削除
  • 重複排除list.indexOf で重複を見つけて削除する素朴な方法 (Set のほうが効率はいい)
  • ページから削除 ─ ページネーション結果から削除済み件をフィルタする

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • remove(int)remove(Integer) の混同list.remove(1) がどちらか曖昧。明示的にキャスト
  • ループ内で remove を使い ConcurrentModificationExceptionremoveIf か Iterator で
  • contains で O(n²) ループ ─ 「リストに含まれているか」を多数チェックするなら Set に変換
  • removeAll の挙動を誤解 ─ 引数のコレクションに含まれる 全要素 を削除、Set 相手のほうが速い

パフォーマンス考慮事項

  • remove(int) は O(n) ─ 後ろを詰めるためコピー発生
  • removeIf は内部で一括処理 ─ ループ内 remove より高速
  • contains は O(n) ─ 大量データなら HashSetcontains (O(1)) に置き換え
  • LinkedListremove(Object) ─ 探索 + 削除で結局 O(n)
この章のポイント

ここまでの要点 削除は remove(index) / remove(Object) のオーバーロードに注意、走査中は removeIf か Iterator。contains 多用は Set で代替。

やってみよう

右側のエディタで、課題のメソッド summarize を完成させましょう。手順は次のとおりです。

  1. ArrayList<String> を作って、"apple" "banana" "cherry" を順に add する
  2. list.remove("banana")"banana" を取り除く
  3. list.size()list.contains("apple") を使って、"size=2,hasApple=true" という文字列を組み立てて return する

テストで期待される文字列はぴったり size=2,hasApple=true です。途中の = , の位置、true の小文字、スペースが入っていないことを確認してください。trueTrue は別物で、Java の booleanString に連結すると小文字の true / false になります。慣れたら cherryremove した場合や、contains("banana") の結果も観察してみると、removecontains の関係がぐっと馴染んできます。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は HashMap の基本 で、ArrayList から要素を消したり、目的のものが入っているかを確かめる方法を学ぼう を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. remove/contains の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. remove/contains とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. クラス名は Solution、メソッド名は summarize、戻り値の型は String、引数なしで定義すること
  2. ArrayList<String> を作って "apple" "banana" "cherry" をこの順に add してから、"banana"remove すること
  3. 戻り値は size= + list.size() + ,hasApple= + list.contains("apple") の形で連結した String にすること

入出力例

test-cases.txt

summarize()"size=2,hasApple=true"

ヒント

main.java
main.java
学習モード

メモ

要素の削除と検索

⌘S で保存