一 前言
最近去2家大厂面试被虐了,被虐也在意料之中,自己也没有怎么准备,决定回来好好学习,注重细节,多读源码,切勿眼高手低。给我最大感觉是大厂特别重视基础知识,对这些基础知识的深入理解,我们平时做项目的时候,都是会用一些容器,控件,框架,对源码有点了解,但是没有深入解读,半懂不懂,这样在面试官对知识点的层层深入时,就力不从心,结局你懂得,所以我们还是要多注重细节,多读源码。
不说那么多了,我们一起来看看ArrayList源码的源码吧。
二 源码解读
1. ArrayList概述
ArrayList是一个动态数组,与Java中的数组相比,他的容量可以动态增加(1.5倍),我们来看一下ArrayList特点。
- 快速随机访问
- 允许存放多个null元素
- 底层存储是个Object数组
- 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),对于元素随机访问get和set比较快。
- 线程不安全
2. ArrayList继承关系
先看一下源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- ArrayList继承于AbstractList,AbstractList继承AbstractCollection实现了List接口。
- 实现了List接口,那么ArrayList元素是有序的,可以重复的,可以有null元素的集合。
- 实现了RandomAccess接口,标识着其支持随机快速访问,实际上,查看RandomAccess源码可以看到,是空的啥都没有.ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1)。
- 实现了Cloneable接口,ArrayList里面的clone()复制其实是浅复制。
- 实现java.io.Serializable接口,即ArrayList支持序列化,能通过序列化去传输和存储。
3. ArrayList全局变量
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
// 序列化id
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始的容量
private static final int DEFAULT_CAPACITY = 10;
// 一个空对象
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
// 当前数据对象存放地方,当前对象不参与序列化
transient Object[] elementData; // 注意点
// 当前数组长度
private int size;
// 数组最大长度
private static final int MAX_ARRAY_SIZE = 2147483639;
}
我们看到 transient Object[] elementData;
前面有个修饰符 transient
,transient
标识之后是不被序列化 。
elementData
就是ArrayList
的存储容器,为什么不能被序列化?这个地方有点疑惑。
我们在看ArrayList
源码中可以看到这个2个方法
// 序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
// 反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
ArrayList
在序列化的时候会调用writeObject()
,直接将size
和element
写入ObjectOutputStream
;反序列化时调用readObject()
,从ObjectInputStream
获取size
和element
,再恢复到elementData
。
为什么不用系统的系列化,自己去重写?
原因:主要是从效率考虑,elementData
是一个缓存数组,一般都是会比实际存储大一些,它通常会预留一些容量,等容量不足时再扩充容量(当前容量的1.5倍),那么有些空间可能就没有实际存储元素,采用上面的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
4. 构造函数
4.1 无参构造方法
如果不传入参数,则使用默认无参构建方法创建ArrayList对象
/**
* Constructs an empty list with an initial capacity of ten.
* 构造一个初始容量为10的空列表。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
我看一下DEFAULTCAPACITY_EMPTY_ELEMENTDATA
成员变量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
此时我们创建的ArrayList
对象中的elementData
中的长度是1,size是0,elementData指向了一个空数组,为什么注释却说初始容量为10。
原因在于当进行第一次add的时候,elementData
将会变成默认的长度:10。后面我们会具体分析。
4.2 指定初始容量的构造方法
public ArrayList(int initialCapacity) {
// 如果初始化初始化容量大于0
if (initialCapacity > 0) {
// new一个该大小的object数组赋给elementData
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { // 如果大小为0
// 将空数组赋给elementData
this.elementData = EMPTY_ELEMENTDATA;
} else { // 小于0
// 则抛出IllegalArgumentException异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
注意事项:如果我们知道ArrayList
存储的个数时候推荐使用这个构造方法,避免使用ArrayList的扩容机制而带来额外的开销.
4.3 带参数Collection的构造方法
/**
* 构造一个包含指定集合元素的列表,元素的顺序由集合的迭代器返回。
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray 可能(错误地)不返回 Object[]类型的数组 参见 jdk 的 bug 列表(6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果集合大小为空将赋值为 EMPTY_ELEMENTDATA 空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
参数c为一个Collection,Collection的实现类大概有以下几种常用类型:
List:元素可以重复的容器
Set: 元素不可重复的容器
Queue:结构是一个队列,先进先出
构造方法解析
- 将collection对象转换成数组,然后将数组的地址的赋给elementData。
- 更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData。
- 如果size的值大于0,c.toArray 可能(错误地)不返回 Object[]类型的数组 参见 jdk 的 bug 列表(6260652),则执行Arrays.copy方法,把collection对象的内容copy到elementData中。
5. 重要方法
5.1 增加元素和扩容机制
5.1.1 add(E e)方法
作用: 添加指定元素到末尾
/**
* 添加指定元素到末尾
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
如果是以ArrayList()无参构造方法初始化,那么数组指向的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
.第一次add()元素会进入if内部,且minCapacity为1,那么最后minCapacity肯定是10,所以ArrayList()构造方法上面有那句很奇怪的注释.
下面看一下 ensureExplicitCapacity(minCapacity);
方法。
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
//列表结构被修改的次数,用于保证线程安全,如果在迭代的时候该值意外被修改,那么会报ConcurrentModificationException错
modCount++;
// 如果最小需要空间比elementData的内存空间要大,则需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
下面我们来分析扩容机制grow(minCapacity)
。
/**
* 扩容
*/
private void grow(int minCapacity) {
// overflow-conscious code
//1. 记录之前的数组长度
int oldCapacity = elementData.length;
//2. 新数组的大小=老数组大小+老数组大小的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
//3. 判断上面的扩容之后的大小newCapacity是否够装minCapacity个元素,不够就将数组长度设置为需要的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//4.判断新数组容量是否大于最大值 如果新数组容量比最大值(Integer.MAX_VALUE - 8)还大,那么交给hugeCapacity()去处理,该抛异常则抛异常
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//5. 复制数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
//巨大容量
private static int hugeCapacity(int minCapacity) {
//判断是否溢出,溢出扔出一个小错误
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 我们看一下Integer.MAX_VALUE 和MAX_ARRAY_SIZE,MAX_ARRAY_SIZE 就是获取Java中int的最大限制,以防止越界
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public static final int MAX_VALUE = 0x7fffffff;
add主要的执行逻辑如下:
1)如果原数组是空的,那么第一次添加元素时会给数组一个默认大小10。
确保数组已使用长度size
加1之后是否还有容量存储下一个数据。
2)修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的长度变为原来容量的1.5倍。
3)确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
4)返回添加成功布尔值。
5.1.2 add(int index, E element)
作用:在ArrayList的index位置,添加元素element
public void add(int index, E element) {
// 1. 判断index是否越界
rangeCheckForAdd(index);
//2. 判断是否需要扩容
ensureCapacityInternal(size + 1);
//3. 将elementData从index位置开始,复制到elementData的index+1开始的连续空间
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 4.在elementData的index位置赋值element
elementData[index] = element;
// 5. 记录当前真实数据个数
size++;
}
//index不合法时,抛IndexOutOfBoundsException
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
1)确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
2)确保数组已使用长度(size)加1之后足够存下 下一个数据
3)修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组
4)grow方法会将当前数组的长度变为原来容量的1.5倍。
5)确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。
6)将新的数据内容存放到数组的指定位置(index)上
5.1.3 添加集合到末尾 addAll(Collection
public boolean addAll(Collection<? extends E> c) {
//1. 生成一个包含集合c所有元素的数组a
Object[] a = c.toArray();
//2. 记录需要插入的数组长度
int numNew = a.length;
//3. 判断一下是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//4. 将a数组全部复制到elementData末尾处
System.arraycopy(a, 0, elementData, size, numNew);
//5. 标记当前elementData已有元素的个数
size += numNew;
//6. 是否插入成功:c集合不为空就行
return numNew != 0;
}
5.1.4 添加集合到指定位置 addAll(int index, Collection
public boolean addAll(int index, Collection<? extends E> c) {
//1. 首先检查一下下标是否越界
rangeCheckForAdd(index);
//2. 生成一个包含集合c所有元素的数组a
Object[] a = c.toArray();
//3. 记录需要插入的数组长度
int numNew = a.length;
//4. 判断是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
//5. 需要往后移的元素个数
int numMoved = size - index;
if (numMoved > 0) //后面有元素才需要复制哈,否则相当于插入到末尾
//6. 将elementData的从index开始的numMoved个元素复制到index + numNew处
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//7. 将a复制到elementData的index处
System.arraycopy(a, 0, elementData, index, numNew);
//8. 标记当前elementData已有元素的个数
size += numNew;
//9. 是否插入成功:c集合不为空就行
return numNew != 0;
}
5.2 删除元素
5.2.1 移除指定位置元素 remove(int index)
public E remove(int index) {
//1. 检查参数是否合法
rangeCheck(index);
modCount++;
//2. 记录下需要移除的元素
E oldValue = elementData(index);
//3. 需要往前面挪动1个单位的元素个数
int numMoved = size - index - 1;
if (numMoved > 0) //后面有元素才挪动
//4. 将index后面的元素(不包含index)往前"挪动"(复制)一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//5. 这里处理得很巧妙,首先将size-1,然后将elementData原来的最后那个元素赋值为null(方便GC回收)
elementData[--size] = null; // clear to let GC do its work
//6. 将旧值返回
return oldValue;
}
//检查参数是否合法 参数>size抛出IndexOutOfBoundsException 参数小于0则抛出ArrayIndexOutOfBoundsException
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
5.2.2 移除指定元素 remove(Object o)
public boolean remove(Object o) {
//1. 是否为null
if (o == null) {
//2. 循环遍历第一个为null的元素
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//3. 移除 移除之后就返回true
fastRemove(index);
return true;
}
} else {
//4. 循环遍历第一个与o equals()的元素
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
//5. 移除指定位置元素
fastRemove(index);
return true;
}
}
return false;
}
/*
私有的方法,移除指定位置元素,其实和remove(int 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
}
5.3 改动元素(替换指定下标的元素内容) set(int index, E element)
public E set(int index, E element) {
//1. 检查是否越界
rangeCheck(index);
//2. 记录原来该index处的值
E oldValue = elementData(index);
//3. 替换
elementData[index] = element;
return oldValue;
}
5.4 查询元素(返回指定位置处元素) set(int index, E element)
E elementData(int index) {
return (E) elementData[index];
}
/**
* 返回指定位置处元素
*/
public E get(int index) {
// 检查是否越界
rangeCheck(index);
return elementData(index);
}
5.5 清空列表(清空当前集合的所有元素) set(int index, E element)
public void clear() {
modCount++;
// clear to let GC do its work
// 就是将数组所有元素都置为null,然后GC就有机会去把它回收了
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}