飞道的博客

【Java基础提升】List集合使用细节

346人阅读  评论(0)

是List里面还有很多不为人知的坑,下面就来总结下常见的一些坑

一.Arrays.asList()

  1. 坑1:使用Arrays.asList转换后的对象进行add/remove
    @Test
    public void asListToAddRemove() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = Arrays.asList(arrays);
        list.add("4");
        //java.utilArrays$ArrayList (ArrayList是java.util.Arrays的内部类)
    }

Arrays.asList将一个数组转化为List然后再添加一个元素,会抛出如下异常:

原因: 通过源码可以看出 Arrays.aslist得到的不是java.util.ArrayList而是一个Arrays一个内部类java.utilArrays$ArrayList

Debug结果:

​ addremove方法实际都来自父类AbstractListjava.util.Arrays$ArrayList并没有重写父类的方法,而会抛出UnsupportedOperationException。这也是为什么不支持增删的原因。

  1. 坑2:使用Arrays.asList转换后的对象和原对象进行操作
    @Test
    public void asListReference() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = Arrays.asList(arrays);

        list.set(0, "a");
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list));
        //arrays=["a","2","3"],list=["a","2","3"]
        arrays[2] = "c";
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list));
        ////arrays=["a","2","c"],list=["a","2","c"]
        //可以看到,不管是修改原数组还是新的list集合两者都会互相影响。因为这个方法实现的时候使用了原始的数组

    }

发现: Arrays.asList转换后的对象,不管是修改原数组还是新的list集合两者都会互相影响。

原因: java.util.Arrays$ArrayList内部类仅仅保存的是原数组的内存地址

解决办法:

        String[] arrays = {"1", "2", "3"};
        List<String> list = Arrays.asList(arrays);

        list.set(0, "a");
        arrays[2] = "c";

//解决办法1: 使用java.util.ArrayList存放Arrays.asList转换后的对象
        List<String> newList = new ArrayList<>(Arrays.asList(arrays));
        newList.add("newList");
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list) + ",newList=" + JSONObject.toJSONString(newList));
        //arrays=["a","2","c"],list=["a","2","c"],newList=["a","2","c","new"]

//解决办法2:谷歌提供的Guava  Lists.newArrayList方法
        List<String> newList2 = Lists.newArrayList(newList);
        newList2.add("newList2");
        System.out.println("arrays=" + JSONObject.toJSONString(arrays) + ",list=" + JSONObject.toJSONString(list) + ",newList1=" + JSONObject.toJSONString(newList) + ",newList2=" + JSONObject.toJSONString(newList2));
        //arrays=["a","2","c"],list=["a","2","c"],newList1=["a","2","c","newList"],newList2=["a","2","c","newList","newList2"]

二.foreach循环删除元素

foreach 增加/删除元素大坑

使用foreach删除值为1的元素

    @Test
    public void listForeachAddAndRemoveException() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = new ArrayList<String>(Arrays.asList(arrays));
        for (String str : list) {
            if (str.equals("1")) {
                list.remove(str);
            }
        }
    }

执行结果:

原因:​ 可以看到最终错误是在java.util.ArrayList$Itr.next处抛出,但我们并没有调用该方法,实际上foreach这种写法是一种语法糖,其底层还是使用Iterator迭代器实现方法。

反编译结果

解决办法:

  1. 使用Iteratorremove方法删除元素
    @Test
    public void listForeachAddAndRemoveExceptionByIterator() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = new ArrayList<String>(Arrays.asList(arrays));
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String str = it.next();
            if (str.equals("1")) {
                it.remove();
            }
        }

        System.out.println(list);//[2, 3]
    }
  1. 使用Java8removeIf方法
    @Test
    public void listForeachAddAndRemoveExceptionByRemoveIf() {
        String[] arrays = {"1", "2", "3"};
        List<String> list = new ArrayList<String>(Arrays.asList(arrays));
        list.removeIf(str -> str.equals("1"));
        System.out.println(list);//[2, 3]
    }

removeIf的底层还是使用Iterator

三. ArrayList.subList()

1.举例说明

使用subList()、subMap()、subSet() 可以 List、Map、Set 进行分割处理,但这些方法都存在一些坑,下面我们以subList为例进行讲解:

subList: 是List接口中定义的一个方法,该方法主要用于返回一个集合中的一段、可以理解为截取一个集合中的部分元素,他的返回值也是一个List。

    @Test
    public void testSubList() {
        List<Integer> oriList = new ArrayList<>();
        oriList.add(1);
        oriList.add(2);

        // 通过构造函数新建一个包含oriList 的集合 arrayList 
        List<Integer> arrayList = new ArrayList<>(oriList);

        // 通过subList方法生成一个与oriList一样的集合 subList
        List<Integer> subList = oriList.subList(0, oriList.size());

        //操作截取后生成的子集合
        subList.add(3);


        System.out.println("oriList == arrayList:" + oriList.equals(arrayList) + "----oriList=" + oriList + ",arrayList=" + arrayList);
        //oriList == arrayList:false----oriList=[1, 2, 3],arrayList=[1, 2]
        System.out.println("oriList == subList:" + oriList.equals(subList) + "----oriList=" + oriList + ",subList=" + subList);//true
        //oriList == subList:true----oriList=[1, 2, 3],subList=[1, 2, 3]
    }

上面是通过构造函数包含原始集合或者截取原始集合重新生成一个原始集合一样的list,然后修改截取后的集合subList,最后比较oriList==arrayList、oriList== subList

按照我们常规的思路应该是这样的:

  • arrayList是通过oriList构造出来的,所以应该相等
  • subList通过add新增了一个元素,那么它肯定与oriList不等

理论是应该是:

	oriList == arrayList:true
	oriList == subList:false

但真实结果是:

	oriList == arrayList:false
	oriList == subList: true

2.源码分析

我们先不论结果的正确与否,看看subList方法的源码:

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

subListRangeCheck() 方法是判断 fromIndextoIndex 是否合法,如果合法就直接返回一个 subList 对象。这里需要注意2点:

  • 在产生该 new 该对象的时候传递了一个参数 this ,该参数非常重要,因为他代表着原始 list
  • SubList类是ArrayList的一个内部类,这个类很特别。
    static void subListRangeCheck(int fromIndex, int toIndex, int size) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > size)
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
    }
	/**
     * 继承AbstractList类,实现RandomAccess接口
     */
    private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;    //集合
        private final int parentOffset;   
        private final int offset;
        int size;
 
        //构造函数的parent参数很重要,传的是当前对象的引用
        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;
        }
 
        //set方法
        public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }
 
        //get方法
        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }
 
        //add方法
        public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }
 
        //remove方法
        public E remove(int index) {
            rangeCheck(index);
            checkForComodification();
            E result = parent.remove(parentOffset + index);
            this.modCount = parent.modCount;
            this.size--;
            return result;
        }
        //-----------------------省略其他方法----------------
    }

第一个特别点是它的构造函数,在该构造函数中有2个地方需要注意:

  • this.parent = parent;这里的parent 就是在前面传递过来的 list,也就是说 this.parent 就是原始 list 的引用
  • this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同时在构造函数中它将 modCount(fail-fast机制)也传递过来了。

第二个特别点是它的普通方法

  • get 方法
    return ArrayList.this.elementData(offset + index);这段代码可以清晰表明 get 所返回就是原集合offset + index位置的元素。
        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }
  • add 方法
    parent.add(parentOffset + index, e); this.modCount = parent.modCount; 表明是在原集合上添加 原集合长度parentOffset +当前下标index位置上添加元素
public void add(int index, E e) {
    rangeCheckForAdd(index);
    checkForComodification();
    parent.add(parentOffset + index, e); // 注意这里
    this.modCount = parent.modCount;
    this.size++;
}
  • remove方法:
    parent.remove(parentOffset + index); this.modCount = parent.modCount; 表明删除原集合 原集合长度parentOffset +当前下标index位置的元素
public E remove(int index) {
    rangeCheck(index);
    checkForComodification();
    E result = parent.remove(parentOffset + index); // 注意这里
    this.modCount = parent.modCount;
    this.size--;
    return result;
}

由以上源码,可以判断 subList() 方法返回的 SubList 同样也是 AbstractList 的子类,同时它的方法如 get、set、add、remove 等都是在原list上面做操作,而不是生成一个新的对象

结论:

subList返回的只是原列表的一个视图,它所有的操作最终都会作用在原列表上

视图理解

视图就是集合或者映射中某一部分或者某一类数据的再映射得到的结果集,这个结果集一般不允许更新(有些视图允许更新某个元素,但是不允许新增或者删除元素),只允许读取,结果集中的数据使用的还是原来集合或者映射中的数据的引用。
.
个人理解: 集合视图就是把集合里面的东西给你展示出来度, 仅供查看(有的允许更新,但不允许删除).。 Colletion视图,Map视图都是将集合里面的内容比如: Map.keySet(), 得到Map的键值视图, Map.values()得到Map的值视图, Map.entrySet()得到键/值对视图

3.操作调用subList()方法的原始集合

从上面我们知道

  • subList截取生成的子集合只是原集合的一个视图而已,如果我们操作子集合它产生的作用都会在原集合上面表现,但是如果我们操作原集合会产生什么情况呢?
    @Test
    public void handleOriList() {
        List<Integer> oriList = new ArrayList<Integer>();
        oriList.add(1);
        oriList.add(2);

        //通过subList生成一个与oriList一样的列表 subList
        List<Integer> subList = oriList.subList(0, oriList.size());

        //修改原集合
        oriList.add(3);

        System.out.println("oriList.size=" + oriList.size());
        System.out.println("subList.size-" + subList.size());
    }

执行结果:

  • oriList正常输出,但是subList就抛出ConcurrentModificationException异常(原因为fast-fail机制

  • 通过观察源码,size方法首先会通过checkForComodification验证,然后再返回this.size

public int size() {
     checkForComodification();
     return this.size;
}

private void checkForComodification() {
     if (ArrayList.this.modCount != this.modCount)
     throw new ConcurrentModificationException();
}
  • checkForComodification方法 表明当原集合的modCountthis.modCount不相等时就会抛出ConcurrentModificationException
  • 同时modCountnew的过程中 “继承”了原集合modCount,只有在修改该集合(子集合)时才会修改该值(先表现在原集合后作用于子集合)。而在该实例中我们是操作原集合,原集合的modCount当然不会反应在子集合的modCount上啦,所以才会抛出该异常。
  • 对于子集合,它是动态生成的,生成之后就不要操作原集合了,否则必定会导致子集合的不稳定而抛出异常。最好的办法就是将原集合设置为只读状态,要操作就操作子列表
//通过subList()方法生成一个与oriList一样的列表 subList
List<Integer> subList= oriList.subList(0, oriList.size());
        
//对oriList设置为只读状态
oriList= Collections.unmodifiableList(oriList);

结论:

subList()生成子集合后,不要试图去操作原集合,否则会造成子集合的不稳定而产生异常

4.删除某一段集合

在开发中获取一堆数据后,需要删除某段数据。如,有一个列表存在1000条记录,我们需要删除100-200位置处的数据,可能我们会这样处理:

for(int i = 0 ; i < oriList.size() ; i++){
   if(i >= 100 && i <= 200){
       oriList.remove(i);
       /*
        * 当然这段代码存在问题,list remove之后后面的元素会填充上来,
        * 所以需要对i进行简单的处理,当然这个不是这里讨论的问题。
        */
   }
}

上面代码原因: List每remove掉一个元素以后,后面的元素都会向前移动,此时如果执行i=i+1,则刚刚移过来的元素没有被读取。

解决办法:

  • 利用子列表的操作都会反映在原列表上原理, 使用subList()截取指定返回集合后调用clear()清空子集合
oriList.subList(100, 200).clear();

5.如何创建新的List

利用Java8的stream特性

list.stream().skip(strart).limit(end).collect(Collectors.toList());

6.小结

我们简单总结一下,List的subList方法并没有创建一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示。
所以,我们不能把subList方法返回的List强制转换成ArrayList等类,因为他们之间没有继承关系。

另外,视图和原List的修改还需要注意几点,尤其是他们之间的相互影响:

  1. 对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。
  2. 对子List做结构性修改,操作同样会反映到父List上。
  3. 对父List做结构性修改,会抛出异常ConcurrentModificationException。
    所以,阿里巴巴Java开发手册中有另外一条规定:

m/p/73722487)


转载:https://blog.csdn.net/qq877728715/article/details/106358872
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场