JDK源码剖析与最佳实践—ArrayList
知其然,需知其所以然。——古语
知其所以然,需引而伸之,触类而长之;——虫草
最近准备研究下JDK源码,把常用的一些类作个剖析整理,出个系列文章。ArrayList应该是在开发过程中非常高频使用的一个集合类,就先拿这个类开刀了。
笔者使用的JDK版本为:1.8.0_102,由于源码太多,有些也比较简单,所以挑一些重点说明下。
一、整体介绍
ArrayList类如其名,是一个可以动态扩容的数组列表,是List家族中的一员,支持随机访问,而且在JDK8中新支持了Stream API,使用起来还是非常方便的。不过该类不是线程安全的,所以在多线程情况下需要小心使用。
二、源码剖析与实践
2.1 成员变量
transient Object[] elementData;
private int size;
其中elementData即为ArrayList实现依赖的数组,size为ArrayList实际包含的元素的个数。这里elementData有transient,为什么要加这个呢?看后面的2.5小节。
2.2 构造函数
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,具体过程是先将参数转化成对应数组,再调用Arrays.copyOf方法进行元素的复制,而Arrays.copyOf方法实际是基于System.arraycopy这个本地方法执行的操作。这两个方法在ArrayList被大量用到,主要就是用作数组间的元素拷贝。
最佳实践:在Collection家族的集合类需要转化成ArrayList时,不需要遍历设值,可以直接使用构造方法,这样代码清晰简单,而且性能也好些。
2.3 扩容方法
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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;
}
扩容方法主要看ensureCapacityInternal这个私有方法,该方法在列表添加元素时会被调用,以保证数组容量足够。其它扩容相关私有方法都是该方法去调用的。其中比较关键的是grow方法,参数minCapacity为即将达到的元素个数,newCapacity为旧数组容量的1.5倍,哪个值大以哪个容量为准。不过这里还需要注意的是ArrayList的最大长度也是有限制的,最大也只有Integer.MAX_VALUE,而且再设置成最大之前,还会先设置容量为MAX_ARRAY_SIZE 。
为什么先设置成MAX_ARRAY_SIZE呢?这里源码给出的注释是MAX_ARRAY_SIZE是最大分配的数组大小,因为有些虚拟机还存储一些头信息在数组里,数组容量分配过大可能会有OutOfMemoryError。所以其实更安全的最大数组长度是MAX_ARRAY_SIZE。当然实际情况长度很少可能这么长。
另外这里扩容的时候为什么是原来的1.5倍呢?原因是如果扩容倍数太大,比如2.5倍,那么占用的内存会太大,浪费的内存也会相应多;而扩容太小,比如1.1倍,那么后期元素增加时又需要对数组重新分配内存,消耗性能。所以1.5倍是个经过测试的折衷值。【另可参见《编写高质量代码》建议63】
最佳实践:由于ArrayList动态扩容的特性,而且大多数情况下是扩容1.5倍,所以在已知列表容量时,最好先为列表指定初始化容量,这样可以避免内存空间的浪费,以及扩容过程数组复制的性能开销。
2.4 差集与交集
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
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++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
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;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
这里removeAll方法是将在参数集合中存在的元素中从list中删除,相当于ArrayList与集合参数c的差集;而retainAll方法是将参数集合中不存在的元素中从list中删除,相当于ArrayList与集合参数c的交集。
这里两个方法都调用的是batchRemove方法,只是complement值传的不同,去判断到底是留包含的还是不包含的。这里有逻辑很多逻辑是写在finally的,是为了保证contains判断报错时也正常执行。另外中间有个elementData[i] = null操作,把所有用不到的元素置为null,这样也便于垃圾回收。
最佳实践:(1)集合之间取并集用addAll,取差集用removeAll,取交集用retainAll;(2) 如果数组元素用不到可以置为空,另外在代码里面如果用到一些的List对象,如果不用了,最好调用clear方法,特别是大List或循环创建的。
2.5 序列化与反序列化
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();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
前面介绍成员变量的时候elementData是有transient标识的,即elementData是不会被序列化的。那ArrayList里面的数据到底会不会序列化呢?上面的代码可以看到ArrayList重写了writeObject与readObject方法,即覆盖了默认的序列化实现。序列化与反序列化的时候都是只序列化了List的数组中实际存在的元素,所以ArrayList加transient标识的目的只是为了避免默认序列化机制把整个数组都序列化了,然后自实现了序列化与反序列化方法,将真实存在的数据进行了序列化操作。
2.6 子列表
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
}
ArrayList提供了subList这个方法得到一个指定下标范围内的子列表视图,这里下标范围检查就不看了,所有的List下标相关操作都需要做个是否越界的检查。关键看下SubList 这个内部类,这里限于篇幅代码没有贴全,重点看下这个内部类的成员变量就可以了,特别注意下parent这个是父列表,而实际上所有子列表操作中其实都是操作的父列表。
另外看下SubList 这个内部类的checkForComodification方法,子列表几乎所有操作都会先调用checkForComodification方法,这个方法主要是检查modCount这个值是否相等,就是为了避免父列表修改的。因为父列表有修改时modCount值会增加,而造成不相等,会抛出异常,所以如果得到了子列表,父列表元素不能有变更操作(增删操作)。
最佳实践:(1)子列表的所有操作都会改变原列表,所以有些场景下想修改列表的某部分数据,可以直接得出子列表进行修改,就会修改原列表了;(2)得到子列表后,不要再直接修改原列表了,否则会抛异常。
2.7 Stream API
其中forEach方法参数为函数式方法,这里会遍历每个数组执行对应的操作。spliterator方法返回Spliterator对象,实际是一个可以并行遍历的迭代器,ArrayList从Collection中继承的stream方法就会用到这个方法去构造。removeIf方法参数也会函数式方法,Predicate是个判断条件的函数接口,条件满足时就会将元素删除。replaceAll方法的参数也是UnaryOperator函数接口,这个接口可以执行操作,然后将对应的元素做替换。
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
public Spliterator<E> spliterator() {
return new ArrayListSpliterator<>(this, 0, -1, 0);
}
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
elementData[i] = operator.apply((E) elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
这里Stream API与Lamda表达式一般都关联使用,目前对于这块原理还不是特别清楚,所以就不展开讲。后期理清楚了再补充。这里附几个看到的比较好的相关资料,有助于了解:
官方Lamda表达式教程
Stream语法详解
为什么需要 Stream