本文会对List的各种实现基于使用场景,时间复杂度、代码细节、注意事项等维度做分析。

List 详解 - 图1

Iterator 和 ListIterator

Iterator 仅仅定义了三个方法 hasNext() next() remove()
ListIterator 继承自Iterator 并新增了 hasPrevious() previous() nextIndex() previousIndex() set(E e) add(E e) 方法
这意味着List的有序性赋予了ListIterator双向遍历的能力,同时因为有序支持索引那么对索引处元素的操作也成为了可能。

AbstractList

AbstractList 给List实现类提供了一个基本骨架。jdk里的List实现类都是通过继承它来实现的。

  • 所有List的实现类按照Collection规范都应该提供一个参数为Collection的构造方法,即:
    public XXXList(Collection c);
  • 如果需要实现一个不可修改的List,只需要继承它并且实现 get(int) 和 size() 方法即可。
  • 如果需要实现一个可以修改的List,则需要重写 set(int, Object) set(int, E)方法,这里的可以修改和不可修改仅仅是指List某个index的元素的值是否可以修改。
  • 如果需要一个可变长度的list则需要额外重写 add(int, Object) add(int, E) 以及 remove(int) 方法。
  1. public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
  2. ...
  3. abstract public E get(int index);
  4. public E set(int index, E element) {
  5. throw new UnsupportedOperationException();
  6. }
  7. public void add(int index, E element) {
  8. throw new UnsupportedOperationException();
  9. }
  10. public E remove(int index) {
  11. throw new UnsupportedOperationException();
  12. }
  13. ...
  14. }

通过源码可知,如果尝试对一个不可变的List做出修改操作将抛出UnsupportedOperationException

AbstractList并没有定义存储数据的结构,需要由继承类自己来定义,如ArrayList是用数组来存,LinkedList是用链表来存。

下面是一个简单的基于数组的不可变list的实现

  1. import java.util.AbstractList;
  2. import java.util.Collection;
  3. public class UnmodifiableArrayList extends AbstractList {
  4. private Object[] arr;
  5. public UnmodifiableArrayList(Collection c){
  6. if(c!=null){
  7. arr = new Object[c.size()];
  8. int i = 0;
  9. for (Object o:c){
  10. arr[i]=o;
  11. i++;
  12. }
  13. }else{
  14. arr = new Object[0];
  15. }
  16. }
  17. @Override
  18. public Object get(int index) {
  19. return arr[index];
  20. }
  21. @Override
  22. public int size() {
  23. return arr.length;
  24. }
  25. }

AbstractList 对subList(int,int)方法也有一个基础的实现返回的是一个SubList,LinkedList就是沿用这个方法的。需要注意的ArrayList通过subList方法获取的SubList 和 AbstractList 中返回的不是同一类型 List.SubList 和 AbstractList.SubList 是两个不同的类,不要被类名误导了。

  1. class SubList<E> extends AbstractList<E> {
  2. private final AbstractList<E> l;
  3. private final int offset;
  4. private int size;
  5. SubList(AbstractList<E> list, int fromIndex, int toIndex) {
  6. ...//check
  7. l = list;
  8. offset = fromIndex;
  9. size = toIndex - fromIndex;
  10. this.modCount = l.modCount;
  11. }
  12. public E set(int index, E element) {...}
  13. public E get(int index) {...}
  14. public int size() {...}
  15. public void add(int index, E element) {
  16. rangeCheckForAdd(index);
  17. checkForComodification();
  18. l.add(index+offset, element);//看这里 index+offset
  19. this.modCount = l.modCount;
  20. size++;
  21. }
  22. public E remove(int index) {...}
  23. ...
  24. }

可以看出通过subList方法返回的SubList对象仅仅是原来List的一个视图,也就是说如果原List是只读List那么这个SubList也是只读的,如果原List可修改,那么对SubList中元素的修改会直接作用于原List。

ArrayList

ArrayList 是 AbstractList 基于数组的实现,数组可以通过动态扩容来满足元素个数的持续增长。但是就是因为这个扩容的过程,原本时间复杂度为O(1)的add操作,经过扩容过程均摊后时间复杂度变为了O(n)。
后面我们会具体分析下扩容的过程。
首先我们关注ArrayList的几个基本属性

  1. //默认的容量,如果构造方法中没有传入第一次扩容时数组将会变为这个长度
  2. private static final int DEFAULT_CAPACITY = 10;
  3. //通过 new ArrayList() 创建时,初始化大小为0 而 elementData指向的就是EMPTY_ELEMENTDATA
  4. private static final Object[] EMPTY_ELEMENTDATA = {};
  5. //用于存放数据的数组
  6. private transient Object[] elementData;
  7. //ArrayList包含元素的个数,区别于 elementData.length
  8. private int size;

接下来我们来对比下三个构造方法

  1. //带初始化容量的构造方法 数组初始化大小为 initialCapacity
  2. public ArrayList(int initialCapacity) {
  3. super();
  4. if (initialCapacity < 0)
  5. throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
  6. this.elementData = new Object[initialCapacity];
  7. }
  8. //不带初始容量的构造方法 数组初始化大小为 0
  9. public ArrayList() {
  10. super();
  11. this.elementData = EMPTY_ELEMENTDATA;
  12. }
  13. //传入集合的构造方法 数组初始化大小为 c.size()
  14. public ArrayList(Collection<? extends E> c) {
  15. elementData = c.toArray();
  16. size = elementData.length;
  17. if (elementData.getClass() != Object[].class)
  18. elementData = Arrays.copyOf(elementData, size, Object[].class);
  19. }

ArrayList容量伸缩
当arraylist中的数组被放满,不足以放入下一个元素的时候,arraylist会进行扩容操作,扩容的过程很简单,就是创建一个更大的数组,然后通过数组拷贝将原来的数据拷贝到新的数组中。具体的扩容方法如下

  1. private void grow(int minCapacity) {
  2. // overflow-conscious code
  3. int oldCapacity = elementData.length;
  4. int newCapacity = oldCapacity + (oldCapacity >> 1);
  5. if (newCapacity - minCapacity < 0)
  6. newCapacity = minCapacity;
  7. if (newCapacity - MAX_ARRAY_SIZE > 0)
  8. newCapacity = hugeCapacity(minCapacity);
  9. // minCapacity is usually close to size, so this is a win:
  10. elementData = Arrays.copyOf(elementData, newCapacity);
  11. }

可以看出 newCapacity = oldCapacity + (oldCapacity >> 1);

值得注意的细节是如果arraycopy的原数组和目标数组是同一个的话,应该将无用的坑位置空,help gc,这一点remove方法就做得很好

  1. public E remove(int index) {
  2. rangeCheck(index);
  3. modCount++;
  4. E oldValue = elementData(index);
  5. int numMoved = size - index - 1;
  6. if (numMoved > 0)
  7. System.arraycopy(elementData, index+1, elementData, index,
  8. numMoved);
  9. elementData[--size] = null; // clear to let GC do its work
  10. return oldValue;
  11. }

ArrayList 的add 和remove 操作的时间复杂度均为O(n) 而 get 和 set 方法时间复杂度均为 O(1)

LinkedList

LinkedList是List和Deque的链表实现方式,add操作的复杂度为O(1) remove(i) get(int)和set(int)操作的复杂度为O(n)
但是LinkedList对寻找第i个Node的方法做了一个小优化

  1. Node<E> node(int index) {
  2. if (index < (size >> 1)) {
  3. //如果index位置位于链表的前一半则从表头开始遍历
  4. Node<E> x = first;
  5. for (int i = 0; i < index; i++)
  6. x = x.next;
  7. return x;
  8. } else {
  9. //如果index位置位于链表的后一半则从表尾开始遍历
  10. Node<E> x = last;
  11. for (int i = size - 1; i > index; i--)
  12. x = x.prev;
  13. return x;
  14. }
  15. }

之所以能够这样做是因为记录了头部和尾部两个位置的引用,可能有人会想如果每个node都index都被记录下那时间复杂度岂不是成O(1)了?其实如果每个Node位置都记录那就跟ArrayList没啥区别了

Vector

Vector跟ArrayList相同都是基于数组实现的,一般操作的实现方式也大致相同,下面主要讲下两者间的区别

  • 扩容方式 Vector 可以设置每次容量满时容量增加多少,如果没有设置则按现有容量双倍扩容
  1. int newCapacity = oldCapacity + ((capacityIncrement > 0) capacityIncrement :oldCapacity);
  • 添加修改的方法均用synchronized修饰
  • 提供更多的方法如copyInto(…) trimToSize() 等

Vertor中的每个操作元素的方法都是线程安全的,但并不代表使用过程中不会出现线程安全问题。举个例子:

  1. if(!vector.contains(o)){
  2. vector.add(o);
  3. }

这里如果是多个线程来操作仍然可能存在vector中出现重复元素的可能,正确的做法是对这段代码加锁

总结

在List的使用过程中,我们要对读和写操作做一个考量,选择合理的数据结构。如果读操作比较频繁可以考虑使用ArrayList,因为ArrayList的get方法时间复杂度是常量级别,如果删除操作和写操作比较频繁,可以考虑使用LinkedList,因为对链表的插入删除操作远比数组拷贝效率高。
在使用ArrayList的时,我们需要对List的容量有一个比较好的评估,用来设置List的初始化大小,减少扩容带来的不必要开销。
此外在操作一个List的时候我们要确定这个List是不是只读的,这个List是不是视图List,避免程序出错。
需要线程安全的List,可以使用Vector,也可以通过Collections.synchronizedList(List list)获得,但是需要注意多个同步方法组合使用的情况。