慎用ArrayList中的SubList

双十一了,大家都省了多少钱啊?

题外话:此处交给大家一个查看商品历史价格的小方法:

  • 在商品链接的域名后加上三个v就能查看到该商品的历史价格啦

  • ????

    http://shop.taobao.com/xxxx

    http://shop.taobaovvv.com/xxx

步入正题,为什么说我们在实际开发过程中要慎用ArrayList的subList呢?其实这也是阿里军规中的一条,原因其实很简单:不稳定!也许看到这里会觉得"就是创建一个独立的新的SubList的实例,怎么会不稳定!",如果你是这么想的,那么恭喜你,这篇文章真的能够帮助到你,且往下看:


1. 看看SubList的set方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
        List sourceList = new ArrayList() {
            {
                add("H"); add("E"); add("L"); add("L"); add("O");
                add("W"); add("O"); add("R"); add("L"); add("D");
            }
        };
        List subList = sourceList.subList(2, 5);
        System.out.println("sourceList: " + sourceList);
        System.out.println("sourceList.subList(2, 5)得到: " + subList);

        subList.set(1, "cc");

        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
    }
}

上面代码的执行结果是什么?先不要看下面的答案,自己想一想。

答案

1
2
3
4
sourceList: [H, E, L, L, O, W, O, R, L, D]
subList: [L, L, O]
sourceList: [H, E, L, cc, O, W, O, R, L, D]
subList: [L, cc, O]

哦吼~!答案和你自己想的有没有出入?奇妙吧,为什么修改了subList中的元素,会影响到sourceList?我们来看下ArrayList的subList方法都做了些什么:

JDK源码

1
2
3
4
5
6
7
8
/**
 * Returns a view of the portion of this list between the specified
 * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. 
*/
public List subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

首先是检查我们的fromIndex和toIndex是否合法,然后调用ArrayList的内部类SubList创建一个SubList的实例。好像还真如我们之前想的一样,创建了一个独立的SubList的对象,没什么不对的,那我们来看一下SubList的构造器中都做了些什么吧。

1
2
3
4
5
6
7
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;
}

这是个什么鬼?ArrayList的实例对象(也就是parent)竟然作为参数传到了SubList中,SubList的偏移量为0+fromIndex,大小size为toIndex - fromIndex(也就是和String的substring方法一样,fromIndex到(toIndex -1)的数据集),修改次数modCount和ArrayList的modCount相等,那么我们猜测一下:SubList实例的变动,是否和ArrayList有关呢?

我们看到subList方法的注释中有这么一句话:Returns a view of the portion of this list。难道SubList仅仅是ArrayList的一个被fromIndex和toIndex的区间视图?

上面的例子中,subList调用了它的set方法,我们来看一下这个set方法内部逻辑是什么:

1
2
3
4
5
6
7
8
9
10
public E set(int index, E e) {
  rangeCheck(index);// 下标校验
  checkForComodification();// 校验合法性
  // ***重点
  // 根据偏移量和下标,获取ArrayList对象的elementData数组中下标为(offset + index)的元素
  // offset是什么?从构造器中我们可以看到offset就是0 + fromIndex,也就是我们截取的起始下标,也就是SubList的set方法是直接在原ArrayList实例的内部数组上进行的操作
  E oldValue = ArrayList.this.elementData(offset + index);
  ArrayList.this.elementData[offset + index] = e;
  return oldValue;
}

看到这里就一目了然了,怪不得我们修改了SubList的元素会影响到创建它的对象的值。所以在使用SubList的时候,如果需要修改SubList里面的值,一定要注意一下是否会影响到原List中的数据所涉及的业务,否则这个坑一旦踩上了,不太容易排查啊。

2. 再看看SubList的add方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
        List sourceList = new ArrayList() {
            {
                add("H"); add("E"); add("L"); add("L"); add("O");
                add("W"); add("O"); add("R"); add("L"); add("D");
            }
        };
        List subList = sourceList.subList(2, 5);
        System.out.println("sourceList: " + sourceList);
        System.out.println("sourceList.subList(2, 5)得到: " + subList);

        subList.add("cc");

        System.out.println("sourceList: " + sourceList);
        System.out.println("subList: " + subList);
    }
}

上面代码的执行结果又是什么呢?如果我们稍微思考一下,大致能正确的分析出结果:

答案

1
2
3
4
sourceList: [H, E, L, L, O, W, O, R, L, D]
subList: [L, L, O]
sourceList: [H, E, L, L, O, cc, W, O, R, L, D]
subList: [L, L, O, cc]

我们向subList中添加一个元素,原列表sourceList在toIndex的位置插入了subList中add的元素,也就是我们在SubList中新增一个元素,同时会将这个元素添加到原List中。

JDK源码

我们查看SubList的源码,发现并没有add(E e)方法,那我们调用的add(“cc”)是调用到哪里去了呢?我们查看SubList类的声明,可以看到它是继承了AbstractList抽象类,所以这里应该是调用了超类里的add(E e)方法,

1
2
3
4
5
/** AbstractList.java */
public boolean add(E e) {
  add(size(), e);
  return true;
}

这里可以看到是调用了add(int index, E element)方法进行数据新增的,然而SubList里面实现了这个方法,那么我们来看下SubList中的这个方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
public void add(int index, E e) {
  // 校验下标是否越界
  rangeCheckForAdd(index);
  // 校验原List是否有过修改
  checkForComodification();
  // parent即是在构造器中注入的原List
  parent.add(parentOffset + index, e);
  // 同步列表修改次数
  this.modCount = parent.modCount;
  // 本列表的长度+1
  this.size++;
}

由SubList的源码可以看出,SubList实例的add方法实际上就是在修改原List,包括SubList中所有的方法均是在parent列表上进行操作。

3. 奇葩操作,最坑的坑

仔细分析如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
  List sourceList = new ArrayList() {
    {
      	add("H"); add("E"); add("L"); add("L"); add("O");
      	add("W"); add("O"); add("R"); add("L"); add("D");
    }
  };
  List subList = sourceList.subList(2, 5);
  System.out.println("sourceList: " + sourceList);
  System.out.println("subList: " + subList);

  sourceList.add("cc");

  System.out.println("sourceList: " + sourceList);
  System.out.println("subList: " + subList);
}

这段代码的执行结果是什么?在不执行这段代码的情况下,是不是以为是下面的结果?

1
2
3
4
sourceList: [H, E, L, L, O, W, O, R, L, D]
subList: [L, L, O]
sourceList: [H, E, L, L, O, W, O, R, L, D, cc]
subList: [L, L, O]

如果你说对,就是这个,那你可就说错咯,实际上在执行到System.out.println("sourceList: " + sourceList);这一句代码的时候整个程序的输出都是正常的,但在执行最后一句代码的时候,就会报错了,错误信息是:

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
	at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099)
	at java.util.AbstractList.listIterator(AbstractList.java:299)
	at java.util.ArrayList$SubList.iterator(ArrayList.java:1095)
	at java.util.AbstractCollection.toString(AbstractCollection.java:454)
	at java.lang.String.valueOf(String.java:2994)
	at java.lang.StringBuilder.append(StringBuilder.java:131)
	at cc.kevinlu.sublist.SubListTest.main(SubListTest.java:31)

哦吼~!竟然报错了,我们可以看到是在ArrayList$SubList.checkForComodification方法中报的错,我们来看一下这个方法:

1
2
3
4
5
private void checkForComodification() {
  // 比较原列表修改次数和SubList的修改次数是否相等
  if (ArrayList.this.modCount != this.modCount)
    throw new ConcurrentModificationException();
}

这里抛出异常,说明这两个数是不相等的,那为什么会不相等呢?我们看SubList的add方法中有同步主、'子’列表的语句this.modCount = parent.modCount;,也就是说我们在修改subList的时候,会同步更新主列表的modCount,以保证主、'子’列表始终是一致的。

但是我们在修改主List的时候是不会去同步SubList的modCount的,我们输出SubList的实例实际上就是调用iterator方法,最终是调用了SubList的public ListIterator listIterator(final int index)方法,该方法第一句就是调用checkForComodification方法检查modCount,这里自然就会报错咯!

4. 填坑

既然有坑,就有填坑的办法,不可能一直把坑放在那,是吧。

如果既想修改subList,又不想影响到原list。那么可以创建一个基于subList的拷贝:

1
2
3
4
5
1.创建新的List:
  subList = Lists.newArrayList(subList);

2.lambda表达式:
	sourceList.stream().skip(fromIndex).limit(size).collect(Collectors.toList());

5. 总结

并不是说使用SubList一定不妥,文章开头我们也说的是慎用,所以,根据具体业务进行选择吧。

你可能感兴趣的:(慎用ArrayList中的SubList)