本文将从源代码的角度对Java 最常用的集合类ArrayList进行介绍,代码版本为1.8_121。
继承结构
除了一些功能性的接口,ArrayList的继承大致可以看成是从Collectinotallow=>AbstractCollectinotallow=>AbstractList=>ArrayList
的一个继承流程,接下来依此看看这些类都做了什么工作。
Collection类
collection 类提供了三类接口:
- 查询接口
size() ,isEmpty,contains(),iterator,toArray
- 修改接口
add(), remove()
- 批量操作接口
containsAll(),addAll(),removeAll(),removeIf(),retainAll(),clear()
可以着重提一下的是boolean retainAll(Collection<?> c);
这个方法做的是一个交集操作,也就是只有参数里面有的才会被保留,返回是否做了修改。
另外还有一个removeIf()可以传入一个判定函数,类似于filter的作用,但是不用先转化为stream,不过需要注意的是这个方法不是线程安全的,
Collection类在1.8 之后针对并行计算做了一些支持,加入了几个方法,在这里顺便简单介绍一下:
-
spliterator()
,这个方法类似于iterator(),但是他返回的是一个分割迭代器,这个迭代器的特点是它有一个trySplit()函数,这个函数可以将这个分支迭代器进行分支,得到将流划分为多个分支的效果,以在多个核心上并行执行的效果。例如一个最简单的实现就是将剩下的还没遍历的元素划分为相等的两部分,将序号较小的一部分封装到一个Spliterator里返回,然后将当前Spliterator的index更新为中值加1 -
Stream<E> stream()
是函数式编程的支持,类似于使用Iterator遍历,但是可以对stream进行函数式编程的操作,如使用map、filter函数来处理Stream -
Stream<E> parallelStream()
,返回一个并行的stream
重要的Field
-
transient Object[] elementData;
保存数据的对象数组,从这里可以看出Java泛型底层使用的是类型擦除,直接使用了Object的数组保存数据 -
private int size;
数组的长度
重要方法实现
-
add(E)
,插入元素
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
非常简单的将一个元素放到数组中,但是需要注意的是这里之前的ensureCapacityInternal()
方法。这个方法的调用主要是做了两件事,第一是修改modCount,以便在Iteration里面能检测ConcurrentModification然后抛出异常,另外一个就是当长度不够时修改长度
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
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);
}
可以看到每次的容量是原来的1.5倍,或者直接增长到所需的大小或者是最大的大小,然后使用Arrays.copyOf()将数据拷贝到一个新的数组中去,所以在这里可以看到的是如果能够预知数组的大小,那么在一开始就设置大小会比较好,就能省去在增长容量时数据复制的时间。
- remove
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
在删除之后使用了arraycopy()
方法来移动列表,需要指出的是,arraycopy函数在处理src和dest相同的复制时会首先将需要复制的内容保存到一个临时的表里,然后再移动,所以在对一个容量超大的列表进行删除操作的时候,可以从这个角度寻找优化的空间。
为了印证删除过程中移动的时间开销,写了一个小测试程序,分别从头和从尾部逐个删除一个数组:
public void testArrayList() {
try {
int length = 100000;
ArrayList<Integer> list = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
list.add(i);
}
ArrayList<Integer> list2 = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
list2.add(i);
}
long begin = System.currentTimeMillis();
for (int i = 0; i < length; i++) {
list.remove(0);
}
System.out.println(System.currentTimeMillis()-begin);
begin = System.currentTimeMillis();
for (int i = length-1; i >=0; i--) {
list2.remove(i);
}
System.out.println(System.currentTimeMillis()-begin);
} catch (Exception e) {
e.printStackTrace();
}
}
输出为:
1098
6
可以看出在一个不是很大(100,000)的列表里分别从两头删除的性能差距已经到了150倍以上