本文以 JDK1.8为例,分析 ArrayList
的源码。
主要属性
- 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
- 用于空实例的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
- 用于默认大小的空实例的共享空数组实例。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
- 存储数组元素的列表缓冲区,所以ArrayList的底层实现是 Object[] 数组,并且可以扩容
transient Object[] elementData; // non-private to simplify nested class access
- 包含的元素数
private int size;
- 要分配的数组的最大大小。某些 VM 会在数组中保留一些标头字。尝试分配更大的阵列可能会导致内存不足错误:请求的阵列大小超过 VM 限制
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
带参的构造方法,用户自定义集合初始容量
/**
* 带参构造方法.
*
* @param initialCapacity 列表初始大小
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
无参构造方法
/**
* 无参构造方法
* 初始为空数组,当添加第一个元素时,数组容量变为10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
带参构造方法。用户传入一个集合,通过这个构造方法改造成 ArrayList
/**
* 按照集合的迭代器返回的顺序构造包含指定集合的元素的列表
*
* @param c 要将其元素放入此列表中的集合
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
// 判断数组长度
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 数组为空,转为空集合.
this.elementData = EMPTY_ELEMENTDATA;
}
}
集合常用方法
1. size()
获取列表中的元素数量
/**
* 返回此列表中的元素数。
*
* @return the number of elements in this list
*/
public int size() {
return size;
}
2. isEmpty()
如果此列表不包含任何元素,则返回 true 。阿里巴巴开发手册建议使用这个方法进行集合判空。
/**
* 如果此列表不包含任何元素,则返回 true 。
*
* @return <tt>true</tt> if this list contains no elements
*/
public boolean isEmpty() {
return size == 0;
}
3. toArray()
按顺序返回指定集合中的所有元素,并转换为 Object[]
/**
* 返回一个数组,该数组按正确的顺序(从第一个元素到最后一个元素)包含此列表中的所有元素。
返回的数组将是“安全的”,因为此列表不会维护对它的引用。(换句话说,此方法必须分配一个新数组)。因此,调用方可以自由修改返回的数组。
*
* 此方法充当基于数组和基于集合的 API 之间的桥梁。
*
* @return 以正确顺序包含此列表中所有元素的数组
*/
public Object[] toArray() {
// 调用工具类拷贝方法
return Arrays.copyOf(elementData, size);
}
/**
* 返回一个数组,其中包含此列表中所有元素的正确顺序(从第一个到最后一个元素);返回数组的运行时类型是指定数组的运行时类型。如果列表适合指定的数组,则在其中返回该列表。否则,将分配一个具有指定数组的运行时类型和此列表大小的新数组。
*
* 如果列表适合指定的数组,并留出空间(即,数组的元素比列表多),则紧跟在集合末尾之后的数组中的元素设置为 null。(仅当调用方知道列表不包含任何 null 元素时, 这在 确定列表的长度时才很有用
*
* @param a – 要存储列表元素的数组,如果它足够大;否则,将为此目的分配相同运行时类型的新数组
* @return 包含列表元素的数组
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in
* this list
* @throws NullPointerException if the specified array is null
*/
@SuppressWarnings("unchecked") // 忽略警告
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 创建一个指定泛型类的数组
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// 调用System提供的arraycopy()方法实现数组之间的复制
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
4. get()
返回此列表中指定位置的元素
/**
* 返回此列表中指定位置的元素
*
* @param 查找的元素索引下标
* @return 此列表中指定位置的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
// 判断索引是否合法,否则抛出 IndexOutOfBoundsException 越界异常
rangeCheck(index);
// 返回数据数组中的指定位置元素
return elementData(index);
}
/**
* 检查给定的索引是否在范围内。
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
5. set()
将此列表中指定位置的元素替换为指定的元素。
/**
* 将此列表中指定位置的元素替换为指定的元素。
*
* @param index – 要替换的元素的索引
* @param element – 要存储在指定位置的元素
* @return 原来位于这个位置的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {
// 越界检查
rangeCheck(index);
// 获取原值
E oldValue = elementData(index);
// 重新赋值
elementData[index] = element;
// 返回原值
return oldValue;
}
6. add()
添加元素
/**
* 将指定的元素追加到此列表的末尾。
*
* @param e – 要附加到此列表的元素
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 扩容机制
ensureCapacityInternal(size + 1); // Increments modCount!!
// 新增元素
elementData[size++] = e;
return true;
}
/**
* 在此列表中的指定位置插入指定的元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将一个元素添加到其索引中)
*
* @param index – 要插入指定元素的索引
* @param element – 要插入的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
// 添加元素时使用的索引越界检查
rangeCheckForAdd(index);
// 扩容机制
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将当前索引及后续元素,向后移动,空出下标为 index 的位置
System.arraycopy(elementData, index, elementData, index + 1, size - index);
// 赋值
elementData[index] = element;
size++;
}
/**
* 添加和添加全部使用的 rangeCheck 版本。
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
7. remove()
删除列表元素
/**
* 删除此列表中指定位置的元素。将任何后续元素向左移动(从其索引中减去一个)。
*
* @param index – 要删除的元素的索引
* @return 从列表中删除的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
// 索引越界检查
rangeCheck(index);
// AbstractList 中定制的属性,这些线程不安全的集合中,实现List的 fail-fast 机制
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;
}
/**
* 从此列表中删除指定元素的第一个匹配项(如果存在)。如果列表不包含该元素,则它保持不变。
*
* @param o – 要从此列表中删除的元素(如果存在)
* @return 如果此列表包含指定的元素,则为 true
*/
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
}
8. addAll()
将指定集合中的所有元素插入到此列表中
/**
* 将指定集合中的所有元素追加到此列表的末尾,顺序与指定集合的迭代器返回这些元素的顺序相同.
*
* @param c –包含要添加到此列表中的元素的集合
* @return 如果此列表因调用而更改,则为 true
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
// 将指定集合转为 Object 数组
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;
}
/**
* 从指定位置开始,将指定集合中的所有元素插入到此列表中。将当前位于该位置的元素(如果有)和任何后续元素向右移动(增加其索引)。新元素将按照指定集合的迭代器返回的顺序显示在列表中
*
* @param index – 插入指定集合中的第一个元素的索引
* @param c –包含要添加到此列表中的元素的集合
* @return 如果此列表因调用而更改,则为 true
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 所有下标检查
rangeCheckForAdd(index);
// 将指定集合转为 Object 数组
Object[] a = c.toArray();
// 获取指定集合的长度
int numNew = a.length;
// 扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// 移动指定下标右边的元素
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
// 拷贝数据
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
扩容机制分析
观察 ArrayList 的无参构造方法可以看出,初始化的时候是一个长度为0的空数组。
// 空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
那么到底是什么时候给数组分配容量呢?
我们观察到,在 add()
方法的第一行,调用了一个 ensureCapacityInternal()
方法,所以我们开始分析一下 ensureCapacityInternal()
方法,发现它调用了另外两个方法 calculateCapacity()
和 ensureExplicitCapacity()
,下面针对这三个方法先进行分析。
// 扩容机制入口方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 根据所需最小容量和当前数组计算所需容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前是初始情况下的空数组,返回默认容量和最小容量的较大值。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 不是初始情况则返回最小容量
return minCapacity;
}
// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需最小容量大于集合数组长度,通过grow方法进行扩容
// overflow-conscious code
if (minCapacity - elementData.length > 0) grow(minCapacity);
}
我们假设通过无参构造 ArrayList 后,进行 add() 操作,所以
ensureCapacityInternal()
方法的入参minCapacity
值应该为 1。进入
calculateCapacity()
方法,if 语句判断条件发现这是一个通过无参构造器创建的空集合,因此返回较大值DEFAULT_CAPACITY = 10
,接着调用ensureExplicitCapacity()
方法,if 语句发现需要进行扩容,最后执行grow()
方法。
/**
* 要分配的数组的最大大小。某些 VM 会在数组中保留一些标头字。尝试分配更大的阵列可能会导致内存不足错误:请求的阵列大小超过 VM 限制
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 增加容量以确保它至少可以容纳最小容量参数指定的元素数
*
* @param minCapacity – 所需的最小容量
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 位运算,右移一位,相当于除以二,所以 newCapacity 是 oldCapacity 的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 检查新容量是否足够,若还是小于所需最小容量,那么就把所需最小容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE 则调用 hugeCapacity() 方法进行比较,返回新容量
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);
}
// 判断所需最小容量和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
// 如果 minCapacity 小于零,证明在前面已经溢出了,返回异常
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 对 minCapacity 和 MAX_ARRAY_SIZE 进行比较
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
接着上面的举例分析,执行
grow()
方法的入参是 10 ,因为oldCapacity = 0
,所以通过第一个 if 语句过后newCapacity = 10
,跳过第二个 if 语句后,将elementData
扩容,最后size = 10
。
overflow-conscious code
在上面扩容机制的源码中,看到了这样的注释,意思是这段代码考虑了数值溢出的情况。JDK 中有很多考虑溢出的代码,其中扩容机制中 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);
elementData = Arrays.copyOf(elementData, newCapacity);
}
在上面的代码中,如果 oldCapacity
足够大,那么 1.5 倍之后的 newCapacity
很容易数值溢出从而变为负数,原因需要了解计算机中二进制的加减原则。
在 newCapacity
为负数的情况下,我们如果使用传统的 if (newCapacity < minCapacity)
来判断的话,很显然结果是 true
,那么就会进入 if
语句内部,将 newCapacity
变为 minCapacity
。这个情况很显然并不是我们想要发生的。
所以 JDK 在这里使用的是 if (newCapacity - minCapacity < 0)
进行判断,尽然 newCapacity
已经溢出变为负数,那么newCapacity - minCapacity
必然也会发生溢出,这就意味着相减的结果大于 0 ,那么就不会进入 if 语句内部。在第二个 if 判断时,相减后也必然大于 0 ,那么就会进入 if 内部重新获取 newCapacity
。这样 JDK 就解决了数据溢出的问题。
Java 中 int 溢出问题
在 Java 中,int 长度为 4 字节,也就是32位,在计算机(补码)中的最大值二进制表示为 0111 1111 1111 1111 1111 1111 1111 1111
,(第一位表示为符号位,0代表正,1代表负),那么 int 类型溢出的情况就是在最大值的情况下再加 1,那么就变成了 1000 0000 0000 0000 0000 0000 0000 0000
由于还是补码,那么转为十进制就是 -2147483648
。