从源码角度解析ArrayList.subList的几个坑!

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 从源码角度解析ArrayList.subList的几个坑!

整理收录了一些免费的电子书籍,可以从网站中直接获取:hardyfish.top/

资料链接:url81.ctfile.com/d/57345181-…

访问密码:3899

前言

《阿里巴巴Java开发手册》中提出了以下几点建议和规则:

「规则1:」

「规则2:」 本文通过源码分析,给大家讲清楚《手册》为什么这么规定

ArrayList的subList分析

首先通过 IDEA 的提供的类图工具,我们可以查看下该类的继承体系。

具体步骤:在 SubList 类中 右键,选择 “Diagrams” -> “Show Diagram” 。 可以看到 SubList 和 ArrayList 的继承体系非常类似,都实现了 RandomAccess 接口 继承自 AbstarctList。

但是SubList 和 ArrayList 并没有继承关系,因此 ArrayList 的 SubList 并不能强转为 ArrayList 。

「下面我们写一个简单的测试代码片段来验证转换异常问题:」

public static void main(String[] args) {
    List<Integer> integerList = new ArrayList<>();
    integerList.add(0);
    integerList.add(1);
    integerList.add(2);
    List<Integer> subList = integerList.subList(0, 1);
    ArrayList<Integer> castList = (ArrayList<Integer>) subList;
  }

输出:

Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList

从上面的结果也可以清晰地看出,subList 并不是 ArrayList 类型的实例,不能强转为 ArrayList 。

「我们再来写一个代码片段:」

public static void main(String[] args) {
    List<String> stringList = new ArrayList<>();
    stringList.add("月");
    stringList.add("伴");
    stringList.add("小");
    stringList.add("飞");
    stringList.add("鱼");
    stringList.add("哈");
    stringList.add("哈");
    List<String> subList = stringList.subList(2, 4);
    System.out.println("子列表:" + subList.toString());
    System.out.println("子列表长度:" + subList.size());
    subList.set(1, "周星星");
    System.out.println("子列表:" + subList.toString());
    System.out.println("原始列表:" + stringList.toString());
  }

输出:

子列表:[小, 飞]
子列表长度:2
子列表:[小, 周星星]
原始列表:[月, 伴, 小, 周星星, 鱼, 哈, 哈]

可以观察到,对子列表的修改最终对原始列表产生了影响。

「那么为啥修改子序列的索引为 1 的值影响的是原始列表的第 4 个元素呢?」

下面将进行源码分析和解读。

源码分析

可以看到SubList是ArrayList的一个内部类

这个 SubList 中的 parent 字段就是原始的 List。我们 可以认为 SubList 是原始 List 的视图

看看java.util.ArrayList#subList 源码: 这里在构造子列表对象的时候传入了 this,创建了SubList类

同时从上面注释中我们可以学到以下知识点:

  • 该方法返回本列表中 fromIndex (包含)和 toIndex (不包含)之间的元素视图。如果两个索引相等会返回一个空列表。
  • 如果需要对 list 的某个范围的元素进行操作,可以用 subList,如:list.subList(from, to).clear();
  • 任何对子列表的操作最终都会反映到原列表中。

我们再来看下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;
        }

通过子列表的构造函数我们知道,这里的偏移量 ( offset ) 的值为 fromIndex 参数,因为参数传的offset等于0

接下来我们再看下函数 java.util.ArrayList.SubList#set 源码:

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

可以看到替换值的时候,获取索引是通过 offset + index 计算得来的。

这里的 java.util.ArrayList#elementData 即为原始列表存储元素的数组。

因此上面提到的:

为啥子序列的索引为 1 的值影响的是原始列表的第 4 个元素呢?

这个问题就解答了

「到现在完成了规则1的解释了」

另外在 SubList 的构造函数中,我们发现会将 ArrayList 的 modCount 赋值给 SubList 的 modCount 。

「所以这个就引出了另外一个问题」

我们先看 java.util.ArrayList#add(E)的源码:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

可以发现新增元素和删除元素,都会对 modCount 进行修改。

我们来看下 SubList 的 核心的函数 java.util.ArrayList.SubList#get

public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

会进行修改检查:

private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
        }

我们再来看下 SubList 的 核心的函数 java.util.ArrayList.SubList#add


public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

也会调用checkForComodification进行检查

从上面的 SubList 的构造函数我们可以知道,SubList 复制了 ArrayList 的 modCount,因此对原函数的新增或删除都会导致 ArrayList 的 modCount 的变化。

而子列表的遍历、增加、删除时又会检查创建 SubList 时的 modCount 是否一致,显然此时两者会不一致,导致抛出 ConcurrentModificationException (并发修改异常)。

「至此上面的规则2的原因我们也清楚了。」

「既然 SubList 相当于原始 List 的视图,那么避免相互影响的修复方式有两种:」

一种是,不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在构 造方法传入 SubList,来构建一个独立的 ArrayList;

List<Integer> subList = new ArrayList<>(list.subList(1, 4));

另一种是,对于 Java 8 使用 Stream 的 skip 和 limit API 来跳过流中的元素,以及限制 流中元素的个数,同样可以达到 SubList 切片的目的。

List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());

总结

本文通过类图分析、源码分析以及的方式对 ArrayList 的 SubList 问题进行分析

「要点:」

  • ArrayList 内部类 SubList 和 ArrayList 没有继承关系,因此无法将其强转为 ArrayList 。
  • subList()返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视 图,对于 SubList 的所有操作最终会反映到原列表上
  • ArrayList 的 SubList 构造时传入 ArrayList 的 modCount,因此对原列表的修改将会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。

「学习建议:」

学习不能浮于表面,不能知其然而不知其所以然。而看源码是掌握深度知识最好的方法。

希望大家从现在开始学习和开发中能够偶尔到感兴趣的类中查看源码,这样学的更快,更扎实。通过进入源码中自主研究,这样印象更加深刻,掌握的程度更深。


相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
83 2
|
6天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
6天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
6天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
30天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
55 12
|
25天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
7天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
1月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
59 3
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
67 5

推荐镜像

更多