1. 引言

在 Java 中,使用 List.remove() 移除列表中的某个特定值看似简单直接。但如果你想高效地移除所有匹配的元素,事情就没那么简单了。

本文将带你探索多种实现方案,并分析各自的优缺点。

为了代码简洁,示例中使用了一个自定义的 list(int...) 工具方法,它会返回一个包含传入元素的 ArrayList

2. 使用 while 循环

既然我们知道如何移除单个元素,那用 while 循环反复删除似乎顺理成章:

void removeAll(List<Integer> list, int element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}

但这段代码实际运行会出问题:

// given
List<Integer> list = list(1, 2, 3);
int valueToRemove = 1;

// when
assertThatThrownBy(() -> removeAll(list, valueToRemove))
  .isInstanceOf(IndexOutOfBoundsException.class);

问题出在第三行:list.remove(element) 调用的是 List.remove(int) 方法,它把参数当成索引而不是要删除的值!

上面的例子中,我们实际上一直在调用 list.remove(1),但目标元素的索引其实是 0。每次删除后,后续元素会前移,索引发生变化。

最终只剩第一个元素时,索引 1 就越界了,于是抛出异常。

⚠️ 注意:这个问题只在传入 byteshortcharint 这类基本类型时出现,因为编译器会优先匹配参数为 int 的重载方法(自动类型提升)。

修复方法很简单:把参数类型改为 Integer,强制调用 remove(Object)

void removeAll(List<Integer> list, Integer element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}

现在代码能正常工作了:

// given
List<Integer> list = list(1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

但性能依然堪忧:contains()remove() 都要从头遍历查找元素,做了重复工作。

优化一下,用 indexOf() 记录位置,避免重复查找:

void removeAll(List<Integer> list, Integer element) {
    int index;
    while ((index = list.indexOf(element)) >= 0) {
        list.remove(index);
    }
}

✅ 验证通过,逻辑正确。

但本质问题没变:每次 remove() 仍需移动后续元素,尤其是 ArrayList,频繁删除会导致大量数组拷贝甚至扩容。

3. 利用 remove 的返回值

List.remove(Object) 有个关键特性常被忽略:**删除成功时返回 true,否则 false**。

remove(int index)void,因为只要索引合法就必然成功,否则直接抛异常。

利用这个特性,我们可以简单粗暴地一直删,直到删不动为止:

void removeAll(List<Integer> list, int element) {
    while (list.remove(element));
}

代码短到离谱,效果也正确:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

但底层依然是 remove(Object),性能问题和之前一样——每次都要遍历查找 + 元素移动。图一乐可以,生产环境慎用。

4. 使用 for 循环

for 循环手动控制索引,边遍历边删:

void removeAll(List<Integer> list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        }
    }
}

单个元素能删,但遇到连续重复值就翻车:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(1, 2, 3)); // ❌ 期望是 (2, 3)

原因很简单:删除索引 i 的元素后,后面的元素全部前移,但 i 却自增了,导致下一个元素被跳过。

解决方案

✅ 方案一:删除后索引回退

void removeAll(List<Integer> list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
            i--; // 删除后回退,抵消下次的 i++
        }
    }
}

✅ 方案二:只在不删除时才自增

void removeAll(List<Integer> list, int element) {
    for (int i = 0; i < list.size();) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        } else {
            i++; // 只有不删才前进
        }
    }
}

两种写法都正确,但性能依然拉胯:

  • ArrayList:删除 = 大量元素拷贝
  • LinkedList:按索引访问 = 从头遍历,O(n) 查找

所以这种“手动挡”写法,除非数据量极小,否则不推荐。

5. 使用 Iterator

for-each 循环内部用的是 Iterator,直接操作它才能安全删除:

void removeAll(List<Integer> list, int element) {
    for (Iterator<Integer> i = list.iterator(); i.hasNext();) {
        Integer number = i.next();
        if (Objects.equals(number, element)) {
            i.remove(); // ✅ 安全删除
        }
    }
}

✅ 测试通过,无并发修改异常。

关键点在于:**Iterator 自己触发删除,能同步内部状态**,所以不会崩溃。

而且不同 List 实现(如 ArrayListLinkedList)的 Iterator 通常都做了最优实现,比手动 for 循环更高效。

⚠️ 缺点:代码稍显啰嗦,不如 for-each 直观。

6. 收集法(创建新列表)

换个思路:不删旧的,直接建个新的,只保留想要的元素:

List<Integer> removeAll(List<Integer> list, int element) {
    List<Integer> remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }
    return remainingElements;
}

使用方式稍有不同:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
List<Integer> result = removeAll(list, valueToRemove);

// then
assertThat(result).isEqualTo(list(2, 3));

✅ 优点:

  • ConcurrentModificationException
  • 无元素移动开销,对 ArrayList 友好
  • 代码清晰,逻辑简单

⚠️ 缺点:

  • 返回新 List,原列表不变
  • 返回类型由方法决定,可能和原类型不一致

如果想保持“原地修改”的语义,可以这样改:

void removeAll(List<Integer> list, int element) {
    List<Integer> remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }

    list.clear();
    list.addAll(remainingElements);
}

这样既避免了遍历中的删除问题,又只在 clear()addAll() 时可能触发数组扩容,性能相当不错。

7. 使用 Stream API

Java 8 的 Stream 让代码更优雅:

List<Integer> removeAll(List<Integer> list, int element) {
    return list.stream()
      .filter(e -> !Objects.equals(e, element))
      .collect(Collectors.toList());
}

这本质上还是“收集法”,只是用函数式风格写出来。

✅ 优点:代码极简,可读性强。

⚠️ 注意:返回新列表,且有一定性能开销(创建 Stream 对象等)。适合数据量不大或追求代码简洁的场景。

同样,可以通过 clear() + addAll() 改造成原地修改。

8. 使用 removeIf(终极方案)

Java 8 在 Collection 接口中新增了 removeIf(Predicate),完美解决此问题:

void removeAll(List<Integer> list, int element) {
    list.removeIf(n -> Objects.equals(n, element));
}

✅ 为什么推荐它?

  • 性能最优:由 List 实现类自己优化,比如 ArrayList 会用批量复制,避免逐个移动
  • 语义清晰:直接表达“删除满足条件的元素”
  • 线程安全:内部处理了迭代器状态,无并发修改问题
  • 代码最短:一行搞定,无需手动管理索引或迭代器

测试验证:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;

// when
removeAll(list, valueToRemove);

// then
assertThat(list).isEqualTo(list(2, 3));

✅ 无论是 ArrayList 还是 LinkedListremoveIf 都是目前最推荐的方案。

9. 总结

方法 是否修改原列表 性能 推荐度
while + remove(Object) ❌ 差
for 循环手动索引 ❌ 差
Iterator.remove() ✅ 中 ⭐⭐⭐
收集法(新列表) ✅ 好 ⭐⭐⭐
Stream + filter ✅ 好(小数据) ⭐⭐⭐⭐
removeIf ✅✅ 最优 ⭐⭐⭐⭐⭐

最终建议

  • ✅ **首选 removeIf**:简洁、高效、安全,Java 8+ 项目的标准解法
  • ✅ 数据量小且需新列表:用 Stream
  • ❌ 避免手动 for 循环删除,容易踩坑且性能差

示例代码已托管至 GitHub:https://github.com/baeldung/core-java-modules/tree/master/core-java-collections-list


原始标题:Remove All Occurrences of a Specific Value from a List | Baeldung