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

简介: 从源码角度解析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 异常。

「学习建议:」

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

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


相关文章
|
11月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
1097 29
|
11月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
467 4
|
11月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11月前
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
|
11月前
|
存储 前端开发 JavaScript
在线教育网课系统源码开发指南:功能设计与技术实现深度解析
在线教育网课系统是近年来发展迅猛的教育形式的核心载体,具备用户管理、课程管理、教学互动、学习评估等功能。本文从功能和技术两方面解析其源码开发,涵盖前端(HTML5、CSS3、JavaScript等)、后端(Java、Python等)、流媒体及云计算技术,并强调安全性、稳定性和用户体验的重要性。
|
12月前
|
机器学习/深度学习 自然语言处理 算法
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
3299 1
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
1221 1
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
11月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
自然语言处理 数据处理 索引
mindspeed-llm源码解析(一)preprocess_data
mindspeed-llm是昇腾模型套件代码仓,原来叫"modelLink"。这篇文章带大家阅读一下数据处理脚本preprocess_data.py(基于1.0.0分支),数据处理是模型训练的第一步,经常会用到。
420 0

推荐镜像

更多
  • DNS