list大坑请注意

问题一:Arrays.asList()返回的list不支持增删操作

问题复现

package com.geekmice.onetomany.list;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ListTest {
    
    
    public static void main(String[] args) {
    
    
        t1();
    }

    /**
     * @description 数组转换list
     */
    public static void t1() {
    
    
        ArrayList<Object> objects = new ArrayList<>();
        String[] array = {
    
    "张三", "李四", "王五"};
        List<String> list = Arrays.asList(array);
        // Exception in thread "main" java.lang.UnsupportedOperationException
        list.add("小三");
    }
}

错误提示

Exception in thread “main” java.lang.UnsupportedOperationException

在这里插入图片描述

分析:上面这几行代码,主要是将数组数组转换list,转换后list,进行添加操作提示错误 UnsupportedOperationException,刚开始很不解,Arrays#asList 返回明明也是一个 ArrayList,为什么添加一个元素就会报错?这以后还能好好新增元素吗;后面看了一下java.util.ArrayList和Arrays#asList 内部情况,其实不一样的;
对于Arrays#asList 而言

    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
    
    
        return new ArrayList<>(a);
    }

发现这个Arrays#asList 返回的 ArrayList 其实是个赝品,仅仅只是 Arrays 一个内部类,并非真正的 java.util.ArrayList
在这里插入图片描述
从上图我们发现,add/remove 等方法实际都来自 AbstractList,而 java.util.Arrays$ArrayList 并没有重写父类的方法。而父类方法恰恰都会抛出 UnsupportedOperationException。

    public void add(int index, E element) {
    
    
        throw new UnsupportedOperationException();
    }

真正ArrayList,重写add,delete方法,可以正常使用
在这里插入图片描述

问题二:List,为什么却还互相影响

问题复现

    public static void t2() {
    
    
        String[] array = {
    
    "张三", "李四", "王五"};
        List<String> list = Arrays.asList(array);
        list.set(0, "001");
        array[1] = "002";
        System.out.println("array:" + Arrays.toString(array));
        System.out.println("list:" + list);
    }

效果展示

array:[001, 002, 王五]
list:[001, 002, 王五]

从日志输出可以看到,不管我们是修改原数组,还是新 List 集合,两者都会互相影响。
查看 java.util.Arrays$ArrayList 实现,我们可以发现底层实际使用了原始数组

        @Override
        public E set(int index, E element) {
    
    
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }

知道了实际原因,修复的办法也很简单,套娃一层 ArrayList 呗!

 List<String> list = new ArrayList<>(Arrays.asList(arrays));
    public static void t2() {
    
    
        String[] array = {
    
    "张三", "李四", "王五"};
        List<String> list = new ArrayList<>(Arrays.asList(array));
        list.set(0, "001");
        array[1] = "002";
        System.out.println("array:" + Arrays.toString(array));
        System.out.println("list:" + list);
    }

array:[张三, 002, 王五]
list:[001, 李四, 王五]
不过这么写感觉十分繁琐,推荐使用 Guava Lists 提供的方法。

 List<String> list = Lists.newArrayList(arrays);

引入依赖

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>22.0</version>
        </dependency>

完善后代码

   public static void t2() {
    
    
        String[] array = {
    
    "张三", "李四", "王五"};
        ArrayList<String> list = Lists.newArrayList(array);
        list.set(0, "001");
        array[1] = "002";
        System.out.println("array:" + Arrays.toString(array));
        System.out.println("list:" + list);
    }

最终效果

array:[张三, 002, 王五]
list:[001, 李四, 王五]

问题复现二

除了 Arrays#asList产生新集合与原始数组互相影响之外,JDK 另一个方法 List#subList 生成新集合也会与原始 List 互相影响;

 public static void t2() {
    
    
      
        ArrayList<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);
        // Returns a view of the portion of this list between the specified
        List<Integer> subList = integerList.subList(0, 2);
        subList.set(0, 10);
        integerList.set(1, 20);
        System.out.println("integerList:" + integerList);
        System.out.println("subList:" + subList);
    }

效果展示

integerList:[10, 20, 3]
subList:[10, 20]

查看 List#subList 实现方式,可以发现这个 SubList 内部有一个 parent 字段保存保存最原始 List

   public List<E> subList(int fromIndex, int toIndex) {
    
    
       subListRangeCheck(fromIndex, toIndex, size);
       return new SubList(this, 0, fromIndex, toIndex);
   }

   SubList(AbstractList<E> parent,
           int offset, int fromIndex, int toIndex) {
    
    
       this.parent = parent;
       this.parentOffset = fromIndex;
       this.offset = offset + fromIndex;
       this.size = toIndex - fromIndex;
       this.modCount = ArrayList.this.modCount;
   }

所有外部读写动作看起来是在操作 SubList ,实际上底层动作却都发生在原始 List 中,比如 add 方法

出现OOM问题场景

private static List<List<Integer>> data = new ArrayList<>();

private static void oom() {
    
    
    for (int i = 0; i < 1000; i++) {
    
    
        List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
        data.add(rawList.subList(0, 1));
    }
}

data 看起来最终保存的只是 1000 个具有 1 个元素的 List,不会占用很大空间。但是程序很快就会 OOM。

OOM 的原因正是因为每个 SubList 都强引用个一个 10 万个元素的原始 List,导致 GC 无法回收。

这里修复的办法也很简单,跟上面一样,也来个套娃呗,加一层 ArrayList

问题三:不可变集合,说好不变,你怎么就变了

为了防止 List 集合被误操作,我们可以使用 Collections#unmodifiableList 生成一个不可变(immutable)集合,进行防御性编程。

这个不可变集合只能被读取,不能做任何修改,包括增加,删除,修改,从而保护不可变集合的安全

问题复现

 public static void t3() {
    
    
        ArrayList<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
        List<String> unmodifiableList = Collections.unmodifiableList(list);
        // Exception in thread "main" java.lang.UnsupportedOperationException
        // 看起来没什么问题
        unmodifiableList.add("1");
        unmodifiableList.remove(1);
        unmodifiableList.set(0, "t");
        // 以下进行测试
        // list.set(0, "first_modify");
        // Assertions.assertEquals(list.get(0), unmodifiableList.get(0));
        // list.add("fourth");
        // Assertions.assertEquals(list.get(3), unmodifiableList.get(3));
        // Assertions.assertEquals(list.size(), unmodifiableList.size());
    }

效果展示

在这里插入图片描述
上面单元测试结果将会全部通过,这就代表 Collections#unmodifiableList 产生不可变集合将会被原始 List 所影响。

分析

查看 Collections#unmodifiableList 底层实现

        UnmodifiableList(List<? extends E> list) {
    
    
            super(list);
            // 这里面引入原始list
            this.list = list;
        }

可以看到这跟上面 SubList 其实是同一个问题,新集合底层实际使用了原始 List。

解决方案

使用 JDK9 List#of 方法。

List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = List.of(list.toArray(new String[]{
    
    }));

使用 Guava immutable list

List<String> list = new ArrayList<>(Arrays.asList("one", "two", "three"));
List<String> unmodifiableList = ImmutableList.copyOf(list);

相比而言 Guava 方式比较清爽,使用也比较简单,推荐使用 Guava 这种方式生成不可变集合。

问题四:foreach 增加/删除元素大坑

问题复现

    public static void t4(){
    
    
        String[] array = {
    
    "1","2","3"};
        ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
        for (String s : list) {
    
    
            if("1".equals(s)){
    
    
                // Exception in thread "main" java.util.ConcurrentModificationException
                list.remove(s);
            }

        }
    }

上面代码我们使用foreach方式遍历list集合,如果符合条件,将会从集合中删除该元素;
这个程序编译正常,但是运行时候,程序异常,日志如下

效果展示

Exception in thread “main” java.util.ConcurrentModificationException
at java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 901 ) a t j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList Itr.checkForComodification(ArrayList.java:901)atjava.util.ArrayListItr.next(ArrayList.java:851)
at com.geekmice.onetomany.list.ListTest.t4(ListTest.java:67)
at com.geekmice.onetomany.list.ListTest.main(ListTest.java:14)

在这里插入图片描述

分析

可以看到最终错误是由ArrayList$Itr.next处代码抛出,但是代码中我们并没有调用,为什么呢?
实际上这是foreach方式给java提供语法糖,编译后编程另外一种方式,反编译看一下

    public static void t4() {
    
    
        String[] array = new String[]{
    
    "1", "2", "3"};
        ArrayList<String> list = new ArrayList(Arrays.asList(array));
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
    
    
            String s = (String)var2.next();
            if ("1".equals(s)) {
    
    
                list.remove(s);
            }
        }

    }

可以看到foreach这种方式实际就是迭代器Iterator实现的,这也就是foreach被遍历的类需要实现Iterator接口的原因
在这里插入图片描述
看到modCount 和expectedModCount不同才错误,再看看这两个属性什么含义

        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;
    public boolean remove(Object o) {
    
    
        if (o == null) {
    
    
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
    
    
                    fastRemove(index);
                    return true;
                }
        } else {
    
    
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
    
    
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
        /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
    
    
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

在这里插入图片描述
modCount 来源于 ArrayList 的父类 AbstractList,可以用来记录 List 集合被修改的次数
modCount 计数操作将会交子类自己操作,ArrayList 每次修改操作(增、删)都会使 modCount 加 1

解决方案

使用迭代器删除

String[] array = {
    
    "1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
    
    
	String str = iterator.next();
	if(str.equals("1"){
    
    
		iterator.remove();
	}
}

使用removeIf删除

String[] array = {
    
    "1","2","3"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
list.removeIf(str->str.equals("1"));

问题五:普通for循环删除元素失败

问题复现

描述
目前有个业务,数据库返回list数据,年纪大于19岁删除,只需要小于等于19的数据

相关代码

package com.geekmice.onetomany.list;

import com.google.common.collect.Lists;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@SpringBootTest
public class ListTest {
    
    
     @Test
    public void t6() {
    
    
        List<Student> list = new ArrayList<Student>() {
    
    {
    
    
            add(new Student("豹子头", 18));
            add(new Student("鲁提辖", 20));
            add(new Student("赖老三", 29));
        }};
        for (int i = 0; i < list.size(); i++) {
    
    
            if (list.get(i).getAge() > 19) {
    
    
                list.remove(i);
            }
        }
        System.out.println(list);
        // [Student{userName='豹子头', age=18}, Student{userName='赖老三', age=29}]

    }
}

在这里插入图片描述

问题分析

奇怪的是,明明条件是年龄大于19删除对应数据,'鲁提辖’这条数据却没有被清除;
删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用

public E remove(int index) {
    
    
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

解决方案

普通for循环改善

顺序循环时,删除当前位置的值,下一个值就会补到当前位置,所以需要执行i–操作

        List<Student> list = new ArrayList<Student>() {
    
    {
    
    
            add(new Student("豹子头", 18));
            add(new Student("鲁提辖", 20));
            add(new Student("赖老三", 29));
        }};
        for (int i = 0; i < list.size(); i++) {
    
    
            if (list.get(i).getAge() > 19) {
    
    
                list.remove(i);
                i--; // 关键代码
            }
        }
        System.out.println(list);

迭代器遍历删除

注意必须用迭代器的remove()方法,不要用list的remove,不然会发生java.util.ConcurrentModificationException 异常

        System.out.println("迭代器遍历删除开始");
        Iterator<Student> studentIterator = list.iterator();
        while (studentIterator.hasNext()) {
    
    
            if (studentIterator.next().getAge() > 19) {
    
    
                studentIterator.remove();
            }
        }
        System.out.println("迭代器遍历删除结束");

倒序遍历删除

因为list删除只会导致当前元素之后的元素位置发生改变,所以采用倒序可以保证前面的元素没有变化

List<Student> list = new ArrayList<Student>() {
    
    {
    
    
    add(new Student("豹子头", 18));
    add(new Student("鲁提辖", 20));
    add(new Student("赖老三", 29));
}};
for (int i = list.size() - 1; i >= 0; i--) {
    
    
	if (list.get(i).getAge() > 19) {
    
    
	  list.remove(i);
	}
}
System.out.println(list);

stream流处理

List<Student> list = new ArrayList<Student>() {
    
    {
    
    
    add(new Student("豹子头", 18));
    add(new Student("鲁提辖", 20));
    add(new Student("赖老三", 29));
}};
list.removeIf(item -> item.getAge() > 19);

总结

第一、Arrays.asList和List.subList就是一个普通独立的ArrayList

如果没有办法,使用了Arrays.asList和List.sublist,返回给其他方法时候,一定要嵌套真正的ArrayList

第二、jdk提供的不可变集合非常笨重,低效,不安全,推荐使用guava不可变集合
第三、不要随便在foreach增加/删除元素
第四、ArrayList是从新复制一个数组返回的,所以从后往前删除才删的干净。
参考博文
1、List如何在遍历时删除元素

猜你喜欢

转载自blog.csdn.net/greek7777/article/details/126334416