源码解析 || ArrayList源码解析
前言
这篇文章的ArrayList源码是基于jdk1.8版本的源码,如果与前后版本的实现细节出现不一致的地方请自己多加注意。先上一个它的结构图
ArrayList作为一个集合工具,对于我而言它值得我们注意的地方有:
- 参数的作用细节
- 扩容的细节
- 迭代的细节
- 特殊API的细节
那么我就由这四个细节对ArrayList进行分析。
ArrayList的参数细节
ArrayList参数其实并不是特别多,值得我们拿出来讲的那就更少了。下面我通过一张图的展示,同时列出一些值得我们谈一谈的参数:
- DEFAULT_CAPACITY、MAX_ARRAY_SIZE 两个参数规定了ArrayList的默认长度和最大长度。但是,如果使用默认构造函数,在初始化ArrayList的时候,它的长度是0,只有第一次添加数据时,它才会扩容为10;
- EMPTY_ELEMENTDATA、DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这两个参数的值都是Object空数组,至于为什么作者要写两个同样的变量,那当然是为了区分不同方法当中的语义,使源码更加易读,当然这对于我们来说可以往后再关注。
- size ArrayList中的大小,注意这也可以指已存放的数据的个数,和下面elementData的长度也还有一定的区别。
- elementData Object数组,这个是整个ArrayList最核心的参数之一,ArrayList存放的数据都在这里,对ArrayList的增删改查都基于这个Object数组。同时,它是被transient关键字修饰的,这意味着ArrayList需要进行序列化的时候,会把它忽略。那么我们会有一个问题,elementData 里面的数据难道不用序列化了吗? 答案当然是需要的,但是它不是直接将一整个数组都序列化,而是通过方法writeObject(),把elementData 中有数据的位置序列化。通俗的话就是,它序列化elementData的前size个,而elementData的真实长度中,size后面的空间都认为是没有数据的,如果也将它序列化会造成一定的流量浪费,影响传输性能。
- modCount 这个参数不是在ArrayList中声明的,它是在父类AbstractList中声明的,它的作用是记录ArrayList的结构(增加或删除)改变次数,以此来配合迭代器进行安全检查,迭代器一旦发现modCount被修改了,则会抛出ConcurrentModificationException。
扩容的细节
首先,为什么不讲增删改查直接谈扩容呢?因为ArrayList的查找和修改的实现细节其实和普通的数组操作一样,并没有什么特别的地方。而添加和删除涉及到数组的动态调整,也就是我现在写的扩容,其它的其实和普通的数组操作差不多。
那么,当ArrayList执行一次add()方法的时候,它会有什么样的操作呢?首先我们先来看一个图。这是一个方法嵌套,执行到最后,就能确保list的空间是充足的。
上面是执行add方法后的一系列调用流程。可以看出方法内在调用完ensureCapacityInternal()后,空间是能确保数据的填充的。而往下调用的方法中,我们只关注grow()方法就行,这是个扩容方法,具体的代码和意图如下所示。
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//新容量暂时为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//这一步是确保扩容的时候,扩容的空间尽量合理,避免频繁扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//假设参数大于MAX_VALUE,设定最大容量为Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//调用数组工具把数据覆盖并开辟内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
迭代的细节
ArrayList的迭代器采用的是fast-fail方式,也就是我们的快速失败方式。这是什么意思呢?我们直接贴出源码的注释来解释。
在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
那么 ,iterator通过什么方式判断列表被修改了呢?答案是在ArrayList内部还有一个内部 Iterator的实现类,里面有一个参数expectedModCount,这个值与modCount 比较,若两个值不相等,则抛出异常。
//源码方法
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
注意!我所说的迭代要注意的细节,指的是在循环过程中,伴随着ArrayList结构上的修改,例如添加或删除。如果只是简单的循环遍历输出,其实各种方法没有太大的区别。
下面例举集中常见的错误使用方式:
这种情况通常是迭代用迭代器,而对于修改则是用ArrayList的方法引起的,要解决这个问题只需要利用iterator的remove()方法即可。
ArrayList<Integer> list = new ArrayList<>(3);
list.add(111);
list.add(222);
list.add(333); Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer next = iterator.next();
if (next.equals(111)){
//错误的使用,在创建迭代器后使用list方法来remove,会抛出ConcurrentModificationException
list.remove(new Integer(111));
}
System.out.println(next);
}
}
下面这种情况如果你只是想要找到一个目标值,同时将这个值删除并break退出是可以的。但是,如果你还要继续遍历下去,这种情况则会导致ArrayList集合遍历的不完整。
ArrayList<Integer> list = new ArrayList<>(3);
list.add(111);
list.add(222);
list.add(333);
for (int i=0;i<list.size();i++){
if (list.get(i).equals(222)){
//执行这一步,size的值为2,导致333这个值没有输出就结束循环。
list.remove(i);
continue;
}
System.out.println(list.get(i)); //输出结果:111
} }
一般来说,当我们迭代有对ArrayList进行remove的需求的时候,可以利用迭代器来遍历ArrayList的结构,这样比较安全规范的且不会产生错误。
import java.util.ArrayList;
import java.util.Iterator; public class ArrayListDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(111);
list.add(222);
list.add(333);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
Integer next = iterator.next();
if(next.equals(222)){
iterator.remove();
continue;
}
System.out.println(next);
}
}
}
特殊API的细节
- remove方法参数的不确定性。
就在我刚才的示例代码中就有一种隐藏的危险,当你的ArrayList存放的是Integer类型的时候,有一种场景下你需要移除一个值。你会理所当然的调用list.remove(222);这个方法来移除222这个值。但是,这个时候其实它是指移除索引位置在222上的值。这个时候就会有删除错误或者范围异常的发生。
Integer next = iterator.next();
if (next.equals(222)){ list.remove(222);
}
2. subList方法的实现及返回类型
ArrayList中有一个subList方法可以用于对ArrayList的切割,下面先列出subList方法返回在SubList内部类的继承关系和构造方法。
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size; 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接口来接收返回的集合,但是其实它返回的具体类型是一个内部类SubList。与ArrayList肯定不是同一个类型,因此,如果你把它强制转换为ArrayList则会发生错误。
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(3);
list.add(111);
list.add(222);
list.add(333);
List<Integer> subList = list.subList(0, 1);
//编译不报错,运行时报错
ArrayList worngList = (ArrayList)subList;
}
同时,从构造函数可以看出。SubList类中的集合其实是从ArrayList中直接赋值来的,它只是通过修改边界范围和size来构成一个新集合。也就是说,实质上SubList和ArrayList用的是同一个elementData数组。因此,当对SubList进行增删改的时候,也会影响到ArrayList的存放的数据。示例代码如下:
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(3);
list.add(111);
list.add(222);
list.add(333);
List<Integer> subList = list.subList(0, 1);
subList.add(444);
subList.add(555);
}
我们通过debug可以看到,当添加到444,555的时候,两个对象都出现了这两个数据。
源码解析 || ArrayList源码解析的更多相关文章
- Java集合源码剖析——ArrayList源码剖析
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- Java容器源码学习--ArrayList源码分析
ArrayList实现了List接口,它的底层数据结构是数组,因此获取容器中任意元素值的时间复杂度为O(1),新增或删除元素的时间复杂度为O(N).每一个ArrayList实例都有一个capacity ...
- ArrayList源码解析
ArrayList简介 ArrayList定义 1 public class ArrayList<E> extends AbstractList<E> implements L ...
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...
- 面试必备:ArrayList源码解析(JDK8)
面试必备:ArrayList源码解析(JDK8) https://blog.csdn.net/zxt0601/article/details/77281231 概述很久没有写博客了,准确的说17年以来 ...
- Java泛型底层源码解析-ArrayList,LinkedList,HashSet和HashMap
声明:以下源代码使用的都是基于JDK1.8_112版本 1. ArrayList源码解析 <1. 集合中存放的依然是对象的引用而不是对象本身,且无法放置原生数据类型,我们需要使用原生数据类型的包 ...
- ArrayList源码解析[一]
ArrayList源码解析[一] 欢迎转载,转载烦请注明出处,谢谢. https://www.cnblogs.com/sx-wuyj/p/11177257.html 在工作中集合list集合用的相对来 ...
- ArrayList源码解析(二)
欢迎转载,转载烦请注明出处,谢谢. https://www.cnblogs.com/sx-wuyj/p/11177257.html 自己学习ArrayList源码的一些心得记录. 继续上一篇,Arra ...
- Java中的容器(集合)之ArrayList源码解析
1.ArrayList源码解析 源码解析: 如下源码来自JDK8(如需查看ArrayList扩容源码解析请跳转至<Java中的容器(集合)>第十条):. package java.util ...
随机推荐
- Spring事务异常rollback-only 笔记
造成以上异常的原因情形: 在spring里面我们配置了事务的传播机制是REQUIRED,所以这两个事务最终会合并成一个事务.当a方法调用b方法时,程序中a方法中由于某某原因导致抛出异常(或者明确将该事 ...
- 【转】java 泛型详解
java 泛型详解 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下. 本文参考java 泛型详解.Java中的泛型方法. java泛型详解 ...
- oracle--DG查询同步
查询归档历史: SELECT FIRST_TIME,FIRST_CHANGE#,NEXT_CHANGE#, SEQUENCE# FROM V$LOG_HISTORY; 检查归档文件路径和创建信息 SE ...
- Helm 常用命令及操作
Helm 常用命令 查看版本 #helm version 查看当前安装的charts #helm list 查询 charts #helm search redis 安装charts #helm in ...
- 【转】PyQt弹出式对话框的常用方法及标准按钮类型
pyQt之弹出式对话框(QMessageBox)的常用方法及标准按钮类型 一.控件说明 QMessageBox是一种通用的弹出式对话框,用于显示消息,允许用户通过单击不同的标准按钮对消息进行反馈,且每 ...
- Blackbox_exporter黑盒监测
一.概述 blackbox_exporter是Prometheus 官方提供的 exporter 之一,可以提供 http.dns.tcp.icmp 的监控数据采集.Blackbox_exporter ...
- Linux(01):linux的起源、应用场景和学习目标
- WPF 精修篇 动画组TransformGroup
原文:WPF 精修篇 动画组TransformGroup 动画分组 TransformGroup 一个元素可能要有缩放 ScaleTransform和移动 TranslateTransform等多个效 ...
- 聊聊 .net Core webAPi 的Get和POST 相关(1)
上篇文章,我们试着调用API,成功返回值,今天接下来看看代码是怎么构成的 [Route("api/[controller]")] [ApiController] public cl ...
- Oracle数据库触发器
第一次写触发器,浪费了一个小时,少了一个;编译不通过 当A表有更新或插入数据时,则触发器执行向B表插入对应条件的数据1 CREATE OR REPLACE TRIGGER Test -- 触发器名称 ...