源码解析 || 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 Cloud微服务安全实战_4-2_常见的微服务安全整体架构
这个图适用于中小公司的微服务架构 微服务:SpringBoot 写的Rest服务 服务注册与发现:微服务所必备的.每个微服务都会到上边去注册.不管是微服务之间的调用,还是服务网关到微服务的转发,都是通 ...
- BBS 03day
目录 BBS_03 day: 自定义标签 过滤器: 文章的点赞,点彩功能: 文章的评论功能 transaction用法: 自定义 标签代码展示: BBS_03 day: 自定义标签 过滤器: --&g ...
- [LeetCode] 857. Minimum Cost to Hire K Workers 雇佣K名工人的最低成本
There are N workers. The i-th worker has a quality[i] and a minimum wage expectation wage[i]. Now w ...
- C语言实现五子棋
可以称得上史上最简单的五子棋版本了. 可以使用curses库来改进页面和下棋方式. 并且对于输入的坐标没有进行鉴别,如果输入的坐标超过棋盘大小,就会段错误退出. 我改进了一点,但是还是没有完全避免这个 ...
- mysql出生日期转成年龄
可以直接用数据库函数进行转换,省去java代码转换的麻烦 SELECT TIMESTAMPDIFF(YEAR, '1988/01/10', CURDATE()) 且此函数容错很好,就算是null,‘ ...
- 第02组 Alpha冲刺(4/6)
队名:無駄無駄 组长博客 作业博客 组员情况 张越洋 过去两天完成了哪些任务 摸鱼 提交记录(全组共用) 接下来的计划 沟通前后端成员,监督.提醒他们尽快完成各自的进度 学习如何评估代码质量 准备Al ...
- Ubuntu 14.04 apt-get update失效解决(转)
现象如下: VirtualBox:~$ sudo apt-get update Err http://mirrors.aliyun.com trusty InRelease Err http://mi ...
- Java开发:字符串切割split函数——切割符转码注意事项
一.问题如下: 1.先对一个已有字符串进行操作,使用 ; 进行分割: //示例字符串 String string="sr1.db1.tb1.df1;sr2.db2.tb2.d ...
- [翻译 EF Core in Action 2.4] 加载相关数据
Entity Framework Core in Action Entityframework Core in action是 Jon P smith 所著的关于Entityframework Cor ...
- shell脚本注意点
1.等号两边不能有空格,例如: 获取七天前的日期: before_7_day=`date -d "7 days ago" +%Y-%m-%d` 2.自定义函数只能返回数值,不能返回 ...