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
就越界了,于是抛出异常。
⚠️ 注意:这个问题只在传入 byte
、short
、char
或 int
这类基本类型时出现,因为编译器会优先匹配参数为 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
实现(如 ArrayList
和 LinkedList
)的 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
还是 LinkedList
,removeIf
都是目前最推荐的方案。
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