双十一了,大家都省了多少钱啊?
题外话:此处交给大家一个查看商品历史价格的小方法:
步入正题,为什么说我们在实际开发过程中要慎用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<String> sourceList = new ArrayList<String>() { { add("H" ); add("E" ); add("L" ); add("L" ); add("O" ); add("W" ); add("O" ); add("R" ); add("L" ); add("D" ); } }; List<String> 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 public List<E> 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<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; }
这是个什么鬼?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(); 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<String> sourceList = new ArrayList<String>() { { add("H" ); add("E" ); add("L" ); add("L" ); add("O" ); add("W" ); add("O" ); add("R" ); add("L" ); add("D" ); } }; List<String> 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 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); checkForComodification(); parent.add(parentOffset + index, e); this .modCount = parent.modCount; 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<String> sourceList = new ArrayList<String>() { { add("H" ); add("E" ); add("L" ); add("L" ); add("O" ); add("W" ); add("O" ); add("R" ); add("L" ); add("D" ); } }; List<String> 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 () { 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<E> 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一定不妥,文章开头我们也说的是慎用,所以,根据具体业务进行选择吧。