欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JDK源码剖析与最佳实践—ArrayList

程序员文章站 2022-06-16 16:24:46
...

知其然,需知其所以然。——古语

知其所以然,需引而伸之,触类而长之;——虫草

最近准备研究下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


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++;
}
其中forEach方法参数为函数式方法,这里会遍历每个数组执行对应的操作。spliterator方法返回Spliterator对象,实际是一个可以并行遍历的迭代器,ArrayList从Collection中继承的stream方法就会用到这个方法去构造。removeIf方法参数也会函数式方法,Predicate是个判断条件的函数接口,条件满足时就会将元素删除。replaceAll方法的参数也是UnaryOperator函数接口,这个接口可以执行操作,然后将对应的元素做替换。

这里Stream API与Lamda表达式一般都关联使用,目前对于这块原理还不是特别清楚,所以就不展开讲。后期理清楚了再补充。这里附几个看到的比较好的相关资料,有助于了解:
官方Lamda表达式教程
Stream语法详解
为什么需要 Stream