java sublist_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());

System.out.println("---------------LinkedList------------");

subListTest(new LinkedList());

}

private static void subListTest(List list)

{

if(list == null)

{

throw new IllegalArgumentException("Argument " + list + " is null.");

}

for(int i = 0; i<5; i++)

{

list.add(i);

}

List 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 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 implements RandomAccess {

private final AbstractList parent;

private final int parentOffset;

private final int offset;

int size;

SubList(AbstractList 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 subList(int fromIndex, int toIndex) {

return (this instanceof RandomAccess ?

new RandomAccessSubList<>(this, fromIndex, toIndex) :

new SubList<>(this, fromIndex, toIndex));

}

发现需要判断当前类是否是 RandomAccess 的子类,我们双击 RandomAccess ,在Eclipse中按Ctrl+T, 查看该接口的继承关系树,如下:

fcb6e0c81cdd6570482a44a514881673.png

发现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 extends AbstractList {

private final AbstractList l;

private final int offset;

private int size;

SubList(AbstractList 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 subList = new ArrayList<>(list.subList(2, list.size()));

对于LinkedList:

List subList = new LinkedList<>(list.subList(2, list.size()));

(2)将subList()方法得到的结果,也是进行一次包装,只不过是作为另一个List对象addAll()方法的参数传递过去:

对于ArrayList:

List subList = new ArrayList<>();

subList.addAll(list.subList(2, list.size()));

对于LinkedList:

List 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]

c1f89ee121c8551574103e180b913318.png

大小: 24 KB

分享到:

18e900b8666ce6f233d25ec02f95ee59.png

72dd548719f0ace4d5f9bca64e1d7715.png

2015-05-17 21:54

浏览 7132

评论

1 楼

cleverGump

2015-05-17

请教下大家,我想把重点代码标黄,但文中只有this被成功标黄了,其他都显示成了如下的html代码 ,请问我该怎么处理才能标黄呢?在此先提前谢谢了!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值