asList和subList的缺陷

概述

在开发中我们经常使用asList去把一个数组转换为List。也存在通过subList、subMap、来对List、Map、Set来进行类似使用subString方法来对String对象进行分割处理一样来对它们进行分割处理。

但是,asList存在一些缺陷。而sub方法也存在一些瑕疵。

使用8个基本类型数组转换为列表时会存在一个比较有味的缺陷。先看如下程序:

public static void main(String[] args) {

        int[] ints = {1,2,3,4,5,6} ;
        List list = Arrays.asList(ints) ;
        System.out.println("list.size() = "+list.size());
    }
-------------   output  ------------
list.size() = 1

  程序的运行结果并没有像我们预期的那样是5而是逆天的1,这是什么情况?先看源码:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

  asLsit接受的参数是一个泛型的变长参数。我们知道基本数据类型是无法泛型化的。也就是说8个基本类型是无法作为asList的参数。要想作为泛型参数就必须使用其所对应的包装类型。但是,这个实例中为什么没有出错。并且返回 1 了 ? 因为该实例是将int类型的数组当了其参数。而在Java中数组是一个对象。它是可以泛型化的。所以该例子是不会产生错误的。既然例子是将整个int 类型的数组当做泛型参数。那么,经过asList转化就只有一个int 的列表了。如下:

public static void main(String[] args) {

        int[] ints = {1,2,3,4,5,6} ;
        List list = Arrays.asList(ints) ;

        System.out.println("list 的类型 :"+list.get(0).getClass());
        System.out.println("list.get(0) == ints :"+list.get(0).equals(ints));
    }
------------    output  ------------
list 的类型:class [I
list.get(0) == ints:true 

  从这个运行结果我们可以充分证明 list里面的元素就是 int 数组。弄清楚这点了,那么修改方法也就一目了然了:将int 改变为Integer。

public static void main(String[] args) {

        Integer[] ints = {1,2,3,4,5,6} ;
         List list = Arrays.asList(ints);
            System.out.println("list.size() = " + list.size());
            System.out.println("list.get(0) 的类型:" + list.get(0).getClass());
            System.out.println("list.get(0) == ints[0]:" + list.get(0).equals(ints[0]));
    }
---------   output  ------------
list.size() = 6
list.get(0) 的类型:class java.lang.Integer
list.get(0) == ints[0]:true

  细节 : 在使用asList时不要将基本数据类型当作参数

二、asList产生的列表不可操作

 对于上面的实例我们再做一个小小的修改:

public class asList {

    public static void main(String[] args) {

        Integer[] ints = {1,2,3,4,5,6} ;
         List list = Arrays.asList(ints);

         list.add(7);
         System.out.println("list = "+list );
    }

  该实例就是讲ints通过asList转换为list 类别,然后再通过add方法加一个元素,这个实例简单的不能再简单了,但是运行结果呢?打出我们所料:

运行结果尽然抛出UnsupportedOperationException异常,该异常表示list不支持add方法。这就让我们郁闷了,list怎么可能不支持add方法呢?难道jdk脑袋堵塞了?我们再看asList的源码:

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

  asList接受参数后,直接new 一个ArrayList,到这里看应该是没有错误的啊?别急,再往下看:


private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable{
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            if (array==null)
                throw new NullPointerException();
            a = array;
        }
        //.................
    }

  这是ArrayList的源码,从这里我们可以看出,此ArrayList不是java.util.ArrayList,他是Arrays的内部类。该内部类提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改变list结果的方法从AbstractList父类继承过来,同时这些方法也比较奇葩,它直接抛出UnsupportedOperationException异常:

通过这些代码可以看出asList返回的列表只不过是一个披着list的外衣,它并没有list的基本特性(变长)。该list是一个长度不可变的列表,传入参数的数组有多长,其返回的列表就只能是多长。所以: 细节 : 不要试图改变asList返回结果的列表,不然坐看枫叶飘落

三、subList返回仅仅只是一个视图

 首先我们先看如下实例:

  public static void main(String[] args) {

           List<Integer> list1 = new ArrayList<Integer>();
            list1.add(1);
            list1.add(2);

            //通过构造函数新建一个包含list1的列表 list2
            List<Integer> list2 = new ArrayList<Integer>(list1);

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

            //修改list3
            list3.add(3);

           System.out.println("list _1 = "+list1+"     list _2 = "+list2+"     list _3 = "+list3);

            System.out.println("list1 == list2:" + list1.equals(list2));
            System.out.println("list1 == list3 :" + list1.equals(list3));
    }

  这个例子非常简单,无非就是通过构造函数、subList重新生成一个与list1一样的list,然后修改list3,最后比较list1 == list2?、list1 == list3?。按照我们常规的思路应该是这样的:因为list3通过add新增了一个元素,那么它肯定与list1不等,而list2是通过list1构造出来的,所以应该相等,所以我们猜想的结果应该是: >list1 == list2:true >list1 == list3 : false 首先我们先不论结果的正确与否,我们先看subList的源码:

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

  subListRangeCheck方式是判断fromIndex、toIndex是否合法,如果合法就直接返回一个subList对象, 注意在产生该new该对象的时候传递了一个参数 this ,该参数非常重要,因为他代表着原始list。

/**
     * 继承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;

        //构造函数
        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;
        }
    }

  

该SubLsit是ArrayList的内部类,它与ArrayList一样,都是继承AbstractList和实现RandomAccess接口。
同时也提供了get、set、add、remove等list常用的方法。但是它的构造函数有点特殊,在该构造函数中有两个地方需要注意:
1、this.parent = parent;而parent就是在前面传递过来的list,也就是说this.parent就是原始list的引用。

2、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同时在构造函数中它甚至将modCount(fail-fast机制)传递过来了。

  我们再看get方法,在get方法中return ArrayList.this.elementData(offset + index);这段代码可以清晰表明get所返回就是原列表offset + index位置的元素。同样的道理还有add方法里面的:

parent.add(parentOffset + index, e);

this.modCount = parent.modCount;

  remove方法里面的

E result = parent.remove(parentOffset + index);

this.modCount = parent.modCount;

  诚然,到了这里我们可以判断subList返回的SubList同样也是AbstractList的子类,同时它的方法如get、set、add、remove等都是在原列表上面做操作,它并没有像subString一样生成一个新的对象。所以 subList返回的只是原列表的一个视图,它所有的操作最终都会作用在原列表上;。 那么从这里的分析我们可以得出上面的结果应该恰恰与我们上面的答案相反:

list _1 = [1, 2, 3] list _2 = [1, 2] list _3 = [1, 2, 3]

list1 == list2:false

list1 == list3:true

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

四、subList生成子列表后,不要试图去操作原列表

 从上面我们知道subList生成的子列表只是原列表的一个视图而已,如果我们操作子列表它产生的作用都会在原列表上面表现,但是如果我们操作原列表会产生什么情况呢?



public static void main(String[] args) {

         List<Integer> list1 = new ArrayList<Integer>();
            list1.add(1);
            list1.add(2);

            //通过subList生成一个与list1一样的列表 list3
            List<Integer> list3 = list1.subList(0, list1.size());
            //修改原列表 list1的数据
            list1.add(3);

            System.out.println("list1.size:" + list1.size()+"  list1的元素 :"+list1);
            System.out.println("list3.size:" + list3.size()+"  list3的元素 :"+list3);

    }

  该实例如果不产生意外,那么他们两个list的大小都应该都是3,但是偏偏事与愿违,事实上我们得到的结果是这样的:

list1正常输出,但是list3就抛出ConcurrentModificationException异常,看过我另一篇博客的同仁肯定对这个异常非常,fail-fast?不错就是fail-fast机制,在fail-fast机制中,(更多请点这里:fail-fast机制)。我们再看size方法:

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

  

size方法首先会通过checkForComodification验证,然后再返回this.size。



private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
        }

  

该方法表明当原列表的modCount与this.modCount不相等时就会抛出ConcurrentModificationException。同时我们知道modCount 在new的过程中 “继承”了原列表modCount,只有在修改该列表(子列表)时才会修改该值(先表现在原列表后作用于子列表)。而在该实例中我们是操作原列表,原列表的modCount当然不会反应在子列表的modCount上啦,所以才会抛出该异常。

对于子列表视图,它是动态生成的,生成之后就不要操作原列表了,否则必然都导致视图的不稳定而抛出异常。最好的办法就是将原列表设置为只读状态,要操作就操作子列表:

public static void main(String[] args) {

         List<Integer> list1 = new ArrayList<Integer>();
            list1.add(1);
            list1.add(2);

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

          //对list1设置为只读状态
            list1 = Collections.unmodifiableList(list1);

            //修改list3
            list1.add(3);

            System.out.println("list1.size:" + list1.size()+"  list1的元素 :"+list1);
            System.out.println("list3.size:" + list3.size()+"  list3的元素 :"+list3);

    }

  Exception in thread “main” java.lang.UnsupportedOperationException
        at java.util.Collections$UnmodifiableCollection.add(Collections.java:1055)
        at hu.subList.main(subList.java:22)

细节 : :生成子列表后,不要试图去操作原列表,否则会造成子列表的不稳定而产生异常

 

五、推荐使用subList处理局部列表

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

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

  

这个应该是我们大部分人的处理方式吧,其实还有更好的方法,利用subList。在前面LZ已经讲过,子列表的操作都会反映在原列表上。所以下面一行代码全部搞定:

list1.subList(100, 200).clear();

  

 

 

原文地址:https://www.cnblogs.com/q1359720840/p/14360590.html