Integer型のListでremove(1)が値ではなくインデックスを削除する注意点を解説する記事のアイキャッチ。

Java

JavaのList.remove(1)で値ではなく添字が消える理由|Integerを削除する方法

AIによる要約

Listにはremove(int index)とremove(Object value)があります。List<Integer>にremove(1)と書くと、引数1はintなのでremove(int index)が選ばれ、値1ではなくインデックス1の要素が削除されます。値1を消すならremove(Integer.valueOf(1))、条件で複数件を消すならremoveIfを使います。値と添字を混同しないよう、変数名とテストデータでも意図を明示します。

新人SE
新人SE
`List<Integer>`から1を消したつもりなのに、2が消えました。Javaのバグですか?
ポンコツSE
ポンコツSE
removeには同名の2メソッドがあります。数値リテラルの1はintなので、値ではなくインデックスとして扱われます。

注文IDや選択済みコードを`List<Integer>`で扱うと、`remove(1)`が「値1の削除」に見えます。しかしJavaは引数の型から`remove(int index)`を選ぶため、2番目の要素を削除します。

結果がたまたま期待どおりになるデータもあり、レビューや単体テストをすり抜けやすい点が問題です。オーバーロードの仕組みと、値削除を明示する書き方を押さえます。

この記事のポイント

  • remove(1)の1はintなので添字として扱われる
  • 値を消すならInteger.valueOfでObject側を選ぶ
  • remove(Object)は最初に一致した1件だけを削除する
  • 条件一致を全件消すならremoveIfを使う
  • ループ中の削除方法と変更不可Listにも注意する

XのJavaクイズで結果を確認する

List<Integer> ids =
        new ArrayList<>(List.of(1, 2, 3));

ids.remove(1);
System.out.println(ids);

出力は`[1, 3]`です。インデックス1にある値2が削除されます。

Xの出題ポスト回答ポストも参照できます。

List.removeには2つのオーバーロードがある

E remove(int index);
boolean remove(Object value);

`remove(int index)`は指定位置の要素を削除し、削除した要素を返します。`remove(Object value)`は最初に一致した値を削除し、削除できたかをbooleanで返します。

`ids.remove(1)`の1はintリテラルなので、引数型がそのまま一致する`remove(int index)`が選ばれます。Listの要素型がIntegerでも、「IntegerのListだから値削除になる」とは判断されません。

値1を削除する正しい書き方

List<Integer> ids =
        new ArrayList<>(List.of(1, 2, 3));
boolean removed = ids.remove(Integer.valueOf(1));

System.out.println(ids);     // [2, 3]
System.out.println(removed); // true

`Integer.valueOf(1)`でObjectとして渡すと`remove(Object)`が選ばれます。値が存在しなければListは変わらず、戻り値はfalseです。

Integer targetId = 1;
boolean removed = ids.remove(targetId);

Integer型の変数を渡しても値削除になります。ただし途中で`int targetId`へ変更されると意味が変わるため、値削除であることを明確にしたい箇所では`Integer.valueOf`が安全です。

remove(Object)は最初の1件だけを削除する

List<Integer> ids =
        new ArrayList<>(List.of(1, 2, 1, 3));

ids.remove(Integer.valueOf(1));

System.out.println(ids); // [2, 1, 3]

同じ値が複数あっても、remove(Object)が消すのは最初に一致した1件です。値1をすべて削除するなら`removeIf`を使います。

ids.removeIf(id -> id.equals(1));

nullを含む可能性があるListでは`id.equals(1)`がNullPointerExceptionになるため、`Objects.equals(id, 1)`を使えます。

ids.removeIf(id -> Objects.equals(id, 1));

添字削除では範囲外にも注意する

List<Integer> ids = new ArrayList<>(List.of(10));
ids.remove(1); // IndexOutOfBoundsException

値1がなくてもremove(Object)ならfalseですが、インデックス1がなければ`IndexOutOfBoundsException`です。空の一覧や1件だけの一覧で落ちるため、件数が多い正常データだけでは見つかりません。

拡張for文の中で直接removeしない

for (Integer id : ids) {
    if (id < 0) {
        ids.remove(id);
    }
}

走査中のListを直接変更すると`ConcurrentModificationException`になる可能性があります。条件削除ならremoveIfが最も分かりやすい書き方です。

ids.removeIf(id -> id < 0);

削除しながら別処理も行う必要がある場合はIteratorの`remove`を使いますが、まず処理を分けられないか検討します。1つのループに検証、削除、集計を詰め込むとレビューしづらくなります。

変更不可Listではどちらのremoveも失敗する

List<Integer> ids = List.of(1, 2, 3);
ids.remove(Integer.valueOf(1));

`List.of`の戻り値は変更不可なので、値削除を正しく指定しても`UnsupportedOperationException`です。削除が必要なら可変Listへコピーします。

List<Integer> mutableIds =
        new ArrayList<>(List.of(1, 2, 3));
mutableIds.remove(Integer.valueOf(1));

現場ではIDと位置を変数名で分ける

int selectedIndex = 1;
selectedIds.remove(selectedIndex);

Integer selectedId = 1;
selectedIds.remove(selectedId);

`index`と`id`を名前に含めるだけでも意図が伝わります。画面の行番号とDBのIDを同じintで持つ実装では、削除対象を取り違えやすいため、FormやDTOでも役割を分けます。

境界値を含むテストを書く

@Test
void 値1だけを削除する() {
    List<Integer> ids =
            new ArrayList<>(List.of(1, 2, 3));

    boolean removed =
            ids.remove(Integer.valueOf(1));

    assertTrue(removed);
    assertEquals(List.of(2, 3), ids);
}

@Test
void 存在しない値ならfalseを返す() {
    List<Integer> ids =
            new ArrayList<>(List.of(1, 2, 3));

    assertFalse(ids.remove(Integer.valueOf(9)));
    assertEquals(List.of(1, 2, 3), ids);
}

@Test
void 範囲外の添字は例外になる() {
    List<Integer> ids =
            new ArrayList<>(List.of(1));

    assertThrows(
            IndexOutOfBoundsException.class,
            () -> ids.remove(1));
}

`[1, 2, 3]`だけでなく、1件、空、重複、対象なしを確認します。値1とインデックス1が同時に存在するデータを使うと、取り違えを検出しやすくなります。

現場レビューでよくある指摘

// レビューコメント例
List<Integer>のremove(1)はインデックス削除になります。
値1を削除する意図ならInteger.valueOf(1)を渡してください。

// レビューコメント例
同じIDが複数ある場合、remove(Object)は最初の1件だけです。
全件削除が要件ならremoveIfを使ってください。

// レビューコメント例
拡張for中に元Listを変更しているため、
removeIfかIteratorを使う形へ変更してください。

提出前のセルフチェック

  • 削除対象は値か添字か明確か
  • 引数のコンパイル時型を確認したか
  • 値削除ならInteger.valueOfを使ったか
  • 最初の1件だけでよいか
  • 全件削除ならremoveIfを検討したか
  • 走査中に元Listを変更していないか
  • List自体が変更可能か確認したか
  • 空・1件・重複・対象なしをテストしたか

Javaのオーバーロードを整理する参考書

コレクションAPIだけでなく、プリミティブ型、ラッパー型、オーバーロードの選択規則を基礎から確認すると、似た取り違えを防げます。

書籍「スッキリわかるJava入門 第5版」の表紙
商品画像:Amazon.co.jp
PR Java初学者向け

スッキリわかるJava入門 第5版

中山 清喬 (著), 国本 大悟 (著), 株式会社フレアリンク (監修)

Java文法とオブジェクト指向を、最初から整理する一冊。

豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。

  • Javaを基礎から体系的に学びたい
  • Java 25 LTS対応の入門書を選びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

List<Integer>のremove(1)は、int引数に一致するremove(int index)が選ばれるため、値1ではなくインデックス1の要素を削除します。

値を削除するならInteger.valueOf、条件一致を全件削除するならremoveIfを使います。値と添字が紛らわしいコードでは変数名と境界値テストで意図を固定し、変更不可Listやループ中の削除にも注意しましょう。

-Java
-, , ,