ArrayList集合源码解读(一)
前言
笔者在阅读网上众多的ArrayList源码解读时发现他们都是以1.8版本的来进行讲解,并且很多都是囫囵吞枣,看的人一脸懵逼。
其实现在的很多公司都换成了17版本的jdk。笔者决定自己写一个ArrayList集合源码的解读,笔者将直接通过17版本的jdk深入底层的带大家学习ArrayList。17版本的jdk大部分代码和1.8是一样的,只要极少部分存在漏洞的代码进行了优化。所以大家学完17版本的后,1.8版本的肯定都可以看懂,并且各位还能发现1.8的一些实现漏洞。
ArrayList
底层是基于 Object[]
数组实现的。与 Java
中的静态数组相比,它的容量能动态增长。可以理解为 ArrayList
是一种动态数组。那么今天我们就来阅读下源码,看看 Java
是如何实现这个动态数组的集合。
注意:ArrayList
适用于频繁的查找工作,是一个线程不同步的集合,这点他与 LinkedList
相同。
首先,我们照例看一下 ArrayList
的类定义
public class ArrayList<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
一. 继承和实现关系
ArrayList
类继承了 AbstractList
抽象类。
- 我们可以点进源码看一下作者对
AbstractList
抽象类的描述:此类提供了List接口的骨架实现,以最大限度地减少实现此接口所需的工作量。
ArrayList
实现了 List
, RandomAccess
, Cloneable
, Serializable
这些接口。
-
List
: **表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 ** -
RandomAccess
:这是一个标志接口,表明实现这个接口的List
集合支持 快速随机访问 。- 什么是快速随机访问? -> 可以通过索引下标快速访问元素就叫做快速随机访问
-
Cloneable
:Cloneable
是一个标记接口,我们点进去会发现它并没有任何方法。此接口表明ArrayList
具有拷贝能力,可以进行深拷贝或浅拷贝操作 。package java.lang; public interface Cloneable { }
-
Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
二. 核心源码解读
笔者以 JDK17 为例,分析一下 ArrayList
的底层源码。
2.1. 属性及构造函数解读
ArrayList
提供了三个构造方法帮助我们创建对象。我们来详细分析下这三个构造方法实现的底层逻辑。
ArrayList(int initialCapacity)
:通过给定的initialCapacity
创建集合对象。ArrayList()
:无参构造方法,创建一个初始值为空数组的集合对象。- 注意: 以无参数构造方法创建
ArrayList
时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
- 注意: 以无参数构造方法创建
ArrayList(Collection<? extends E> c)
:传入一个集合,构造一个包含此集合中所有元素的集合对象。
源码:
// 默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于空实例)
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小空实例的共享空数组实例。
//我们把它从 EMPTY_ELEMENTDATA 数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 保存ArrayList数据的数组
transient Object[] elementData; // non-private to simplify nested class access
// 元素个数
private int size;
/**
* 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果传入的参数大于0,创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 传入的值为 0,则创建一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else { // 不合规,报错
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 默认无参构造函数
*/
public ArrayList() {
// 初始化为一个空数组
// 在添加第一个元素时,会将容量扩容为 10 (后面笔者会专门讲解扩容源码,各位有个印象即可)
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 传入一个集合,构造一个包含此集合中所有元素的列表,按照它们由集合的迭代器返回的顺序。
*/
public ArrayList(Collection<? extends E> c) {
// 将指定集合转换成数组
Object[] a = c.toArray();
// (size = a.length) != 0 做了两个操作
// 1.将 a.length 的值赋值给 size
// 2.判断 size 是否不等于 0
if ((size = a.length) != 0) {
// 如果 elementData 是 ArrayList 类型数据,就直接赋值
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 如果不是则转换成 Object[] 数组再赋值(因为 ArrayList 底层是 Object[] 数组)
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 如果给定集合 c 的长度为 0,则替换成空数组
elementData = EMPTY_ELEMENTDATA;
}
}
2.2 核心扩容方法解读
ArrayList
的扩容机制提高了性能,如果每次只扩充一个,那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList
的扩容机制避免了这种情况。
源码:
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
*
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length // 要保证传入的 minCapacity 大于当前集合中数据长度
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA // elementData不能为空
&& minCapacity <= DEFAULT_CAPACITY)) { // 传入的 minCapacity 要大于默认长度10
modCount++; // 版本号控制
// 扩容底层实现方法,调用此方法代表已经开始扩容了
grow(minCapacity);
}
}
/**
* 增加容量,以确保它至少可以容纳最小容量参数指定的元素数量
*
* @param minCapacity 所需的最小容量
* @throws OutOfMemoryError 如果 minCapacity 小于 0,抛出异常
*/
private Object[] grow(int minCapacity) {
// 获取当前集合中已有的元素个数
// oldCapacity为旧容量,newCapacity 为新容量
int oldCapacity = elementData.length;
// 如果 oldCapacity > 0 或者 集合中的元素不为空数组,则进行扩容
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 扩容的主体代码,其实简单来说就是将新数组扩容成老数组的 1.5 倍
// 大家要知道位运算的速度远远快于整除运算,所以看似炫技的写法 oldCapacity >> 1 其实是为了性能考虑
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, // 判断要增加几个元素位置
oldCapacity >> 1); // >> 1 等价于 / 2 eg: 5 >> 1 = 5 / 2 = 3
// 将原数据拷贝到新数组长度的数组中
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
// 进入 else 则证明我们的 oldCapacity <= 0 或者 elementData 为u空数组,则进行下方的操作
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
/**
* 要分配的最大数组大小
*/
public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
/**
* 根据给定的参数返回一个新长度
*
* @param oldLength 数组的当前长度(必须为非负)
* @param minGrowth 所需的最小增长量(必须为正)
* @param prefGrowth 首选增长量
* @return 新数组长度
* @throws OutOfMemoryError 如果新长度超过 Integer.MAX,报错
*/
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// 大家看上方的官方解释可能很懵逼。什么是所需的最小增长量?什么是首选增长量?
// 其实通俗的理解,就是比较 当前数组的长度的一半 (oldLength >> 1) 和 我们期望增加的长度 (minCapacity - oldCapacity) 谁更大,就用谁
int prefLength = oldLength + Math.max(minGrowth, prefGrowth);
// 合法性检测 SOFT_MAX_ARRAY_LENGTH:要分配的最大数组大小
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// else 中的逻辑,此处涉及不到,感兴趣的同学可以自己去研究下
return hugeLength(oldLength, minGrowth);
}
}
2.3 常用方法解读
未完待续~