Java中List的subList()方法的使用陷阱

如果没有看过List或者两个常用的实现类ArrayList、LinkedList的subList()方法的源码,而只是通过API文档,那么很多朋友很容易调入一个陷阱。或者有些朋友根据String的subString()方法来推测,List的subList()方法应该和String的subString()方法类似吧。的确,subList()得到的结果确实是该List的一个子list,这没有错,但是在得到该子list的同时,系统还做了一件隐蔽的事情,那就是,将该子List(我们称作LIst B)内部的一个重要的List(我们称作LIst C)引用字段指向了该父List (我们称作LIst A)所指向的对象(也就是说,经过subList()方法运算之后,原先只有父List (LIst A)一个引用指向的对象,现在增加为两个引用指向该对象了,这两个引用分别是List A 和 List C),而之所以说这个内部的List 引用(List C)重要,是因为凡是该子List (List B)后续的增删操作,其实在实现他自己的容量和数据变化之外,还对他内部的这个List引用字段(List C)也进行了相应的增删操作,而List A (也就是原先的父List)和 该List C又同时指向原先List A (原先的父List)所指向的对象,所以在子List(List B)进行增删操作的时候,原先的父List(List A)内存放的内容也必定会一起进行相同的增删变化。

 
先举个例子说明一下,下面分别用常见的ArrayList和LinkedList进行举例 :
public class SubListDemo
{
    public static void main(String[] args)
    {
        System.out.println("---------------ArrayList------------");
        subListTest(new ArrayList<Integer>());
        System.out.println("---------------LinkedList------------");
        subListTest(new LinkedList<Integer>());
    }
 
    private static void subListTest(List<Integer> list)
    {
        if(list == null)
        {
            throw new IllegalArgumentException("Argument " + list + " is null.");
        }
        for(int i = 0; i<5; i++)
        {
            list.add(i);
        }
         
        List<Integer> subList = list.subList(2, list.size());
        // 期望输出和实际输出一致,都是[0, 1, 2, 3, 4]
        System.out.println("Original list: " + list); 
        // 期望输出和实际输出一致,都是[2, 3, 4]
        System.out.println("Sublist:       " + subList);
         
        subList.add(10);
        // 但这里,实际输出结果却可能会出乎我们的意料,我们可能会认为输出结果不变,
 // 但却发现实际输出结果竟然变化了,比原先多了个元素10,变为 [0, 1, 2, 3, 4, 10]
        System.out.println("Original list: " + list);
        // 期望输出和实际输出一致,都是[2, 3, 4, 10]
        System.out.println("Sublist:       " + subList);
    }
}

 

实际输出结果如下:

---------------ArrayList------------
Original list: [0, 1, 2, 3, 4]
Sublist:       [2, 3, 4]
Original list: [0, 1, 2, 3, 4, 10]
Sublist:       [2, 3, 4, 10]
---------------LinkedList------------
Original list: [0, 1, 2, 3, 4]
Sublist:       [2, 3, 4]
Original list: [0, 1, 2, 3, 4, 10]   // 多了一个元素10
Sublist:       [2, 3, 4, 10]
从上述输出结果的标黄部分可知,在sublist进行add()操作时,原先的list也被add了相同的元素。同样地,sublist进行删除操作也将导致原先的list也会删除相同的元素。
为什么呢? 下面从源码角度来分析原因:
 
先分析ArrayList,下面是ArrayList的subList()方法的源码:
public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
}
该方法其实调用的是 new SubList( this, 0, fromIndex, toIndex); 这个构造方法,注意该构造方法的第一个参数this,我用黄色标注了,需要引起大家的注意。也就是说,我们在调用ArrayList的subList()方法时,他实际上是new了一个ArrayList.SubList对象(我们称作List B)作为返回值,同时在new该对象的时候将当前的ArrayList对象(我们称作List A)作为参数传递给了该ArrayList.SubList的构造方法。 
下面我们来看看ArrayList的这个内部类SubList的部分源码:
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;
        }
 
       public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }
 
        public E remove(int index) {
            rangeCheck(index);
            checkForComodification();
            E result = parent.remove(parentOffset + index);
            this.modCount = parent.modCount;
            this.size--;
            return result;
        }
 
        protected void removeRange(int fromIndex, int toIndex) {
            checkForComodification();
            parent.removeRange(parentOffset + fromIndex,
                               parentOffset + toIndex);
            this.modCount = parent.modCount;
            this.size -= toIndex - fromIndex;
        }
 
        public boolean addAll(Collection<? extends E> c) {
            return addAll(this.size, c);
        }
 
        public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
            int cSize = c.size();
            if (cSize==0)
                return false;
 
            checkForComodification();
            parent.addAll(parentOffset + index, c);
            this.modCount = parent.modCount;
            this.size += cSize;
            return true;
        }
// ..............此处省略其他代码...................
}
 刚才提到,在new这个内部类的时候,也将原ArrayList的引用作为参数传了进去,经过查看上面的源码可知,传进去的原ArrayList引用(List A),被赋值给了SubList 类内部一个名为parent的AbstractList引用(我们称作 List C, 见源码第9行),这句代码的含义是:该parent引用(List C)将指向原ArrayList引用(即:List A)所指向的对象,这样该对象将会由一个引用指向变为两个引用指向了,这两个引用有任何一对该对象进行了增删改,都会影响到另一个引用对该对象查询的结果。而我们在得到sublist后,再对该sublist进行增删改操作(见源码中的add,remove等方法)时,都会执行parent的add,remove等方法,而parent和原先的ArrayList引用指向同一个对象,因此parent执行他的add,remove等方法,其实增删的就是原ArrayList所指向的对象,所以我们就不难理解,在调用sublist的add(10)方法,让子list增加一个元素10的时候,为何原先的ArrayList中也会增加一个元素10了。 
 
下面看LinkedList的subList()的源码:
LinkedList中没有定义subList()方法,所以我们就找其父类AbstractSequentialList的源码,发现还是没有定义该方法,于是我们再找其父类的父类AbstractList的源码,终于在该类中找到了subList()方法的定义。
public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
}
 发现需要判断当前类是否是 RandomAccess 的子类,我们双击 RandomAccess ,在Eclipse中按Ctrl+T, 查看该接口的继承关系树,如下:
Java中List的subList()方法的使用陷阱
发现LinkedList并未实现该接口,所以LinkedList的subList()方法调用的是 new SubList<>(this, fromIndex, toIndex)); 该SubList类是AbstractList类的一个内部类,其实这里和前面分析ArrayList的subList()方法类似,也是将LinkedList的引用this作为参数传给了另一个List的内部类(AbstractList.SubList)的构造方法,并且该内部类中同样包含了一个List引用类型的字段(名为l),在new该内部类时,传递的this同样被赋值给了该内部的引用字段l,并且该内部类的增删改查方法同样调用的是该内部引用字段l的增删改查方法。思路和ArrayList的一样。所以就不分析了,直接贴出相关源码,并把重要的代码标黄,如下:
class SubList<E> extends AbstractList<E> {
    private final AbstractList<E> l;
    private final int offset;
    private int size;
 
    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > list.size())
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
        l = list;
        offset = fromIndex;
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }
 
    public E set(int index, E element) {
        rangeCheck(index);
        checkForComodification();
        return l.set(index+offset, element);
    }
 
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return l.get(index+offset);
    }
 
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        checkForComodification();
        l.add(index+offset, element);
        this.modCount = l.modCount;
        size++;
    }
 
    public E remove(int index) {
        rangeCheck(index);
        checkForComodification();
        E result = l.remove(index+offset);
        this.modCount = l.modCount;
        size--;
        return result;
    }
 
    protected void removeRange(int fromIndex, int toIndex) {
        checkForComodification();
        l.removeRange(fromIndex+offset, toIndex+offset);
        this.modCount = l.modCount;
        size -= (toIndex-fromIndex);
    }
 
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
 
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        int cSize = c.size();
        if (cSize==0)
            return false;
 
        checkForComodification();
        l.addAll(offset+index, c);
        this.modCount = l.modCount;
        size += cSize;
        return true;
    }
}
 
分析到此为止,我们再来回想一下文章开始处提到的那个例子中我们的疑惑,想必现在应该明白原因了吧。 那么,我们有没有什么办法,可以让获得的sublist在进行增删改查时,不会干扰到原list呢?其实是有办法的,根据我们先前的分析,subList()方法的返回值的内部包含一个引用指向了先前的List的对象,导致对该返回值进行增删改查的操作都会干扰到原先的List的内容。所以我们需要对这个方法的返回值进行一番处理,我们有两种处理方式:
(1)将subList()方法得到的结果,再进行一次包装,将他作为一个新的List对象构造方法的参数即可: 对于ArrayList: 
List<Integer> subList = new ArrayList<>(list.subList(2, list.size()));
 对于LinkedList:
List<Integer> subList = new LinkedList<>(list.subList(2, list.size()));
 
(2)将subList()方法得到的结果,也是进行一次包装,只不过是作为另一个List对象addAll()方法的参数传递过去:
对于ArrayList:
List<Integer> subList = new ArrayList<>();
subList.addAll(list.subList(2, list.size()));
 对于LinkedList:
List<Integer> subList = new LinkedList<>();
subList.addAll(list.subList(2, list.size()));
这样,输出的结果就是:
Original list: [0, 1, 2, 3, 4]
Sublist:       [2, 3, 4]
Original list: [0, 1, 2, 3, 4]   // 这里没有增加元素10了
Sublist:       [2, 3, 4, 10]

你可能感兴趣的:(java)