Java8 ArrayList 学习笔记

概述

java.util.Collection是Java集合类的基本接口,存储和操作一组类型相同的对象。而ArrayList是java语言最常用的集合类,是表ADT的一种实现。

ArrayList继承树

Imgur

实现

速览一下ArrayList的源码,详细介绍一下常用的方法。

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;

ArrayList中的数据存于一个数组Object[],可以理解为ArrayList对数组进行了一层封装,便于使用。

transient标记这个elementData不可序列化,而使用writeObject方法序列化ArrayList对象。

1
protected transient int modCount = 0;

还有个继承于AbstractList的属性很重要,用于记录list结构修改的次数,更详细的说明后文中会有。

get&set

1
2
3
4
5
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
1
2
3
4
5
6
7
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}

get和set方法都是对数组指定位置元素的操作。

add

1
2
3
4
5
6
7
8
9
10
11
/**
* 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;
}

add方法中需要先检查数组空间是否足够,如有必要则执行扩容,扩容使用Arrays.copyOf方法(底层调用System.arraycopy)

remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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;
}

remove方法实际也使用System.arraycopy

iterator

1
private class Itr implements Iterator<E>

ArrayList有个内部类Itr实现了Iterator接口,提供hasNext(),next(),remove()等方法,使调用者在迭代collection时能做删除操作。

可以用iterator()方法返回一个迭代器,在使用foreach语法实际也用的是这个Itr

1
2
3
public Iterator<E> iterator() {
return new Itr();
}

同理还有个ListIterator

fail-fast

之前提到ArrayList继承了AbstractList的一个非常重要的属性modCount,用于记录list结构改变的次数,在add(),remove()等代码中都有对modCount的自增操作。该属性用于iterator以及list iterator的实现,当modCount的值不符合预期,会抛异常ConcurrentModificationException。

以下代码就是典型的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误的代码 java.util.ConcurrentModificationException
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (String s : list) {
if ("2".equals(s)) {
list.remove(s);
}
}
}

然而,如果迭代器调用了它自己的remove方法,那么这个迭代器就仍然是合法的。

RandomAccess

发现ArrayList还实现了一个标记接口RandomAccess,文档中解释说这个接口的作用是使list的实现在随机访问时有更高的效率。

1
2
3
4
5
6
7
8
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();

Spliterator

// TODO
java8新特性,有待研究,之后补上

小结

ArrayList提供了List ADT的一种可增长数组的实现,使用ArrayList的优点在于,对get和set调用花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在ArrayList的末端进行。

适合需要快速访问数据,较少插入或删除元素的场景。

变种

concurrent包下提供了CopyOnWriteArrayList

写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

以下代码是向ArrayList里添加元素,可以发现在add方法中是需要加锁的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

get方法则无需加锁

1
2
3
4
5
6
7
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}

当线程A执行修改操作,修改的是copy出来的新数组,其它读线程此时读取的任是原数组。

那么问题来了,修改操作会锁住整个List,是一个并发瓶颈;频繁的copy则会导致内存和gc相关的问题。CopyOnWriteArrayList不是一个通用的并发List,适合读多写少的场景。