Java集合ArrayList详解
文中源码均基于JDK1.8进行分析。
2.1 ArrayList
ArrayList是List接口的一个实现类。
2.1.1 ArrayList底层实现和构造函数
package java.util;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//该elementData是真正存放元素的容器,可见ArrayList是基于数组实现的
transient Object[] elementData; // non-private to simplify nested class access
private int size;
//ArrayList构造函数
//带初始容量参数的构造函数。(用户自己指定容量)
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
this.elementData = new Object[initialCapacity];//创建initialCapacity大小的数组
} else if (initialCapacity == 0) {//初始容量等于0
this.elementData = EMPTY_ELEMENTDATA;//创建空数组
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//ArrayList支持默认大小构造,和空构造,当空构造的时候存放数据的Object[] elementData是一个空数组{}
//默认构造函数,使用初始容量10构造一个空列表(无参数构造)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造包含指定Collection元素的列表,这些元素利用该集合的迭代器按顺序返回,如果指定的集合为null,throws NullPointerException。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
}
从以上源代码中可以看出ArrayList是基于数组实现的,以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。
2.1.2 ArrayList扩容机制
当初始化的list是一个空ArrayList的时候,会直接扩容到DEFAULT_CAPACITY,该值大小是一个默认值10。而当添加进ArrayList中的元素超过了数组能存放的最大值就会进行扩容。下面将分步骤来详细讲解ArrayList扩容机制,以无参构造函数创建的 ArrayList 为例进行分析。
首先看一下添加元素的方法
//将指定的元素追加到此列表的末尾
public boolean add(E e) {
//添加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
再看add 方法 调用的ensureCapacityInternal(size + 1)方法
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
// 当要add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
再看ensureExplicitCapacity() 方法
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
》当我们要 add 进第1个元素到 ArrayList 时,elementData.length为0(因为还是一个空的list),
因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为10。
》此时,minCapacity - elementData.length > 0 成立,所以会执行grow(minCapacity)方法。
当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成10了。
》此时,minCapacity - elementData.length > 0 不成立,所以不会执行grow(minCapacity)方法。
》当添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。
》直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大,
此时进入grow方法进行扩容。
*/
再看grow(int minCapacity)方法
//要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// ArrayList扩容的核心方法
private void grow(int minCapacity) {
//oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//采用右移运算,就是原来的一半,所以扩容1.5倍。比如10的二进制是1010,右移后变成101就是5
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,就把最小需要容量作为数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,则执行hugeCapacity()方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
》当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
》当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
》以此类推····
*/
最后看hugeCapacity() 方法
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
最后说一下扩容机制中还有一个ensureCapacity方法
这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,这个方法的作用是:向 ArrayList 添加大量元素之前最好先使用ensureCapacity 方法,以减少增量重新分配的次数。
/**
如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
2.1.3 ArrayList中arraycopy()方法实现数组复制
阅读源码的话,我们就会发现 ArrayList 中大量调用了System.arraycopy() 和 Arrays.copyOf()这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)、toArray() 等方法中都用到了该方法。
//在此列表中的指定位置插入指定的元素。
//先调用 rangeCheckForAdd 对index进行界限检查;
//然后调用 ensureCapacityInternal 方法保证capacity足够大;
//再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()方法实现数组自己复制自己
//elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
java是无法自己分配空间的,是底层C和C++的实现。以C为例,我们知道C中数组是一个指向首部的指针,比如我们C语言对数组进行分配内存。Java就是通过arraycopy这个native方法实现的数组的复制。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
copyOf() 内部实际调用了 System.arraycopy() 方法
// 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);
//返回的数组的运行时类型是指定数组的运行时类型。
public Object[] toArray() {
//elementData:要复制的数组;size:要复制的长度
return Arrays.copyOf(elementData, size);
}
二者区别:arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 ,copyOf() 是系统自动在内部新建一个数组,并返回该数组。
2.1.4 ArrayList实现序列化安全
为什么中elementData用transient修饰?
transient Object[] elementData; // non-private to simplify nested class access
(1)transient的作用是该属性不参与序列化。
(2)ArrayList继承了标示序列化的Serializable接口。
(3)对arrayList序列化的过程中进行了读写安全控制。
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();
}
}
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,deserialize it).
*/
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
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, 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();
}
}
}
在序列化方法writeObject()方法中可以看到,先用默认写方法,然后将size写出,最后遍历写出elementData,因为该变量是transient修饰的,所有进行手动写出,这样它也会被序列化了。那是不是多此一举呢?
protected transient int modCount = 0;
当然不是,其中有一个关键的modCount, 该变量是记录list修改的次数的,当写入完之后如果发现修改次数和开始序列化前不一致就会抛出异常,序列化失败。这样就保证了序列化过程中是未经修改的数据,保证了序列化安全。(java集合中都是这样实现)
参考博文:
https://binhao.blog.csdn.net/article/details/113279914
https://www.cnblogs.com/baichunyu/p/12965241.html