遍历移除List中符合条件的元素的解决方案

时间:2024-03-25 15:12:28
代码只有经过多线程、效率和安全的考研,才算是优秀的代码

JDK环境:JDK8
遍历移除List中符合条件的元素
可能产生问题:
1、删除元素后List的元素数量会发生变化,随之索引也会发生变化
2、对List进行删除操作可能会产生并发问题,遍历List的时候不允许并发操作本次需求:
从已有的List列表中移除typeId是3和4的对象
准备:
/**
* 遍历移除List中符合条件的元素
* 可能产生问题:
* 1、删除元素后List的元素数量会发生变化,随之索引也会发生变化
* 2、对List进行删除操作可能会产生并发问题,遍历List的时候不允许并发操作
*
* 本次需求:
* 从已有的List列表中移除typeId是3和4的对象
*
* @author hongwei.lian
* @date 2018年3月28日 下午3:37:30
*/
public class RemoveListTest {
/**
* 存储对象的列表
*/
private List<SystypeBaseVo> list;
/**
* 存储去除元素列表
*/
private List<SystypeBaseVo> removeList;
/**
* 存储去除后的元素列表
*/
private List<SystypeBaseVo> removedList;
/**
* 初始化列表
*
* @author hongwei.lian
* @date 2018年3月28日 下午3:40:01
*/
@Before
public void initList() {
list = new ArrayList<>();
list.add(new SystypeBaseVo(1, 8, 1, "次"));
list.add(new SystypeBaseVo(2, 8, 2, "年化"));
list.add(new SystypeBaseVo(3, 8, 3, "每期"));
list.add(new SystypeBaseVo(4, 8, 4, "其它"));
}
}
使用普通for循环遍历:
/**
* 使用普通for循环遍历,没有抛出异常,但结果与预期不符合
* 使用ArrayList实现List接口的public E remove(int index)方法
*
* @author hongwei.lian
* @date 2018年4月1日 下午6:15:48
*/
@Test
public void testListRemove1() {
for (int i = 0; i < list.size(); i++) {
//-- 等价于list.get(i).getTypeId() == 3 || list.get(i).getTypeId() == 4
if (list.get(i).getTypeId() >= 3) {
list.remove(i);
}
}
list.forEach(System.out::println);
}
结果:typeId=4的对象并没有移除,也没有抛出异常
遍历移除List中符合条件的元素的解决方案另一个重载的remove()方法
/**
* 使用普通for循环遍历,没有抛出异常,但结果与预期不符合
* 使用ArrayList实现List接口的public boolean remove(Object o)方法
*
* @author hongwei.lian
* @date 2018年4月1日 下午7:15:03
*/
@Test
public void testListRemove12() {
for (int i = 0; i < list.size(); i++) {
SystypeBaseVo vo = list.get(i);
//-- 等价于vo.getTypeId() == 3 || vo.getTypeId() == 4
if (vo.getTypeId() >= 3) {
list.remove(vo);
}
}
list.forEach(System.out::println);
}
与上述得出的结果是相同的,那么为什么得出这样的结果呢,下面的remove(Object o)方法通过看源码可知是因为再次通过一次循环,得出元素的索引值,然后根据索引值删除元素,这就和上面的remove(int index)的出现的结果都是一致的,都是由于列表的大小和列表的索引值引起的与预期结果不符合。删除列表中的元素都是通过数组拷贝进行的,当列表大小和索引值发生变化后,我们使用的判断条件将不再符合预期结果,因为预期结果是在列表数量固定的情况下进行判断的,那么先前的条件将不再符合变化的列表大小和变化的索引值,也就会造成结果不符合预期。
使用增强for循环遍历:
JDK8之前提供的增强for循环:
/**
* 使用增强for循环遍历,与上述结果一致,依然与预期结果不符合
*
* @author hongwei.lian
* @date 2018年4月1日 下午7:56:54
*/
@Test
public void testListRemove2() {
for (SystypeBaseVo vo : list) {
if(vo.getTypeId() >= 3) {
list.remove(vo);
}
}
list.forEach(System.out::println);
}
使用JDK8提供的增强for循环遍历:
/**
* 使用JDK8提供的增强for循环遍历,直接抛出ConcurrentModificationException异常
*
* @author hongwei.lian
* @date 2018年3月28日 下午2:30:42
*/
@Test
public void testListRemove2() {
list.forEach(vo -> {
if(vo.getTypeId() >= 3) {
list.remove(vo);
}
});
list.forEach(System.out::println);
}
结果:直接抛出ConcurrentModificationException异常,并发修改异常
遍历移除List中符合条件的元素的解决方案
JDK8之前提供的增强for循环不再赘述,来看为什么JDK8提供的forEach(Consumer<? super T> action)方法,这个方法定义在Iterable接口中,在ArrayList类中对该方法进行了重写,可以看源码就可以知道为什么抛出这个并发修改异常。
使用迭代器遍历:
/**
* 使用迭代器遍历
* ArrayList类中的内部类Itr重写了Iterator接口中的remove()方法
* 因此使用迭代器遍历列表中的元素的同时可以移除其中的元素,不会抛出异常
*
* @author hongwei.lian
* @date 2018年3月28日 下午2:30:48
*/
@Test
public void testListRemove3() {
Iterator<SystypeBaseVo> it = list.iterator();
while(it.hasNext()) {
if(it.next().getTypeId() >= 3)
it.remove();
}
list.forEach(System.out::println);
}
结果:遍历过程中移除=符合条件的元素,得到符合预期的结果,在注释中已经给出了解释,在Iterator接口中定义的remove()是抛出UnsupportedOperationException异常的,在ArrayList类重写后的迭代器是private class Itr implements Iterator<E>中的remove()方法,具体可以查看源码。
遍历移除List中符合条件的元素的解决方案
使用List接口的removeAll()方法
/**
* 使用List接口的removeAll()方法
*
* @author hongwei.lian
* @date 2018年3月28日 下午3:22:53
*/
@Test
public void testListRemove4() {
removeList = new ArrayList<>();
list.forEach(vo -> {
if(vo.getTypeId() >= 3) {
removeList.add(vo);
}
});
list.removeAll(removeList);
list.forEach(System.out::println);
}
结果:符合预期,但是不可避免地多创建了额外的对象
遍历移除List中符合条件的元素的解决方案
使用JDK8提供的Predicate判断接口
/**
* 使用JDK8提供的Predicate判断接口
* or()方法和negate()方法和test()方法
*
* @author hongwei.lian
* @date 2018年4月1日 下午6:31:20
*/
@Test
public void testListRemove5() {
Predicate<SystypeBaseVo> filter = vo -> vo.getTypeId() == 3;
removedList = list.stream()
.filter(filter
.or(vo -> vo.getTypeId() == 4)
.negate())
.collect(Collectors.toList());
removedList.forEach(System.out::println);
}
/**
* 使用JDK8提供的Predicate判断接口
* and()方法和test()方法
*
* @author hongwei.lian
* @date 2018年4月1日 下午8:22:07
*/
@Test
public void testListRemove51() {
Predicate<SystypeBaseVo> filter = vo -> vo.getTypeId() != 3;
removedList = list.stream()
.filter(filter
.and(vo -> vo.getTypeId() != 4))
.collect(Collectors.toList());
removedList.forEach(System.out::println);
}

/**
* 使用JDK8提供的Predicate判断接口
* test()方法
*
* @author hongwei.lian
* @date 2018年4月1日 下午8:22:40
*/
@Test
public void testListRemove52() {
removedList = list.stream()
.filter(vo -> vo.getTypeId() < 3)
.collect(Collectors.toList());
removedList.forEach(System.out::println);
}
结果:符合预期结果,相应的疑惑可以查看Strean接口的filter()方法源码和Predicate接口源码
遍历移除List中符合条件的元素的解决方案
使用JDK8提供的Collection接口中的removeIf()方法
/**
* 使用JDK8提供的Collection接口中的removeIf()方法
* 看源码可知使用的是迭代器遍历移除的方式,只是做了简单封装
*
* @author hongwei.lian
* @date 2018年3月28日 下午3:24:57
*/
@Test
public void testListRemove6() {
//-- 符合条件的从列表中移除
list.removeIf(vo -> vo.getTypeId() >= 3);
list.forEach(System.out::println);
}
结果:
遍历移除List中符合条件的元素的解决方案
从上面的结果可以看出JDK8提供了安全方便的接口和API,我们可以拥抱JDK8带来的新特性,可以相应的减少不必要的异常和与预期不符合的结果。

附上JDK8的Collection接口中的removeIf(Predicate<? super E> filter)方法源码:
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
这个基本上代表了正确安全的去除List集合符合条件的元素的方法,使用迭代器遍历移除,使用Predicate判断接口的test()方法。