• 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("小三");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    错误提示

    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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    发现这个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();
        }
    
    • 1
    • 2
    • 3

    真正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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    效果展示

    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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

     List<String> list = new ArrayList<>(Arrays.asList(arrays));
    
    • 1
        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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    引入依赖

            <dependency>
                <groupId>com.google.guavagroupId>
                <artifactId>guavaartifactId>
                <version>22.0version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    完善后代码

       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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最终效果

    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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    效果展示

    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;
       }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    所有外部读写动作看起来是在操作 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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    效果展示

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

    分析

    查看 Collections#unmodifiableList 底层实现

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

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

    解决方案

    使用 JDK9 List#of 方法。

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

    使用 Guava immutable list

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

    相比而言 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);
                }
    
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面代码我们使用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);
                }
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    可以看到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;
    
    • 1
    • 2
    • 3
        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
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述
    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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    使用removeIf删除

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

    总结

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

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

    第二、jdk提供的不可变集合非常笨重,低效,不安全,推荐使用guava不可变集合
    第三、不要随便在foreach增加/删除元素

  • 相关阅读:
    Java中使用Jsoup实现网页内容爬取与Html内容解析并使用EasyExcel实现导出为Excel文件
    C++实现基于区块链的物流信息存储系统
    把Eclipse创建的Web项目(非Maven)导入Idea
    使用synchronized 加锁你加对了么?
    C++11标准模板(STL)- 算法(std::lower_bound)
    1720. 解码异或后的数组
    pyinstaller打包python/fastapi项目为exe
    QEMU模拟ATF启动
    服务器证书是网络信息安全的基础
    操作配置文件保存方式(上位机)
  • 原文地址:https://blog.csdn.net/greek7777/article/details/126334416