要素の削除と検索
このレッスンで分かること
list.remove(int index)は インデックス指定の削除、list.remove(Object o)は値指定Integer引数のときオーバーロード曖昧さがあり、list.remove((Integer) 1)でキャストして区別list.contains(o)で存在確認、list.indexOf(o)で位置取得- 走査中の
removeはConcurrentModificationException。Iterator.removeかremoveIfを使う
要素の削除と検索 とは
ArrayList から要素を消したり、目的のものが入っているかを確かめる方法を学ぼう。本レッスンでは、要素の削除と検索 の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。
ArrayList から要素を消す・探す
ArrayList に値を入れただけでは、List を「使いこなしている」とはまだ言えません。実際のプログラムでは「ユーザーがキャンセルしたタスクを消す」「カート内に既に商品があるか確認する」「重複を排除する」のように、追加した後に 取り除く ・ 探す という操作が必ず登場します。今回はその中心となる remove と contains、そして仲間の 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 であれば中身の文字が同じなら true、Integer であれば値が同じなら 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で扱いたいときは、必ずequalsとhashCodeをオーバーライドしてください。デフォルトの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を返す。空のリストなら0isEmpty()はbooleanを返す。size() == 0と同じ意味だが、こちらの方が意図が伝わる
if (list.size() > 0)よりもif (!list.isEmpty())の方が読みやすい、というのがコードレビューで好まれるスタイルです。
動きを図で追ってみる
ここで、本日の課題と同じ流れを図にしてみましょう。apple banana cherry を追加してから banana を消すと、間が詰まって cherry がインデックス 1 番に繰り上がります。
中央が空いたままにならず、後ろが繰り上がるのが 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を使いConcurrentModificationException─removeIfか Iterator で containsで O(n²) ループ ─ 「リストに含まれているか」を多数チェックするならSetに変換removeAllの挙動を誤解 ─ 引数のコレクションに含まれる 全要素 を削除、Set相手のほうが速い
パフォーマンス考慮事項
remove(int)は O(n) ─ 後ろを詰めるためコピー発生removeIfは内部で一括処理 ─ ループ内 remove より高速containsは O(n) ─ 大量データならHashSetのcontains(O(1)) に置き換えLinkedListのremove(Object)─ 探索 + 削除で結局 O(n)
ここまでの要点
削除は remove(index) / remove(Object) のオーバーロードに注意、走査中は removeIf か Iterator。contains 多用は Set で代替。
やってみよう
右側のエディタで、課題のメソッド summarize を完成させましょう。手順は次のとおりです。
ArrayList<String>を作って、"apple""banana""cherry"を順にaddするlist.remove("banana")で"banana"を取り除くlist.size()とlist.contains("apple")を使って、"size=2,hasApple=true"という文字列を組み立ててreturnする
テストで期待される文字列はぴったり size=2,hasApple=true です。途中の = , の位置、true の小文字、スペースが入っていないことを確認してください。true と True は別物で、Java の boolean を String に連結すると小文字の true / false になります。慣れたら cherry を remove した場合や、contains("banana") の結果も観察してみると、remove と contains の関係がぐっと馴染んできます。
よくある質問
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 つができることを確認しましょう。
- remove/contains の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. remove/contains とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- クラス名は
Solution、メソッド名はsummarize、戻り値の型はString、引数なしで定義すること ArrayList<String>を作って"apple""banana""cherry"をこの順にaddしてから、"banana"をremoveすること- 戻り値は
size=+list.size()+,hasApple=+list.contains("apple")の形で連結したStringにすること
入出力例
test-cases.txt
summarize() → "size=2,hasApple=true"