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

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

作者 | Hollis

集合是Java开发日常开发中经常会使用到的。在之前的一些文章中,我们介绍过一些关于使用集合类应该注意的事项,如《为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作》、《为什么阿里巴巴建议集合初始化时,指定集合容量大小》等。

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

image.png

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

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的一个视图。

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

1、非结构性改变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中对应的值也会发生改变。读者可以自行尝试一下。

1、结构性改变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的结构也同样发生了改变。

1、结构性改变原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是个什么鬼?》中分析过,这里原理相同,就不再赘述了。

小结

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

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

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

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

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

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

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

image.png

如何创建新的List

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

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

参考资料:
https://www.jianshu.com/p/5854851240df
https://www.cnblogs.com/ljdblog/p/6251387.html

来源 | HollisChuang's Blog

相关文章
|
Java 测试技术 API
Java RESTful中的PATCH请求:局部更新与资源修改
在RESTful架构中,PATCH请求是一种用于局部更新已有资源的操作。PATCH请求允许客户端将部分数据发送到服务器,以便对资源进行局部修改,而不必替换整个资源。本文将引导您深入了解Java中使用PATCH请求构建RESTful API,探讨其特点、实现方式、用例以及在实际应用中的优势。
|
24天前
|
人工智能 Linux API
阿里云轻量服务器部署OpenClaw与千问Qwen3.6-Plus全流程实战:从环境搭建到模型接入及避坑指南
OpenClaw(原Clawdbot)作为2026年主流的开源AI智能体框架,凭借轻量化、跨平台、可自托管、多IM集成与大模型灵活对接的特性,成为个人与团队搭建专属AI助手的首选方案。本文聚焦阿里云轻量服务器部署OpenClaw的最简流程,覆盖本地MacOS/Linux/Windows11全平台部署方法,详解阿里云千问Qwen3.6-Plus高性能API配置、飞书即时通讯集成步骤,并整理全场景高频问题与避坑方案,所有命令可直接复制运行,零基础用户可按步骤一次部署成功。
809 17
|
存储 缓存 安全
每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal
每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal
414 0
|
7月前
|
人工智能
2025数字人短视频工具TOP5榜单:从入门到进阶的必备神器 
随着人工智能技术的快速发展,数字人短视频工具正成为内容创作领域的重要助力。从入门级简单操作到进阶专业应用,各类工具功能各异。本文将为您揭晓2025年最值得关注的五款数字人工具,助您轻松选择最适合的创作伙伴。
|
Java 调度
ScheduledThreadPoolExecutor分析
ScheduledThreadPoolExecutor 是一种适用于延时或周期性任务调度的线程池。它继承自 ThreadPoolExecutor,具备更强大的功能,可通过设定参数实现周期性任务调度。 内部实现中,`ScheduledFutureTask` 是关键类,通过 `run` 方法实现任务的循环调度与时间管理。`DelayedWorkQueue` 提供延迟队列功能,利用堆结构确保任务按优先级执行,并通过条件变量实现精确的延迟控制。整体设计结合了循环调度与延迟机制,满足高效的任务管理需求。
224 1
|
9月前
|
存储 设计模式 NoSQL
ddd领域驱动设计
领域驱动设计(DDD)是一种应对复杂软件系统的思维革命与系统方法。它通过“通用语言”统一团队认知,运用“限界上下文”划分业务边界,结合“聚合”“领域事件”等战术模式,精准构建业务模型。DDD不仅提升软件对业务的映射能力,更为微服务架构提供科学的边界划分依据,是打造高内聚、低耦合系统的核心方法论。
|
SQL JSON Java
Hive学习-数据查询语句
Hive学习-数据查询语句
427 6
|
SQL Java 数据库连接
Hibernate 批量操作来袭!掌握最佳实践,轻松应对数据洪流,开启高效开发新时代
【9月更文挑战第3天】在软件开发中,高效数据操作至关重要。作为流行的Java持久化框架,Hibernate提供了强大的数据库操作功能。本文探讨了Hibernate批量操作,包括批量插入、更新和删除的最佳实践,通过使用原生SQL和`Session`的`createNativeQuery()`方法,结合`addBatch()`及`executeBatch()`方法实现高效批量操作。合理设置批量大小、事务管理和性能测试是优化的关键。在实际开发中,应根据业务需求和性能要求选择合适的方法,以提升程序性能和可维护性。
1054 3
|
存储 Java 数据库连接
JPA 之 Hibernate EntityManager 使用指南
JPA 之 Hibernate EntityManager 使用指南
1768 0