简单介绍
本篇文章我们将重点分析 ArrayList 源码,并且同时介绍并对比 Vector 和 LinkedList
源码分析素材为 JDK1.8 的 ArrayList 源码。
什么是ArrayList, Vector, LinkedList?
ArrayList
ArrayList 实现了 List 接口,是一种变长的集合类,基于定长数组实现,它允许所有元素,包括null。
Vector
Vector 和 ArrayList 几乎是一样的,区别在于Vector是线程安全的,因为这个原因,它的性能较 ArrayList 差。
LinkedList
LinkedList 底层采用的双向链表结构,和 ArrayList 一样,支持空值和重复值。
接下来我们以提问的方式来回答ArrayList,Vector,LinkedList所具有的特征
ArrayList 的特征
ArrayList 是线程安全的吗?
ArrayList
不是线程安全的。
ArrayList
没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一个解决方法就是在创建List时构造一个同步的List
List list = Collections.synchronizedList(new ArrayList(...));
底层数据结构?
Arraylist
底层使用的是 Object
数组
是否支持快速随机访问?
ArrayList
实现了 RandomAccess
接口(该接口是个标志性接口),表明它具有随机访问的能力。
内存空间占用?
ArrayList
的空间浪费主要体现在 list 列表的结尾会预留一定的容量空间
Vector 的特征
Vector 是线程安全的吗?
Vector
是线程安全的。
底层数据结构?
Vector
和 ArrayList
一样 底层使用的是 Object
数组
是否支持快速随机访问?
Vector
和 ArrayList
一样,支持随机访问。
内存空间占用?
Vector
和 ArrayList
一样,会有在list结尾会预留容量空间的内存浪费
LinkedList 的特征
LinkedList 是线程安全的吗?
LinkedList 是线程不安全的,不是同步的。
底层数据结构?
LinkedList
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表JDK1.7 取消了循环。)
是否支持快速随机访问?
LinkedList
不支持高效的随机元素访问
内存空间占用?
LinkedList
的空间花费则体现在它的每一个元素都需要消耗比 ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList 源码分析
底层数据结构
类的属性:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 默认初始容量大小
*/
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
/**
* ArrayList 所包含的元素个数
*/
private int size;
}
所以这里我们看出来 ArrayList
的底层数组 elementData
是底层的核心存储数组。
接下来我们通过构造函数了解
elementData
的初始化
构造函数
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这两个构造方法做的事情目的都是初始化底层数组 elementData
。其中,无参构造方法会将 elementData
初始化一个空数组,以后再根据扩容机制进行重新初始化。
我们一般使用默认构造函数就行了,如果知道会将 ArrayList
插入多少元素,我们可以使用有参构造方法,避免浪费空间。
add方法 (插入)
ArrayList 插入逻辑主要代码:
/** 在元素序列尾部插入 */
public boolean add(E e) {
// 1. 检测是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 2. 将新元素插入序列尾部
elementData[size++] = e;
return true;
}
/** 在元素序列 index 位置处插入 */
public void add(int index, E element) {
rangeCheckForAdd(index);
// 1. 检测是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 2. 将 index 及其之后的所有元素都向后移一位
//arraycopy()方法实现数组自己复制自己
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 3. 将新元素插入至 index 处
elementData[index] = element;
size++;
}
根据插入的位置不同,ArrayList 源码分成两种不同的逻辑
-
在元素序列尾部插入
- 检测数组是否需要扩容
- 将新元素插入到序列尾部
-
在元素序列指定位置插入
- 检测数组是否需要扩容
- 将 index 及其之后的所有元素都向后移一位
- 将新元素插入至 index 处
如下图所示:
从这个过程我们可以看出插入元素是一个时间复杂度为O(N)的操作。所以我们应该尽量避免在大集合中调用第二个插入方法。
扩容机制(自动)
分析完插入后,接着我们就分析插入逻辑中出现的扩容机制
其中,扩容的入口方法是
ensureCapacityInternal
,核心方法是grow
我们知道 ArrayList 是一个变长数组,当底层的数组结构没有空余的空间时就需要进行扩容。
/** 计算最小容量 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/** 扩容的入口方法 */
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/** 扩容的核心方法 */
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
// newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 如果最小容量超过 MAX_ARRAY_SIZE,则将数组容量扩容至 Integer.MAX_VALUE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
扩容的核心在 grow
方法:
-
oldCapacity为旧容量,我们以 oldCapicity 的1.5倍来作为 newCapacity(新容量)
-
检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量
-
如果新容量大于 MAX_ARRAY_SIZE, 进入(执行)
hugeCapacity()
方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为
Integer.MAX_VALUE
,否则,新容量大小则为 MAX_ARRAY_SIZE 即为Integer.MAX_VALUE - 8
。 -
最后调用
Arrays.copyof()
方法完成扩容
remove方法 (删除)
ArrayList 的删除根据删除元素的位置不同,同样分出了两种逻辑。但是不同于插入操作,ArrayList 并没有无参的删除方法,只能删除指定位置的元素或者删除指定元素。
/** 删除指定位置的元素 */
public E remove(int index) {
rangeCheck(index);
modCount++;
// 返回被删除的元素值
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 将 index + 1 及之后的元素向前移动一位,覆盖被删除值
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将最后一个元素置空,并将 size 值减1
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
E elementData(int index) {
return (E) elementData[index];
}
/** 删除指定元素,若元素重复,则只删除下标最小的元素 */
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
}
-
删除指定位置的元素
- 获取指定位置 index 处的元素值
- 将 index + 1 及之后的元素向前移动一位,覆盖被删除值
- 将最后一个元素置空,并将 size 值减 1
- 返回被删除值,完成删除操作
如下图:
所以可以发现,这里删除指定位置的元素的时间复杂度也是O(N)
-
删除指定元素
- 遍历数组,查找要删除元素的位置(若重复,只删除最小下标上的元素)
- 对该位置进行 "快速删除"(不进行边界检查,也不返回删除的元素值)
缩容机制(手动)
分析完删除后,我们思考这么一种情况:
我们往 ArrayList 中插入了很多的元素后,然后又删除了很多元素,这个时候底层数组肯定会空闲大量的空间。
由于 ArrayList 没有自动缩容的机制,导致底层数组大量的空闲空间不能被释放,造成浪费。对于这种情况ArrayList也提供了对应的处理方,即
trimToSize
方法
/** 将数组容量缩小至元素数量 */
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
通过这个方法,我们可以手动出发 ArrayList 的缩容机制,释放多余的空间,提高空间利用率。
Arrays.copyof() 和 System.arraycopy() 方法
在上面我们注意到了源码中不止一次的出现了
Arrays.copyof()
和System.arraycopy()
方法所以我认为这在这章讲讲这两个也是有必要的。
其中本章的测试引用自:javaguide.cn/java/collec…
System.arraycopy()
方法
方法签名:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
参数解释:
- src: 源数组;
- srcPos: 源数组中的起始位置;
- dest:目标数组;
- destPos:目标数组中的起始位置;
- length:要复制的数组元素的数量;
这个方法是一个native方法,于是我们直接对它做测试:
public class ArraycopyTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] a = new int[10];
a[0] = 0;
a[1] = 1;
a[2] = 2;
a[3] = 3;
System.arraycopy(a, 2, a, 3, 3);
a[2]=99;
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}
结果:
0 1 99 2 3 0 0 0 0 0
Arrays.copyof()
方法
源码:
public static int[] copyOf(int[] original, int newLength) {
// 申请一个新的数组
int[] copy = new int[newLength];
// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
使用Arrays.copyof()方法主要目的应该是为了给原有数组扩容,测试:
public class ArrayscopyOfTest {
public static void main(String[] args) {
int[] a = new int[3];
a[0] = 0;
a[1] = 1;
a[2] = 2;
int[] b = Arrays.copyOf(a, 10);
System.out.println("b.length"+b.length);
}
}
结果:
10
快速随机访问
ArrayList 实现了
RandomAccess
接口(该接口是个标志性接口),表明它具有随机访问的能力。ArrayList 底层基于数组实现,所以它可在常数阶的时间内完成随机访问,效率很高。
对 ArrayList 进行遍历时,一般情况下,我们喜欢使用 foreach 循环遍历,但这并不是推荐的遍历方式。ArrayList 具有随机访问的能力,如果在一些效率要求比较高的场景下,更推荐下面这种方式:
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
至于原因也不难理解,foreach 最终会被转换成迭代器遍历的形式,效率不如上面的遍历方式。
fail-fast (快速失败机制)
什么是快速失败机制?
java.util
包下的集合类都是实现了快速失败机制的。当遇到并发修改时,迭代器会快速失败,以免程序在该机制将来不确定的时间里出现不确定的行为。该机制被触发时,会抛出并发修改异常ConcurrentModificationException
。
原理
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount
变量。集合在被遍历期间如果内容发生变化,就会改变 modCount
的值。每当迭代器使用 hashNext()/next()
遍历下一个元素之前,都会检测 modCount
变量是否为 expectedmodCount
值,是的话就返回遍历;否则抛出 ConcurrentModificationException
异常,终止遍历。
ArrayList 的迭代器的源码:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
public E next() {
// 并发修改检测,检测不通过则抛出异常
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
// 省略不相关的代码
}
这里的逻辑不止可以在ArrayList中找到,在Vector和LinkedList中也有
观察源码我们知道了这里异常的抛出条件是检测到 modCount!=expectedmodCount
这个条件。如果集合发生变化时修改 modCount
值刚好又设置为了 expectedmodCount
值,则异常不会抛出。
既然谈到了快速失败机制,那么我们干脆就把与之相对应的失败安全机制一并端上来罢
safe-fast (失败安全机制)
什么是失败安全机制?
java.util.concurrent
包下的容器都是实现了失败安全机制的。可以在多线程下并发使用,并发修改。采用失败安全机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException
。
同时迭代器并不能访问到修改后的内容,在遍历期间原集合发生的修改迭代器是不知道的。由于是需要拷贝的,所以也比较吃内存。
ArrayList 的使用
如何遍历 ArrayList?
遍历 ArrayList 主要有以下3种方式:
-
迭代器遍历
Iterator<String> itr = list.iterator(); // Iterator遍历 while (itr.hasNext()) { System.out.println(itr.next()); }
-
for循环
// for循环遍历 for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
-
foreach循环
// foreach遍历 for (String element : list) { System.out.println(element); }
小结
ArrayList
、LinkedList
、Vector
是 List
接口的三个实现类,本文在特征以及概述部分分析了它们的异同。并且在源码分析篇中详细分析了 ArrayList
的源码。
ArrayList
作为最为常用的集合类之一,是面试中考察的重点。如果对这几个集合类有新的理解,本文会进行更新。
本文参考: