如果没有看过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, 查看该接口的继承关系树,如下:
发现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]