推荐阅读时间 13min+
目录
- ArrayList 关键词
- 源码阅读
- 问题解答和总结
前言
本文基于Java8的源代码进行了源代码的代码阅读分析,关于Java8中的新增加的Stream API
特性在数据结构中的时候会在之后之后专门使用一篇文章的内容进行介绍,这里只介绍源代码和代码逻辑分析。
ArrayList关键词
阅读ArrayList的相关文档,很容易从中提取出如下的关键词:
backing array
-
capacity
/incremental reallocation
-
structural modification
/fail-fast
综合这三个关键词,我们不难了解到ArrayList
的特性和问题。backing array
指出实现了ArrayList
的幕后机制的是一个幕后数组(backing array,Object数组),其中它的容量(capacity
)是可以增量调整的(incremental reallocation
),并且ArrayList
并不像它的前辈Vector
是一种线程安全的容器,如果出现了结构性变化(structural modification,比如 add remove,set不是 ,会通过modCount标记这个结构性变化)会使用一种机制,这种机制不会让存在这缺陷的过程继续下去,而是立刻停止系统工作,这种机制也被称为fail-fast
。fail-fast
是一种尽最大努力(best-effort
)的机制,不可以基于它抛出的异常做错误控制。
从三个关键词中衍生出三个问题
- 如果是一个Object数组,
ArrayList
如何实现类型检查和相关的问题? -
fail-fast
机制如何实现的,具体如何体现? -
ArrayList
如何实现扩容的?
关于这三个问题,希望读者能在我的分析中思考,最后我也会给出答案。
源码分析
ArrayList的继承关系图
其中
ArrayListSpliterator
和Strem API有关,这里后续会详细分析。
ArrayList中的关键常量
这些常量都是见名知意的,{}表示空数组字面量
名称 | 类型 | 初始值 | 意义 |
---|---|---|---|
DEFAULT_CAPACITY | Object[] | 10 | 表示下面的DEFAULT空数组的大小 |
DEFAULTCAPACITY_EMPTY_ELEMENTDATA | Object[] | {} | |
elementData | Object[] | {} | |
EMPTY_ELEMENTDATA | Object[] | {} | 空数组大小为0,和DEFAULT数组相区别 |
size | int | 0 | List大小 |
modCount | int | 0 | fail-fast标记 |
MAX_ARRAY_SIZE | int | Integer.MAX_VALUE-8 | 最大大小 |
构造函数
ArrayList的中的三个构造函数
- 空构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
直接初始化了elementData
为DEFAULT
数组,大小默认为10
- 带有初始大小参数的构造函数
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);
}
}
- 将现有的Collection的内容初始化ArrayList
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;
}
}
其中Collection
中的元素的在ArrayList中的顺序和Collection c
的iterator
的遍历顺序相同,具体的实现在toArray
函数中,这里采取ArrayList
和LinkedList
的toArray
作为参考:
- ArrayList
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
- LinkedList
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
ArrayList
中迭代器迭代的顺序是按照数组的顺序,LinkedList
中的顺序是按照Node
连接的顺序,没毛病。
这里看一下 打****的问题,这是一个官方bug,为什么toArray()
不一定能返回Object[]
? 事实上这是一个类型转换的问题,这篇文章说的非常清晰,轻容许我简要说明一下他的观点:
public class MyList extends ArrayList {
// toArray() 的同名方法
public String[] toArray() {
return new String[]{"1", "2", "3"};
}
}
因为方法的重载不看返回值的,如果子类定义了这个方法,当调用toArray()
的时候,就回返回String[]
,这样就会出现错误。
add相关操作
add
有很多相关方法,不一一列举代码了,具体实现大同小异。add
操作的代码调用情况图如下:
这些方法都依赖
ensureCapacity
相关方法,这里要着重分析一下。
ensure相关方法
ensure
相关方法都和扩容操作有关,minCapacity
就是如果elementData
容量不够大,就会最小扩容到这个大小,并且留意modCount
的变化
private void ensureCapacityInternal(int minCapacity) {
//封装方法 详细调用,直接看后面
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果backing array是Default数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//体现了Default数组和其Default大小的对应关系
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
//这是一个结构性变化,
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//首先尝试扩容到原来的大小的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//下面的的两个if语句指向了扩容情况的两个极端:
//不够minCapacity
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
所以我们再看看最基本的add(E e)
方法干了什么,看一个方法,其余的同名方法的做法大同小异:
public boolean add(E e) {
//minCapacity = size + 1 然后在进行是否扩容的试探
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
综上所述,add
操作都是结构性变化的操作,通过ensure
相关函数进行扩容和modCount
的自增(fail-fast
防止多线程操作)。再扩容期间,每次都会扩容1.5倍,所以在感觉可能数据很多的情况下,使用默认的无参数构造函数的所产生的10个空间是不够的。
remove相关操作
remove
操作也是一个结构性变化的操作,我们主要看看几个修改modCount
的操作:
fastRemove
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;
}
//fastRemove 之所以fast是因为不需要在这个方法中做边界判断,边界判断在上面的for循环已经完成了
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
}
batchRemove()
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++)
//如果需要保留c中的元素,complement取true
if (c.contains(elementData[r]) == complement)
//利用双指针进行复制覆盖原来的位置
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
//因为ArrayList是可以容纳null元素的,所以contains不会抛出异常,但是有些容器不能容纳null的时候,会从上面的if语句进入finally块
//直接默认r后面的是需要保留的
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;
//set不是结构性变化操作,删除才是,所以是size - w
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
介绍完了modCount
的两个主要修改的地方,下面请看modCount
对于fail-fast的作用的体现。
checkForComodification()
ArrayList
有两个迭代器Itr
和ListItr
,ListItr
在上面的图中可以看出是继承了Itr
的。这篇优秀的文章
中给我们展示了CME(ConcurrentModificationException
)的两种出现的的情况,不难看出,CME常常出现在使用迭代器迭代的情况下,或者Java的语法糖foreach中,对List进行结构性修改。通过阅读Itr
的源码可以对CME的出现原因了解的非常清楚。
Itr
中的重要常量:
名称 | 类型 | 初始值 | 意义 |
---|---|---|---|
cursor | int | 0 | 游标 |
lastRet | int | -1 | |
expectedModCount | int | modCount | 在初始化Itr时会复制modCount,用于确保不会有其他线程对其进行修改 |
在Itr
的next
和remove
方法中都需要调用checkForComodification
方法:
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
在上面的文章中列举的单线程情况中,因为ArrayList
的remove
方法虽然修改modCount
但是,和Itr
中的expectedModCount
不符,而导致异常。说穿了,这实际上是因为外部类的modCount
和内部类的expectedModCount
不相同导致的问题,这只能说是一个缺陷。在多线程的环境下,使用modCount
才是比较好的控制策略。
SubList
SubList
是ArrayList
的一个内部类,其中的方法大体上和ArrayList
一致,可以看做是对ArrayList
的一个“视图”,但是这个视图是可以影响其原本映射的List
的。
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
问题解答和总结
虽然这篇文章并没有对于整体的所有代码进行逐行解释,但是大体上能够让读者对于ArrayList
源码有一个直观地认识。
下面来回答文章开头提的几个问题:
-
fail-fast
是通过modCount
和Itr
还有ListItr
,SubList
等中的checkForComodification
操作实现的。但是在foreach和while中使用迭代器模式进行遍历时,禁止使用ArrayList
的remove
和add
操作。这样的机制在最大程度上避免了多线程修改。 - 扩容机制是通过
ensure
函数实现的,如果大小不够会通过扩充1.5倍看看。 - 类型检查。首先使用了静态类型检查,泛型做了一定的限定。其次,针对
toArray()
这里天然的语法缺陷,也做了个逻辑层面的判断,规避类型不一产生的问题。