java备忘录之ArrayList源码分析

ArrayList源码分析

ArrayList

ArrayList底层是用数组实现,能够自动扩展大小以适应存储元素的不断增加,它具有数组的一些特点,例如查找修改快而插入删除慢。

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//默认初始空间大小
private static final int DEFAULT_CAPACITY = 10;

//空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};


// 和EMPTY_ELEMENTDATA一样,为了知道怎样扩充当第一个元素被添加
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/*对象数组
基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList
具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。*/
transient Object[] elementData; // 非私有,以简化嵌套类访问

//集合元素个数
private int size;

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 传入初始容量的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//新建指定容量的Object类型数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

//不带参数的构造方法
public ArrayList() {
//将空的数组实例传给elementData
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

//传入外部集合的构造方法
public ArrayList(Collection<? extends E> c) {

//持有传入集合的内部数组的引用
elementData = c.toArray();
//更新集合元素个数大小
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//判断引用的数组类型, 并将引用转换成Object数组引用
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 如果元素个数为0,引用指向空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}

增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//增(添加)
public boolean add(E e) {
//添加前先检查是否需要拓展数组, 此时数组长度最小为size+1
ensureCapacityInternal(size + 1);
//将元素添加到数组末尾
elementData[size++] = e;
return true;
}

//增(插入)
public void add(int index, E element) {
//插入位置范围检查
rangeCheckForAdd(index);
//检查是否需要扩容
ensureCapacityInternal(size + 1);
//挪动插入位置后面的元素
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//在要插入的位置赋上新值
elementData[index] = element;
size++;
}

//删
public E remove(int index) {

//index不能大于size
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
//将index后面的元素向前挪动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; //置空引用 clear to let GC do its work

return oldValue;
}


//查
public E get(int index) {
rangeCheck(index);

return elementData(index);
}

//改
public E set(int index, E element) {
rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}

每次添加一个元素到集合中都会先检查容量是否足够,否则就进行扩容,扩容的细节下面会讲到。我们先看具体增删改查要注意的地方。
增(添加):仅是将这个元素添加到末尾。操作快速。
增(插入):由于需要移动插入位置后面的元素,并且涉及数组的复制,所以操作较慢。
删:由于需要将删除位置后面的元素向前挪动,也会设计数组复制,所以操作较慢。
改:直接对指定位置元素进行修改,不涉及元素挪动和数组复制,操作快速。
查:直接返回指定下标的数组元素,操作快速。

扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

private void ensureCapacityInternal(int minCapacity) {
//如果此时还是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//和默认容量比较, 取较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//数组已经初始化过就执行这一步
ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

//如果最小容量大于数组长度就扩增数组
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/**
* The maximum size of array to allocate.
* 一些VM在数组中保留一些标题字。
* 尝试分配更大的数组可能会导致
* OutOfMemoryError: Requested array size exceeds VM limit
*/
//集合最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//增加数组长度
private void grow(int minCapacity) {

//获取数组原先的容量
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);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

每次添加元素前会调用ensureCapacityInternal这个方法进行集合容量检查。在这个方法内部会检查当前集合的内部数组是否还是个空数组,如果是就新建默认大小为10的Object数组。如果不是则证明当前集合已经被初始化过,那么就调用ensureExplicitCapacity方法检查当前数组的容量是否满足这个最小所需容量,不满足的话就调用grow方法进行扩容。

在grow方法内部可以看到,每次扩容都是增加原来数组长度的一半,扩容实际上是新建一个容量更大的数组,将原先数组的元素全部复制到新的数组上,然后再抛弃原先的数组转而使用新的数组。至此,我们对ArrayList中比较常用的方法做了分析,其中有些值得注意的要点:

ArrayList底层实现是基于数组的,因此对指定下标的查找和修改比较快,但是删除和插入操作比较慢。

构造ArrayList时尽量指定容量,减少扩容时带来的数组复制操作,如果不知道大小可以赋值为默认容量10。

每次添加元素之前会检查是否需要扩容,每次扩容都是增加原有容量的一半。

每次对下标的操作都会进行安全性检查,如果出现数组越界就立即抛出异常。

ArrayList的所有方法都没有进行同步,因此它不是线程安全的。

Fail-Fast

modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
}
}

Vector

同步

它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}

public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);

return elementData(index);
}

ArrayList 与 Vector

Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。

Vector 替代方案

为了获得线程安全的 ArrayList,可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

1
2
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

1
List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList 是一种 CopyOnWrite 容器,从以下源码看出:读取元素是从原数组读取;添加元素是在复制的新数组上。读写分离,因而可以在并发条件下进行不加锁的读取,读取效率高,适用于读操作远大于写操作的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

final void setArray(Object[] a) {
array = a;
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
打赏

请我喝杯咖啡吧~

支付宝
微信