当前位置: 首页 > 编程笔记 >

为什么阿里要慎重使用ArrayList中的subList方法

巴博耘
2023-03-14
本文向大家介绍为什么阿里要慎重使用ArrayList中的subList方法,包括了为什么阿里要慎重使用ArrayList中的subList方法的使用技巧和注意事项,需要的朋友参考一下

前言

集合是Java开发日常开发中经常会使用到的。

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

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

subList

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

如以下代码:

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的实现类同样也都会报错。

那么,为什么会发生这样的报错呢?我们接下来深入分析一下。

底层原理

首先,我们看下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的内部类,他们之间并没有集成关系,故无法直接进行强制类型转换。

视图有什么问题

前面通过查看源码,我们知道,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。

小结

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

所以,我们不能把subList方法返回的List强制转换成ArrayList等类,因为他们之间没有继承关系。

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

1、对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。

2、对子List做结构性修改,操作同样会反映到父List上。

3、对父List做结构性修改,会抛出异常ConcurrentModificationException。

所以,阿里巴巴Java开发手册中有另外一条规定:

如何创建新的List

如果需要对subList作出修改,又不想动原list。那么可以创建subList的一个拷贝:

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。

 类似资料:
  • 本文向大家介绍python 为什么说eval要慎用,包括了python 为什么说eval要慎用的使用技巧和注意事项,需要的朋友参考一下 eval前言 当内存中的内置模块含有os的话,eval同样可以做到命令执行: 当然,eval只能执行Python的表达式类型的代码,不能直接用它进行import操作,但exec可以。如果非要使用eval进行import,则使用__import__: 在实际的代码中

  • 问题内容: 我最近找到了一个示例代码: 该方法将打开一个用户界面窗口。然后,我尝试将代码修剪如下: 两种版本均能正常工作。有什么区别? 问题答案: 任何一种代码都能在99%的时间内工作。 但是,Swing的设计使得对Swing组件的所有更新都应在事件分发线程(EDT)上完成。阅读有关并发的Swing教程以获取更多信息。 问题是它有1%的时间可能无法正常工作。您不想浪费时间尝试调试随机问题。

  • 本文向大家介绍解释下为什么说通配符选择器要慎用?相关面试题,主要包含被问及解释下为什么说通配符选择器要慎用?时的应答技巧和注意事项,需要的朋友参考一下 通配符号的性能会很差,建议覆盖 特定的标签,例如 body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input

  • 本文向大家介绍Java中AbstractSequentialList的subList()方法,包括了Java中AbstractSequentialList的subList()方法的使用技巧和注意事项,需要的朋友参考一下 该方法返回此列表的一部分,位于指定的fromIndex(包括)和toIndex(不包括)之间。通过将范围设置为两个参数,使用该方法获取子列表。 语法如下- 在这里,参数fromIn

  • 问题内容: 我正在努力了解为什么在Java中 需要方法重载和重写 ? 我已经阅读了一些与此相关的文章,但无法理解为什么实际上需要它? 我还访问了以下url,但在该主题中我还不清楚。 Java重载和覆盖 任何实际的例子将不胜感激。 提前致谢。 问题答案: 来自doc的 方法重载: 假设您有一个可以使用书法绘制各种类型的数据(字符串,整数等)的类,并且包含一个用于绘制每种数据类型的方法。为每个方法使用

  • 本文向大家介绍为什么要使用 kafka,为什么要使用消息队列?相关面试题,主要包含被问及为什么要使用 kafka,为什么要使用消息队列?时的应答技巧和注意事项,需要的朋友参考一下 缓冲和削峰:上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka在中间可以起到一个缓冲的作用,把消息暂存在kafka中,下游服务就可以按照自己的节奏进行慢慢处理。 解耦和扩展性:项目开始的