知乎:zhuanlan.zhihu.com/p/77500314
ArrayList 解析
[TOC]
数据结构
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 也是空数组,为什么有两个空集合后续再说
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 上述空数组对应的 capacity,用处后面说
private static final int DEFAULT_CAPACITY = 10;
// 存储对象的 Object 数组
// 这里可以看到两点:
// 1. transient 标注,避免序列化(后续自定义了序列化方法)
// 2. Object 数组,ArrayList 是支持泛型的,但是存储的时候的确就是一个 Object
// 支持此实现一是 Java 的泛型擦除,二是 ArrayList 完全可以不使用泛型任意存放对象
transient Object[] elementData;此处我们可以看到 ArrayList 的实现就是一个数组,那么对于数组的 get(index) 方法应该是很容易推测出实现方式的了——基于数组下标。
同样可以理解 add(Object) 操作就是在当前数组后追加插入一个元素。现在的问题是,ArrayList 当前可用(free)的数组容量是否满足追加插入的操作。
// 当前 ArrayList 中元素的数量
private int size;
// 存储对象的 Object 数组
// 通过 elementData.length 可以获取到当前数组的长度
transient Object[] elementData;构造函数
刚提到的 ArrayList 初始化了两个空数组,他们的用处在构造 ArrayList 时能够得到体现:
public ArrayList() {
// 这里可以看到,如果使用了默认的构造函数,实际上构建的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 而他对应的 DEFAULT_CAPACITY 为 10.
// 这点会在扩容的时候得到体现,也就是如果使用了空构造函数,实际上扩容时候默认容量为 10
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 此处可以看到,如果 new ArrayList(0),实际上得到的就是一个空数组
// 在扩容的时候也是根据当前 size * 1.5 倍去扩容
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}可以看到如果使用了 new ArrayList(0); 来构造一个 ArrayList,那么在扩容的时候,将从容量 1 为起点逐渐增大容量,而扩容函数基本可以理解为 size * 1.5,那么如果从 1 开始的话,将会在前几次扩容的时候,扩容很小,非常容易造成下一次扩容,从而带来一系列的空间时间成本(如数组的复制、遍历等)。
扩容
我们把 ArrayList 理解成有多个格子的一个容器,每个格子可以存放一个 Object 对象。 elementData.length 代表当前 ArrayList 实际拥有的格子数,size 代表当前已经填充了对象的格子数。他们的差值即可表示当前容器还有多少格子可用了。
不难理解只有在向 ArrayList 内插入数据时,才需要计算当前容量是否充足。所以扩容的时机也是在插入对象的时候。
private static final int DEFAULT_CAPACITY = 10;
// 这里可以看到一开始在初始化空数组的时候为什么需要初始化两个
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组表示的是默认构造函数调用时,elementData 对象的值
// 这里可以看到如果 elementData 就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 那么就会采用上面申明的 DEFAULT_CAPACITY 默认容量作为比较参数
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 此方法是在 ArrayList 内部调用确保容量够用的方法
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) {
// 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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}注意到此处有一段注释 overflow-conscious code,实际上有段代码在 JDK 6 中是这样写的:
// JDK 7
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// JDK 6
if (newCapacity < minCapacity)
newCapacity = minCapacity;注意此处 a < b 和 a - b < 0 代表的是完全不同的两件事情。看代码:
int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
System.out.println("a < b");
}
if (a - b < 0) {
System.out.println("a - b < 0");
}
// output
// a - b < 0此处只有 a - b < 0 的结果为 true,a - b 的结果溢出,得到的值是 -1。
回看 grow(int) 函数,假设 oldCapacity 的值很接近 Integer.MAX_VALUE,那么 newCapacity (由 oldCapacity + (oldCapacity >> 1) 得到) 就很有可能溢出,然后变成了一个负数(可能是 Integer.MIN_VALUE)。 这段代码就是为了解决 newCapacity 如果溢出变成负数的问题:
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - maxSize > 0)
newCapacity = hugeCapacity(minCapacity);具体的解释参看 mail.openjdk.java.net/pipermail/c…
此处 grow(int) 函数最终执行了 Arrays.copyOf(elementData, newCapacity); 要了解扩容的过程就看一下 Arrasy.copy(T[], int) 方法。
// ArrayList.grow(int) 中调用的方法
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 创建一个 copy[] 数组,此处有两点注意
// 1. copyOf 执行过程中实际上是新开辟了一段数组空间给 copy[]
// 2. 注意此处的类型,newType.getComponentType() 会返回当前数组中元素的 Class(注意此处的 newType 类型是 Class<? extends T[]>)
// Class.getComponent() 方法的注释 ↦ Returns the Class representing the component type of an array. If this class does not represent an array class this method returns null.
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 此方法是个 native 方法, 可以通过入参的名称猜到他的执行方式
// public static native void arraycopy(Object src, int srcPos,
// Object dest, int destPos,
// int length);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}迭代器
使用迭代器(iterator)遍历容器是 Java 的常用操作,不过这里必须要先提一嘴 RandomAccess接口,这就是一个空接口,ArrayList 实现了这个接口 。
/**
* Marker interface used by List implementations to indicate that they support fast (generally constant time) random access.
* 标记 List 接口的实现类以表示此实现类是否支持快速(常数时间内)随机访问。
*
* As a rule of thumb, a List implementation should implement this interface if, for typical instances of the class, this loop:
* for (int i=0, n=list.size(); i < n; i++)
* list.get(i);
*
* runs faster than this loop:
* for (Iterator i=list.iterator(); i.hasNext(); )
* i.next();
* 一般来说,一个 List 的实现类如果 for i 循环速度大于 iterator 迭代,就应该 implements RandomAccess 接口。
*/
public interface RandomAccess {
}
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable可以看到如果需要遍历 ArrayList 容器的话,优先使用的还是 for i 循环比较好。
这里讲一下迭代器的使用,JCF 容器是怎么实现迭代器的能力的?答案是内部类,ArrayList 在内部实现了 Itr 类,再通过 iterator() 向外暴露方法以便调用迭代器进行遍历。
ArrayList
↦ AbstractList
↦ AbstractCollection
↦ Collection
↦ Iterable * 此接口内声明了 iterator() 方法
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;
Itr() {}
// hasNext 实现很简单,就是看当前的指针(cursor)是否到了最后(size 对象数量)
public boolean hasNext() {
return cursor != size;
}
// next 也很简单,就是拿到 ArrayList 的 elementData 数组直接用下表访问即可
public E next() {
...
int i = cursor;
Object[] elementData = ArrayList.this.elementData;
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}Iterator 只有后向 next() 获取元素的操作,为了支持前向遍历的操作,ArrayList 实现了 listIterator() 方法,此方法是 List 接口中声明的。
public interface List<E> extends Collection<E> {
...
ListIterator<E> listIterator();
...
}因为 ListIterator 较 Iterator 只是增加了前向遍历的能力,所以它直接继承了 Iterator扩充了部分方法:
private class ListItr extends Itr implements ListIterator<E> {
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
// 可以看到只是指针(cursor)在移动,其余和 next 没啥区别
public E previous() {
...
int i = cursor - 1;
Object[] elementData = ArrayList.this.elementData;
cursor = i;
return (E) elementData[lastRet = i];
}
}JDK 1.8 之后,Iterable 接口还多了一个方法,此方法会在 Stream 处理中会使用到,按下不表。
public interface Iterable<T> {
...
// 此处用 default 关键字,保证了向前兼容
// 如果没有 default 关键字,并声明了此方法,那么代码中所由 Iterable 的实现类都需要加上此方法的默认实现
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}fail-fast
学习一下 ArrayList 是如何进行 fail-fast 处理的。 AbstractList 中声明了一个变量 modCount:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// modCount 用来计数,每当 list 发生了结构性变化时计数 +1
// Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.
// 结构性变化指的是 list 的 size 发生了改变,或者在 list 上执行的行为可能会对正在进行的迭代(iterations)产生不正确结果
// This field is used by the iterator and list iterator implementation returned by the iterator and listIterator methods. If the value of this field changes unexpectedly, the iterator (or list iterator) will throw a ConcurrentModificationException in response to the next, remove, previous, set or add operations.
// 这个字段会被 iterator 和 list iterator 使用。如果在对迭代器进行 next、remove、previous、set、add 操作时,modCount 的改变不在预期内,迭代器会抛出一个 ConcurrentModificationException 异常。
//This provides fail-fast behavior, rather than non-deterministic behavior in the face of concurrent modification during iteration.
// 这些操作提供了 fail-fast 行为,而不是在迭代过程中面对并发修改的不确定性(non-deterministic)行为
// 我想的这个操作提供的就是快速失败,而不是在内部处理并发带来的复杂度问题。
protected transient int modCount = 0;
}再回看一下 Iterator 的代码:
private class Itr implements Iterator<E> {
// 创建迭代器的时候,复制当前 ArrayList 的 modCount 到执行迭代过程中期望的 expectedModCount
int expectedModCount = modCount;
...
public E next() {
checkForComodification();
...
}
public void remove() {
checkForComodification();
...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
private class ListItr extends Itr implements ListIterator<E> {
public E previous() {
checkForComodification();
...
}
// 可以看到 set 方法是调用了 ArrayList 的 set 方法,ArrayList 的 set 方法因为 list 发生了结构性变化,所以 modCount + 1
public void set(E e) {
checkForComodification();
try {
// 此处的 this 是指向 ListItr,需要使用 ArrayList.this 来获取到外部的 ArrayList 的 this
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}Q&A
Q1: ChenHsingYu
这个 modCount 还是不太理解。如果 Iterator 在迭代的过程中,ArrayList 的某个元素被删除,那就会引起元素移动,然后会导致 Iterator 迭代出错?
A1:
没错,如果在迭代过程中,不是由 Iterator 本身执行了删除操作,那么就会抛出 ConcurrentModificationException。
这里看下 Iterator 的 remove() 方法。
public void remove() {
// 边界校验
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 可以看到此处还是执行 ArrayList 的 remove 方法
// 复习一下,这里的 remove 是一个结构性变化,所以此处进行了 modCount++
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 关注此处,修改后的 modCount 被重新赋值给了 Iterator 的 expectedModCount
// 可以看到下面贴上的 checkForComodification() 方法
// 它实际上就是比较 Iterator 的 expectedModCount 和 ArrayList 的 ModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}Q2: skyli
普通的 for 循环如for (int i = 0; i < x.size(); i++),循环过程中可以直接删除元素,并且不会抛出ConcurrentModificationException。这也是正确的操作过程吗?
A2:
这个是不正确的操作流程,这样的 fori 中进行 remove 操作会造成意料之外的结果,每次删除都会导致数组元素位移从而导致想要删除的元素和真实被删除的元素不相符。这是一个不会抛出错误,但是会造成异常结果的,更危险的行为。
如上所述,循环过程中删除元素一定要使用 Iterator 的 remove() 方法进行删除。
Q3: mackong
是什么地方做了处理,使得implements了RandomAccess的接口的 fori 速度大于 iterator 的速度的?
A3: ChenHsingYu
RandomAccess 属于一种 Marker Interface,即仅仅起标记作用。
我觉得这个 Marker Interface 有个好处就是你可以根据数据结构是否实现 RandomAccess 来决定你的迭代方式。
if (list instanceof RandomAccess){
for (int i = 0;i < list.size();i++){
// ...
}
} else {
Iterator it = list.iterator();
while(it.hasNext()){
// ...
}
}Java Doc - Interface RandomAccess 有一段话
Generic list algorithms are encouraged to check whether the given list is an instanceof this interface before applying an algorithm that would provide poor performance if it were applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable performance.
参考: - docs.oracle.com/javase/8/do… - juejin.cn/post/684490…
Q3 附:
那么问题来了,什么情况下fori 会比迭代器执行的速度快?
多数情况下,连续内存块的 fori 循环是比迭代器快的吧。个人的理解。
Q4: ChenHsingYu
为什么数据结构不能定义成transient T[] elementData呢?其他数据结构也是使用Object来代替T,那使用Object[]的好处是什么。
A4:
好问题。看下下面的代码:
// 编译报错,不能直接 new T[10]; 此时 T 类型不可知,不允许此操作
T[] elementData = new T[10];那曲线救国看一下:
// 虽然 IDE warn Unchecked cast: 'java.lang.Object[]' to 'T[]',但还是可以编译通过的
// 这种形式虽然可以,但是会发现其实本质上创建的还是一个 Object[] 数组
transient T[] elements = (T[]) new Object[1];实际上这里会引出一个 Restrictions on Generics,其中提到的 Java 在使用泛型时的一些约束。