如果没有看过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和LinkedList进行举例 : 为什么呢? 下面从源码角度来分析原因: 先分析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, 查看该接口的继承关系树,如下: 发现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]