【源码解析】- ArrayList源码解析,绝对详细
ArrayList源码解析
简介
ArrayList
是Java
集合框架中非常常用的一种数据结构。继承自AbstractList
,实现了List
接口。底层基于数组来实现动态容量大小的控制,允许null
值的存在。同时还实现了RandomAccess
、Cloneable
、Serializable
接口,支持快速访问、复制、序列化操作。
了解数组
数组简单来说就是将所有的数据排成一排存放在系统分配的一个内存块上,通过使用特定元素的索引作为数组的下标,可以在常数时间内访问数组元素的这么一个结构;
数组优缺点
优点
- 简单方便已使用
- 访问元素快
缺点
- 大小固定:数组的大小是静态的,在使用前必须确定好数组的大小
- 分配一个连续空间块:数组初始分配空间时,有时候无法分配能存储整个数组的内存空间(当数组规模太大时);
- 基于位置的插入操作实现复杂:如果要在数组中的给定位置插入元素,那么可能就会需要移动存储在数组中的其他元素,这样才能腾出指定的位置来放插入的新元素;而如果在数组的开始位置插入元素,那么这样的移动操作开销就会很大。
ArrayList解析
我们提到数组的特点是大小固定,ArrayList
的底层是基于数组来实现容量的大小动态变化的,那我们一起来结合源码看看,是如何实现这一功能的。
我们找到java.util.ArrayList
包查看代码。并通过注释的方式,一起来揭开面纱。
1、成员变量
// 默认的容量大小
private static final int DEFAULT_CAPACITY = 10;
// 空数组对象Object
private static final Object[] EMPTY_ELEMENTDATA = {};
// 有一个空数据对象Object
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 默认修饰且不参与序列化的 数组对象,也是实际存储数据的地方
transient Object[] elementData; // non-private to simplify nested class access
// 实际大小容量
private int size;
我们发现有两个一样的空数组对象,为什么要用两个呢?源代码中也进行来解释 We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when first element is added.
也就是说,这是一个共享的空数组实例,通过与默认的空数组区分开,好处是,添加元素时知道该 elementData
从空的构造函数还是有参构造函数被初始化的。以便确认如何扩容。
在AbstractList
父类中还有一个变量
protected transient int modCount = 0;
用来记录对List的操作次数。作用在使用Iterator
时,防止在迭代过程中集合被修改。
2、构造函数
无参数构造
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
默认的无参数构造中直接给elementData
赋值来一个空的数据。但我们看到注释上说初始容量为10的数组,这好像不太对啊。其实这只是一个延后的操作,当第一次添加数据进去时,容量会扩容到10,好处是避免无用的ArrayList
的出现。具体的实现我们接着往后看。
指定初始容量构造
public ArrayList(int initialCapacity) {
// 指定的容量大于0,直接new一个指定容量大小的数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 指定容量等于0。那就赋值空数组。
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 无效容量大小
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
这个还是比较清晰的,根据指定容量初始化一个数组。加入了容量大小的判断操作。
指定Collection集合构造
public ArrayList(Collection<? extends E> c) {
// 首先转成数组
Object[] a = c.toArray();
// 有效大小的数组哈
if ((size = a.length) != 0) {
// 这里做了优化,如果也是一个ArrayList集合直接赋值即可
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 其他的类型就做拷贝啦
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
简单点说其实就是把集合转成数组,然后赋值给elementData
。可能你看到的版本不一样,主要是在
c.getClass() == ArrayList.class
做了优化。如果也是ArrayList
的集合,那就不用做数组拷贝了,这个还是比较耗性能的。
主要操作函数解析
下面将主要的增删改操作进行分析
添加元素操作
单元素添加
public boolean add(E e) {
// 我们需要添加 一个元素,则需要判断+1后的容量是否需要扩容了,同时记录modCount
ensureCapacityInternal(size + 1); // Increments modCount!!
// 接在index后添加元素,并且更新当前集合大小size
// 可以理解成 index =size+1;elementData[index];size++
elementData[size++] = e;
return true;
}
// 提取方法哈,比较计算,确定容量大小
//private static int calculateCapacity(Object[] elementData, int minCapacity) {
// if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// return Math.max(DEFAULT_CAPACITY, minCapacity);
// }
// return minCapacity;
//}
// 做了简化,与calculateCapacity 一致
private void ensureCapacityInternal(int minCapacity) {
//ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
// 提取calculateCapacity方便可以做一个简化,可能你的版本就是这样
// 判断是否是空数组,如果是空数组,那么minCapacity = 10
// 这样就解答了我们前面提到的问题,在添加第一个元素时才将容量设置成10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity)
}
// 记录操作次数,然后判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
// 当添加一个元素后的容量大于当前元素个数,则需要扩容了
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容方法。minCapacity 添加一个元素后的容量
private void grow(int minCapacity) {
// overflow-conscious code
// 先记录当前容量,也就是元素的个数
int oldCapacity = elementData.length;
// 不管,先扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果扩容后的大小小于添加元素后的容量,则没必要了,直接用添加元素后的容量了
// 这里可以看到哈,如果是空构造,添加参数时,newCapacity是等于0的,然后再赋值了10。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 有最大容量限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 就是给一个最大的容量了
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 数组拷贝 底层还是System.arraycopy
elementData = Arrays.copyOf(elementData, newCapacity);
}
指定index添加元素
public void add(int index, E element) {
// 检查index
rangeCheckForAdd(index);
// 是否需要扩容操作,记录modCount
ensureCapacityInternal(size + 1); // Increments modCount!!
// 进行数组拷贝,给这个添加的元素腾出一个位置index
System.arraycopy(elementData, index, elementData, index + 1,size - index);
// 在这个位置index放置元素
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
我们看到哈,在添加元素的方法中,原则是计算容量,判断是否需要扩容,并记录修改次数。这里有一个非常重要的方法就是数组拷贝。扩容需要拷贝,在指定index中放入元素也需要拷贝哈。下面我们通过画图的方式来解释一下System.arraycopy
这个函数
System.arraycopy详解
这是System
类中的一个静态方法,用来实现数组之间的复制,具体的实现我们就不去了解,我们主要来看看他的用法,以及在ArrayList
是怎么使用的
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
参数说明
src:the sourse arr
源数组srcPos:starting position in the source array.
源数组的起始位置dest:the destination array.
目标数组destPos:starting position in the destination data.
目标数组的起始位置length:the number of array elements to be copied.
复制的长度
举几个例子
// 给定数组
int[] src = {1,2,3,4,5};
// 给定目标数组
int[] dest = new int[src.length]
// 要求1 将src 数组全部复制到dest中
System.arraycopy(src,0,dest,0,src.length);
// 要求2 将src的前三位数复制到dest中的后三位
System.arraycopy(src,0,dest,2,src.length-2);
数组拷贝图解
grow扩容拷贝
假定当前我们的集合元素已经有10个了,此时还需要添加一个元素。会经历这样的操作。
1、判断需要扩容,新的容量为15的数组。
2、将源数组拷贝到新的数组中,重新复制给elementData
;
3、在index=10
的位置添加元素
add index 移动拷贝
在集合中已经有了5个元素了。现在需要在index=1
的位置插入一个新的元素,可以理解成插队。
1、判断是否需要扩容。这里发现不需要
2、System.arraycopy(elementData, index, elementData, index + 1,size - index);
以图中为例,我们需要将index 1、2、3、4
整体往后挪动,就像有人插队一样,插入的位置后面整体后移了一位。index=0
的位置是不用动的。这里的写法应该是
System.arraycopy(elementData, 1, elementData, 1 + 1,5 - 1);
3、将index=1
的位置重新赋值,原来index=1
的位置已经被移到现在的index=2
的位置了。
详细的流程可以通过代码的方式观察即可理解这个过程。
移除元素操作
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 指定index 删除其索引位置的元素并返回
public E remove(int index) {
// 老规矩,检查index的边界
rangeCheck(index);
// 记录操作次数
modCount++;
// 找到这个待删除的元素,主要用户返回
E oldValue = elementData(index);
// 需要移动的数量
int numMoved = size - index - 1;
// 如果需要移动 ,如果只从后面删除的话,例如 size=5 index = 4 ,那么numMoved=0
if (numMoved > 0)
// 进行数组拷贝移动,填上那个空位置
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 然后把尾巴多出来的那个元素删掉啦
elementData[--size] = null; // clear to let GC do its work
// 返回
return oldValue;
}
// 删除指定的对象
public boolean remove(Object o) {
// ArrayList元素允许为空的
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 比较元素,然后找到其所在的index 交由fastRemove通过index移除。与remove(index)
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 快速删除 和remove(index) 基本一致 ,在数组index的操作是高效,通过index去操作
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
// 全部删除了
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
这里我们会发现一个问题啊,我们在静态的数组中进行index
所在数据的删除时,一般是直接对 arr[index] = 0;
直接对索引位置的元素进行null
赋值。但在ArrayList
中就不一定是这样了,他一直都是对最后一位元素进行操作elementData[size—] = null;
我们来画个图看一下
例如我们要对上图中index=1
的位置元素进行remove
操作,怎么做呢?
1、找到index 2、3、4、5
需要移动的元素。
2、将他们整体往前移动一位。这个时候需要删除的元素已经被覆盖了
3、再将最后一个删除。(真正移除的那个元素其实和前面一位一样哦)
整体下来发现和add(E e,int index)
整个流程好像正好相反,哈哈!
修改元素操作
public E set(int index, E element) {
// index 检查
rangeCheck(index);
// 找到旧元素
E oldValue = elementData(index);
// 替换所在位置的元素
elementData[index] = element;
return oldValue;
}
这个还是比较简单的。可以理解成是一个替换的操作就可以了。
查询操作
// 指定index 返回其所在的元素
public E get(int index) {
// 边界检查
rangeCheck(index);
// 返回,这个简单,索引快速定位
return elementData(index);
}
// 从前往后查询,第一次出现的位置index
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
// 从后往前查询,第一次出现的位置index
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
查询操作就简单了很多哈。基本上都是基于索引来访问的。
到这里我们已经总结了很多常用的方法,在ArrayList
中还有非常多的方法,例如迭代器Iterator
、suList
操作等等。这里就不过多进行解析了,不过后面会通过专门的篇幅来介绍迭代器Iterator
和为什么不能在for
遍历集合时对集合进行remove
操作,有时还会抛出异常ConcurrentModificationException
。
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
这里有一个我们非常熟悉的变量modCount
。详细的后面在来解析把。
【源码解析】- ArrayList源码解析,绝对详细的更多相关文章
- 源码解析 || ArrayList源码解析
前言 这篇文章的ArrayList源码是基于jdk1.8版本的源码,如果与前后版本的实现细节出现不一致的地方请自己多加注意.先上一个它的结构图 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 ...
随机推荐
- NGK公链脱颖而出,成为值得期待的项目!
当下2020年是动荡的一年,全世界经济危机汲汲可危,在这个特殊的时刻,有人抱怨说这是最坏的年代,也有人庆幸说这是最好的年代,历史不会重演,但总是惊人的相似,首先带你回顾一下上一次金融危机出现的2008 ...
- NGK和USDN的应用
一.NGK和USDN的发展方向 目前区块链将会朝着两个方向去发展,第一种是金融经济的衍生品,第二种是商业应用,快速支付的货币体系,NGK.IO公链是基于分布式应用设计的商用金融区块链操作系统,通过数字 ...
- 使用 mask 实现视频弹幕人物遮罩过滤
经常看一些 LOL 比赛直播的小伙伴,肯定都知道,在一些弹幕网站(Bilibili.虎牙)中,当人物与弹幕出现在一起的时候,弹幕会"巧妙"的躲到人物的下面,看着非常的智能. 简单的 ...
- 【HTB系列】靶机Teacher的渗透测试详解
出品|MS08067实验室(www.ms08067.com) 本文作者:大方子(Ms08067实验室核心成员) Kali: 10.10.14.50 靶机地址:10.10.10.153 先用nmap 对 ...
- 错误信息:...\output\project.axf: error: l6218e: undefined symbol usart1_confing (referred from main.o).
说明:此文档知识用来记录,顺便给大家作为参考,如有错误的地方请大家多多指正,在下内心定会感激不尽. 前言:关于这个问题,我曾花了一个下午在网上寻找,网上的说法五花八门,我办法试尽,但遍寻无果.由此我认 ...
- ng-class动态类几种用法
方法1.逻辑在后面的中括号里面 ng-class="{true : 'checker disabled',false : 'checker' }[selectAllButton]" ...
- JVM之类加载器子系统
类加载器子系统 作用 负责从文件系统或网络系统中加载class文件,class文件在开头有特殊的标记(魔术开头CA FE BA BE) ClassLoader只负责加载class文件,至于能否运行,由 ...
- 最简单的,在win,linux中,用powershell,自动获取Let's Encrypt证书方法
powershell传教士原创 2020-04-12 Let's Encrypt证书有效期3个月,支持泛域名[*.你的网站.net].支持n天内(一般10天内就够用了),用脚本自动续期. 简介: 这个 ...
- dapr学习:dapr介绍
该部分主要是给出学习dapr的入门,描述dapr全貌告诉你dapr是啥以及介绍dapr的主要功能与组件 该部分分为两章: 第一章:介绍dapr 第二章:调试dapr的解决方案项目 1. 介绍dapr ...
- Hadoop的常用命令
注:hadoop的使用命令包含 hadoop fs 开头 or hdfs dfs开头 等多种形式来操作. 这里以hadoo fs的形式来介绍在这些命令 1.列出根目录下所有的目录或文件 hadoop ...