又是热爱学习的一天… 今天准备学习一下 ArrayList 的源码,研究它都干了什么。
总结 (先看结论,再寻找如何得出的此结论)
-
ArrayList 是一种以数组实现的 List,与数组相比,它具有动态扩展的能力,因此也可称之为 动态数组。
-
ArrayList 实现了 List,提供了基础的添加、删除、遍历等操作。
-
ArrayList 实现了 RandomAccess,提供了随机访问的能力。
-
ArrayList 实现了 Cloneable,可以被克隆。
-
ArrayList 实现了 Serializable,可以被序列化。
1. ArrayList 的成员变量
先看看 ArrayList 的成员变量,我把它的每一个成员变量都加了注释用以解释这个变量有何作用。
/**
* 默认初始容量,也就是说,使用 new ArrayList() 创建的 ArrayList ,它的初始容量为 DEFAULT_CAPACITY;
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组,使用 new ArrayList(0) 创建 ArrayList 时,使用的数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 空数组,使用 new ArrayList() 创建 ArrayList 时,使用的数组。
* 在添加第一个元素的时候,会将这个数组的容量初始化为 DEFAULT_CAPACITY 大小
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存放真正的元素数据的数组
* 在添加第一个元素的时候,会使 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组容量初始化为 DEFAULT_CAPACITY 大小,这和上面那句话对应起来了。
* 疑问?这里加上 transient 关键字的意思应该是为了不让序列化这个数组里面的内容,也就是我们存进 ArrayList的真实数据,可是经过测验,却可以序列化该数组里面的数据。
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的实际元素个数,而不是 ArrayList 的长度
* 它和 elementData.length 的区别是,size是实际元素个数,elementData数组里,可能有4个实际元素,6个空元素,所以 elementData.length,代表了 elementData 的长度,它里面包含了空元素。
*/
private int size;
2. ArrayList 的构造函数
2.1 指定容量构造函数:ArrayList(int initialCapacity)
/**
* 传入初始容量,如果容量大于0,就将 elementData 初始化为对应大小。如果等于0,那就使用空数组:EMPTY_ELEMENTDATA。如果小于0,就抛异常了。
* @param initialCapacity 传入指定的容量
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
2.2 默认构造函数:ArrayList()
/**
* 如果不传参数,那就使用空数组:DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
* 目前 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量为0,要等到添加第一个元素时。它才会初始化为 DEFAULT_CAPACITY 的大小。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.3 传入集合构造函数:ArrayList()
/**
* 传入一个 Collection 集合,并调用 toArray() 方法将它里面的内容初始化给 elementData。
* 然后再判断元素个数是否为0,如果为0,就将 elementData 初始化为 EMPTY_ELEMENTDATA 这个空数组
* @param c
*/
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;
}
}
这里请注意!! 该构造方法中写道:c.toArray might (incorrectly) not return Object[] (see 6260652)
意思是: c.toArray方法返回的可能不是 Object[] 类型,详情见JDK bug编号 6260652
那么这里的 if 判断是说,如果 elementData 不是 Object[] 的类型,就通过 copyOf 这个方法将不是 Object[] 的类型的 elementData 数组(它有可能是String[],int[]…不管它是什么,暂时不管),转换成 Object[] 类型。
这句话有点绕,简单来说就是:如果 elementData 如果不是 Object[] 的类型,那就通过 copyOf 方法把它转换成 Object[] 的类型
2.4 疑问:为什么 elementData 会可能不是 Object[] 的类型呢?
为什么这里会有这一步操作呢?为什么 elementData 会不是 Object[] 的类型呢?下面举例子解释:
我们首先来看一下这个构造器里面的内容,我们聚焦在第二层的这个 if 判断上,它判断了 elementData.getClass() 是否等于 Object[].class。那么,elementData 是怎么来的呢?构造器第一句就告诉我们了,他是 Collection 类型的参数 c 调用toArray方法初始化来的。 也就是说,这个 if 判断的是 c.toArray 方法返回的类型是否是 Object[] 类型。
先理解上面这段说明,下面就举一个栗子来分析说明:
// 1、首先使用工具类 Arrays 的 asList 方法,将数组转换成 List。
List<String> myList = Arrays.asList("123456", "ABCDEF");
// 2、模拟进入构造器,构造器需要 Collection 类型的数据,为什么这里传 List 也可以呢?
//因为 ArrayList 实现与 List 接口,List 接口又继承于 Collection 类,所以 ArrayList 也算是 Collection 的子类。所以这里传 List 也 OK。
List<String> arrayList = new ArrayList<>(myList);
// 这里的 myList 就是构造器里面的 c 变量。 myList 为实参, c为形参。
// 根据上面的理解,如果 c.toArray方法返回的类型,也就是这里的 myList.toArray 方法返回的类型不为 Object[] 类型,那就做转换动作,那么 myList 的类型到底是什么呢?
System.out.println(myList.toArray());
打印结果为:
[Ljava.lang.String;@27c170f0
结果说明 c.toArray方法返回的类型 还真有不是 Object[] 类型的,所以构造器里面存在这个if判断操作。
这个时候,可能有同学要问了:
- 为什么 myList.toArray 方法返回的不是 Object[] 类型呢???
- 在例子中 myList 已经是一个 List 了,我为什么不直接用它呢,为什么还要把它当作参数再创建新的ArrayList呢,我直接用
myList 不就得了吗?
2.5 解答问题1:为什么 myList.toArray 方法返回的不是 Object[] 类型呢???
首先看例子中,我们是使用工具类 Arrays 的 asList 方法,将字符串数组转换成 List。那么我们点进 asList 方法看一下源码:
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new Arrays.ArrayList<>(a);
}
可以看见它只不过是 new 了一个 Arrays.ArrayList<>(a) 对象而已。
那么这个 Arrays.ArrayList<> 是个什么东西呢?它其实就是 Arrays 类里面的一个内部类,它就在 asList 方法的下面,源码中他们紧挨着。部分代码如下:
Arrays.ArrayList<> 的源码
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
......
也就是说 Arrays.asList 方法返回的是 Arrays 的一个内部类,它继承于 AbstractList,所以它返回的并不是 java.util.ArrayList。
现在知道 Arrays.asList 方法返回的是什么之后,再来看为什么它调用 toArray() 方法之后,返回的不是 Object[] 类型。
现在将目光聚焦在我贴出来的 Arrays.ArrayList 这个内部类的部分源码,看最后一个方法,这是啥!!!!!其实他重写了父类的toArray()方法,所以当我们在使用 Arrays.asList 方法创建出来的对象的 toArray 方法时,调用的是他自己重写的方法!!
现在再看他是如何重写的,它是 clone 了一份 成员变量 a 的数据,a 是我们创建对象时传进来的,一直往上追,可以发现 a 里面就是我们用例中传的字符串数组,如果我们用例中写的两个int类型的数据,那么此时的 a 就会是这个int数组。所以 toArray 方法其实返回的是一个数组,它并不是一个真正的List。
现在明白为啥例子中打印出来是字符串数组类型的,而不是 Object[] 类型了吧。
2.6 解答问题2:在例子中 myList 已经是一个 List 了,我为什么不直接用它呢,为什么还要把它当作参数再创建新的ArrayList呢,我直接用 myList 不就得了吗?
满足愿望,那就先使用 myList 增加一个元素试试:
myList.add("HyugaNeji");
结果显示:
java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at AsListTest.main(AsListTest.java:25)
结果是抛异常了,原因是 myList 的类型是 Arrays.ArrayList 类型的,它自己没有实现 add 方法,但是它继承的父类 AbstractList 实现了,所以这里调用 add ,是调用到了它的父类的 add 方法,而它的父类是实现了 add 方法的,其内容如下:
public boolean add(E e) {
add(size(), e);
return true;
}
发现这个方法又调用了 另一个 add 方法,继续往下看,在 147 行的位置发现了这个方法:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
结果已经很明显了,它的实现方法就是抛异常…
所以,Arrays.asList 创建的所谓的List,是不可以使用 add、set、remove这些方法的,既然这些方法都不能用,我要他有何用,所以才会使用它作参数再创建新的ArrayList。所以才有了构造器里面的那段代码。
至于,Arrays.ArrayList为什么不直接返回一个 java.util.ArrayList 的原因,我现在暂时还没搞清楚,可能是设计模式层面的东西,因为它现在这样实现,不就是适配器模式吗?
一口气学习了这么多,可累坏了。歇会儿
3. 保证数组容量安全的核心辅助方法
先介绍几个保证数组容量安全的核心辅助方法
3.1 ensureCapacityInternal 方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
ensureCapacityInternal 方法的意思是 确保 ArrayList 内部容量的意思。如果容量不够装了就进行扩容,确保容量。
它的实现是调用了两个方法,它通过调用 calculateCapacity 方法,拿到返回值,再传给 ensureExplicitCapacity 方法。下面我们先看 calculateCapacity 方法做了什么。
3.2 calculateCapacity 方法
calculateCapacity 方法,看名字就知道它是 计算容量的,它的目的是返回 ArrayList 要存放数据的最小的目标容量。
它通常在 add 数据的时候被使用到,参数 minCapacity 的意思是存放数据需要最小的容量,它是: ArrayList 的实际存放元素个数 + 新增的个数
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
首先判断 elementData 的引用和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的引用是否是相同的,也就是判断是否调用了无参构造器。
因为如果调用了无参构造器,那么 elementData 的容量 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的容量 == DEFAULT_CAPACITY(原因详见构造器函数)
如果是相同的,那么 elementData 的初始容量就是 DEFAULT_CAPACITY,所以比较 DEFAULT_CAPACITY 和 minCapacity ,谁大返回谁。
换句话说就是:如果调用了无参构造器,那么最小容量最小就是10。这个10容量,可能是几个实际元素 + 几个空元素;
如果不是调用的无参构造器,那么就直接返回 minCapacity。也就是说:我新增元素的个数 + 数组中已有的元素个数 = 最小容量minCapacity,那你最少的给我准备 minCapacity 个数的容量,才够装我的数据。
3.3 ensureExplicitCapacity 方法
ensureExplicitCapacity 方法决定了 ArrayList 要不要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 首先累加修改次数
- 如果我实际要存放数据的个数(minCapacity) 减去 elementData.length 大于0,
也就是说,我要存放数据的个数大于数组的长度,这个数组装不下了,就需要调用 grow() 方法 扩容了。
3.4 允许最大数组长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
减8的目的是为了留位置存放数组的长度,因为数组自己不能计算长度,需要留个位置记录一下
3.5 扩容的方法 grow (扩容核心函数)
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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:
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow 扩容方法,传入参数(minCapacity) 告诉这个方法,我需要存放的数据的个数,也就是最小的容量,你的容量最小得等于我的个数,不然就不够装啊。
- 首先,算出数组旧的容量,赋值给 oldCapacity 变量
- 然后计算出 旧容量的1.5倍,在赋值给新容量 newCapacity。这里 oldCapacity >> 1 相当于除以2,在加上原来的就是1.5倍了。JDK1.6的做法是:int newCapacity = (oldCapacity*3)/2+1,这里位运算的速度是要比整除效率高。我这是JDK1.8。
- 接着判断,如果 newCapacity - minCapacity 如果小于 0 表示新容量比最小我要求容量还要小,也就是扩容后你还不够装的话,那就使用我要求的最小容量吧。将最小容量的值赋值给新容量变量。
- 再接着判断,如果 newCapacity - MAX_ARRAY_SIZE 大于 0 表示 新容量比允许最大数组长度都还要大,那咋办。那就只有请出我的大宝贝了,啊,呸…请出巨大容量函数:hugeCapacity(minCapacity)。并将它的返回值赋值给新容量。
- 最后,再将老数组的里面的数据拷贝到新数组里面去。
下面看看 巨大容量函数:hugeCapacity(minCapacity) 做了啥。
3.6 hugeCapacity 方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 首先,该函数判断了 minCapacity 是否是小于 0,如果是,那就表示内存溢出啦,直接抛错误。因为int是有范围的,超出了整个范围之后,就会变成一个负数。
- 然后判断,我所需要的容量(minCapacity),是否是大于 MAX_ARRAY_SIZE,如果是那就返回 Integer.MAX_VALUE:整型的最大值 2^{31}-1。否则就返回 MAX_ARRAY_SIZE
所以 ArrayList 扩容的核心思想是:扩容原来数组长度的1.5倍,然后再将老数组里面的数据拷贝到新数组中
4. 平时常用的方法
4.1 添加元素方法:add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
- 首先它调用了 ensureCapacityInternal 方法确保容量,检查是否需要扩容,关于这个方法的详细解释和调用链在前面已经学习过了,这里就不多解释了。
- 然后进行赋值
4.2 添加元素到指定位置方法:add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 首先 调用 rangeCheckForAdd 函数,检查指定位置是否越界,这个函数只有 add 和 addAll 的时候使用。
- 确保容量,检查是否需要扩容
- 把指定索引位置后的元素都往后挪一位;
- 在指定索引位置放置插入的元素;
- 累加实际存放元素个数。
4.3 将指定集合的数据添加到之前的集合中:addAll(Collection<? extends E> c)
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
- 首先,将参数 c 的数据拷贝到数组 a 中
- 算出 a 的长度,也就是这次于要添加数据的个数
- 确保容量,检查是否需要扩容
- 把数组a中的元素拷贝到elementData的尾部;
- 累加实际存放元素个数。
4.4 获取指定索引位置的数据:E get(int index)
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
首先检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是 ArrayIndexOutOfBoundsException异常。
然后在返回指定索引位置处的元素;
4.5 移除指定位置的元素:remove(int index)
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
- 检查索引是否越界;
- 获取指定索引位置的元素;
- 如果删除的不是最后一位,则其它元素往前移一位;(计算后 numMoved 如果等于0,那么表示删除的最后一位)
- 将最后一位置为null,方便GC回收;
- 返回删除的元素。
注意:ArrayList删除元素的时候并没有缩小容量。
4.5 删除指定对象:remove(Object o)
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
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
}
- 如果指定对象为 null的话,单独处理,ArrayList是可以存储null 的。 两段处理逻辑本质并没有变化:找到第一个等于指定元素值的元素
- 调用 fastRemove方法 快速删除;
fastRemove(int index)相对于remove(int index)少了检查索引越界的操作,并且不会返回已删除的值。
4.6 求两个集合的交集:retainAll(Collection<?> c)
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
- 遍历elementData数组;
- 如果元素在c中,则把这个元素添加到elementData数组的w位置并将w位置往后移一位;
- 遍历完之后,w之前的元素都是两者共有的,w之后(包含)的元素不是两者共有的;
- 将w之后(包含)的元素置为null,方便GC回收;
4.7 根据指定集合,删除原数组中与集合数据相同的数据:removeAll(Collection<?> c)
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
与retainAll(Collection<?> c)方法类似,只是这里保留的是不在c中的元素。
4.8 清楚所有元素:clear()
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
- 首先记录修改次数
- 循环 size ,将 elementData 里面的元素设置为 null,方便 GC 工作。
- 最后将 size 设置为 0。
5. 解答上面疑问:transient Object[] elementData 为何 elementData 要设置成 transient
transient 关键字的意思是不让序列化该关键字修饰的内容
而 ArrayList 它是实现了 java.io.Serializable 接口,这表示它可以被序列化,但是真正存储数据的数组却修饰成了不让序列化。那么这么做有什么意义呢?
原因是:ArrayList 源码里面有 writeObject() 和 readObject() 两个方法,这两个方法声明为private,在只有再 java.io.ObjectStreamClass#getPrivateMethod() 方法中通过反射获取到 writeObject() 这个方法
这样做的目的是:为了自己控制序列化的方式! 因为 elementData 是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量, 所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组,这样减少了空间占用。
6. 总结
- ArrayList内部使用数组存储元素,当数组长度不够时进行扩容,每次扩容1.5倍空间,remove 和 clear 不会让 ArrayList进行缩容
- ArrayList添加元素到中间比较慢,因为要挪动元素
- ArrayList从中间删除元素也比较慢,因为要挪动元素
- ArrayList支持随机访问,通过索引访问元素很快。
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。