为何阿里巴巴要求谨慎使用ArrayList中的subList方法

集合是Java开发平常开发中常常会使用到的。java

关于集合类,《阿里巴巴Java开发手册》中其实还有另一个规定:面试

为何阿里巴巴要求谨慎使用ArrayList中的subList方法

 

本文就来分析一下为何会有如此建议?其背后的原理是什么?数据库

1.subList数据结构

subList是List接口中定义的一个方法,该方法主要用于返回一个集合中的一段、能够理解为截取一个集合中的部分元素,他的返回值也是一个List。并发

如如下代码:app

public static void main(String[] args) {
 List<String> names = new ArrayList<String>() {{
 add("Hollis");
 add("hollischuang");
 add("H");
 }};
 List subList = names.subList(0, 1);
 System.out.println(subList);
}

以上代码输出结果为:函数

[Hollis]

 

若是咱们改动下代码,将subList的返回值强转成ArrayList试一下:微服务

public static void main(String[] args) {
 List<String> names = new ArrayList<String>() {{
 add("Hollis");
 add("hollischuang");
 add("H");
 }};
 ArrayList subList = names.subList(0, 1);
 System.out.println(subList);
}

以上代码将抛出异常:高并发

java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

不仅是强转成ArrayList会报错,强转成LinkedList、Vector等List的实现类一样也都会报错。ui

那么,为何会发生这样的报错呢?咱们接下来深刻分析一下。

2.底层原理

首先,咱们看下subList方法给咱们返回的List究竟是个什么东西,这一点在JDK源码中注释是这样说的:

Returns a view of the portion of this list between the specifiedfromIndex, inclusive, and toIndex, exclusive.

也就是说subList 返回是一个视图,那么什么叫作视图呢?

咱们看下subList的源码:

public List<E> subList(int fromIndex, int toIndex) {
 subListRangeCheck(fromIndex, toIndex, size);
 return new SubList(this, 0, fromIndex, toIndex);
}

这个方法返回了一个SubList,这个类是ArrayList中的一个内部类。

SubList这个类中单独定义了set、get、size、add、remove等方法。

当咱们调用subList方法的时候,会经过调用SubList的构造函数建立一个SubList,那么看下这个构造函数作了哪些事情:

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;
}

能够看到,这个构造函数中把原来的List以及该List中的部分属性直接赋值给本身的一些属性了。

也就是说,SubList并无从新建立一个List,而是直接引用了原有的List(返回了父类的视图),只是指定了一下他要使用的元素的范围而已(从fromIndex(包含),到toIndex(不包含))。

因此,为何不能讲subList方法获得的集合直接转换成ArrayList呢?由于SubList只是ArrayList的内部类,他们之间并无继承关系,故没法直接进行强制类型转换。

3.视图有什么问题

前面经过查看源码,咱们知道,subList()方法并无从新建立一个ArrayList,而是返回了一个ArrayList的内部类——SubList。

这个SubList是ArrayList的一个视图。

那么,这个视图又会带来什么问题呢?咱们须要简单写几段代码看一下。

一、非结构性改变SubList

public static void main(String[] args) {
 List<String> sourceList = new ArrayList<String>() {{
 add("H");
 add("O");
 add("L");
 add("L");
 add("I");
 add("S");
 }};
 List subList = sourceList.subList(2, 5);
 System.out.println("sourceList : " + sourceList);
 System.out.println("sourceList.subList(2, 5) 获得List :");
 System.out.println("subList : " + subList);
 subList.set(1, "666");
 System.out.println("subList.set(3,666) 获得List :");
 System.out.println("subList : " + subList);
 System.out.println("sourceList : " + sourceList);
}

获得结果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 获得List :
subList : [L, L, I]
subList.set(3,666) 获得List :
subList : [L, 666, I]
sourceList : [H, O, L, 666, I, S]

当咱们尝试经过set方法,改变subList中某个元素的值得时候,咱们发现,原来的那个List中对应元素的值也发生了改变。

同理,若是咱们使用一样的方法,对sourceList中的某个元素进行修改,那么subList中对应的值也会发生改变。读者能够自行尝试一下。

二、结构性改变SubList

public static void main(String[] args) {
 List<String> sourceList = new ArrayList<String>() {{
 add("H");
 add("O");
 add("L");
 add("L");
 add("I");
 add("S");
 }};
 List subList = sourceList.subList(2, 5);
 System.out.println("sourceList : " + sourceList);
 System.out.println("sourceList.subList(2, 5) 获得List :");
 System.out.println("subList : " + subList);
 subList.add("666");
 System.out.println("subList.add(666) 获得List :");
 System.out.println("subList : " + subList);
 System.out.println("sourceList : " + sourceList);
}

获得结果:

sourceList : [H, O, L, L, I, S]
sourceList.subList(2, 5) 获得List :
subList : [L, L, I]
subList.add(666) 获得List :
subList : [L, L, I, 666]
sourceList : [H, O, L, L, I, 666, S]

咱们尝试对subList的结构进行改变,即向其追加元素,那么获得的结果是sourceList的结构也一样发生了改变。

三、结构性改变原List

public static void main(String[] args) {
 List<String> sourceList = new ArrayList<String>() {{
 add("H");
 add("O");
 add("L");
 add("L");
 add("I");
 add("S");
 }};
 List subList = sourceList.subList(2, 5);
 System.out.println("sourceList : " + sourceList);
 System.out.println("sourceList.subList(2, 5) 获得List :");
 System.out.println("subList : " + subList);
 sourceList.add("666");
 System.out.println("sourceList.add(666) 获得List :");
 System.out.println("sourceList : " + sourceList);
 System.out.println("subList : " + subList);
}

获得结果:

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 com.hollis.SubListTest.main(SubListTest.java:28)

咱们尝试对sourceList的结构进行改变,即向其追加元素,结果发现抛出了ConcurrentModificationException。关于这个异常,咱们在《一不当心就踩坑的fail-fast是个什么鬼?》中分析过,这里原理相同,就再也不赘述了。

4.小结

咱们简单总结一下,List的subList方法并无建立一个新的List,而是使用了原List的视图,这个视图使用内部类SubList表示。

因此,咱们不能把subList方法返回的List强制转换成ArrayList等类,由于他们之间没有继承关系。

另外,视图和原List的修改还须要注意几点,尤为是他们之间的相互影响:

  • 一、对父(sourceList)子(subList)List作的非结构性修改(non-structural changes),都会影响到彼此。
  • 二、对子List作结构性修改,操做一样会反映到父List上。
  • 三、对父List作结构性修改,会抛出异常ConcurrentModificationException。

因此,阿里巴巴Java开发手册中有另一条规定:

为何阿里巴巴要求谨慎使用ArrayList中的subList方法

 

 

5.如何建立新的List

若是须要对subList做出修改,又不想动原list。那么能够建立subList的一个拷贝:

subList = Lists.newArrayList(subList);
list.stream().skip(strart).limit(end).collect(Collectors.toList());

最后分享一份面试宝典《Java核心知识点整理.pdf》“,覆盖了JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等”,还有Java208道面试题(含答案)转发+关注而后加入群(Java填坑之路)789337293便可获取到面试宝典的免费领取方式!

相关文章
相关标签/搜索