Java 集合框架【2】List 体系

List Interface

List 接口继承自 Collection , 除了继承了 Collection 中的能力,自身拓展了几个默认方法:

// 批量修改操作
default void replaceAll(UnaryOperator<E> operator) 
// 排序,使用的是 Arrays.sort 
default void sort(Comparator<? super E> c) 

// 位置访问操作
E get(int index);
E set(int index, E element);
void add(int index, E element); // Collection 有 add ,这里拓展了指定 index 。
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);

// 迭代器
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);

// 容器
List<E> subList(int fromIndex, int toIndex);
复制代码

这里简单介绍一下,List 相较于 Collection 拓展的功能:

  • 批量操作增加排序,说明 List 是有序的
  • 位置访问操作,说明 List 是可以进行位置索引的
  • 容器支持了子 List ,可以对 List 进行截取。

实现

List 接口的实现包括:VectorArrayListLinkedList

ArrayList

基于动态数组实现,支持随机访问。允许存储包括 null 的元素,除了实现 List 接口外,还提供了一些方法用来处理数组扩容的方法。 它与 Vector 的区别在于,Vector 是同步的,所有操作方法都加了 synchronized 修饰。而 ArrayList 没有。

继承关系

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
复制代码

ArrayList 继承自 AbstractList , 并实现了 ListRandomAccess. CloneableSerializable

AbstractList

List 接口的一些方法提供了默认实现,最简单的例子是,List 接口里面有多态的 add 方法,对其进行了包装:

public boolean add(E e) {
    add(size(), e);
    return true;
}

public void clear() {
    removeRange(0, size());
}
复制代码

还对一些索引操作进行了默认实现:

public int indexOf(Object o) {
    ListIterator<E> it = listIterator();
    if (o==null) {
        while (it.hasNext())
            if (it.next()==null)
                return it.previousIndex();
    } else {
        while (it.hasNext())
            if (o.equals(it.next()))
                return it.previousIndex();
    }
    return -1;
}
复制代码

另外,还实现了内部私有迭代器 Itr 和子列表类型 SubList

RandomAccess

用于对支持随机访问的数据结构进行标记。主要作用是,在一些算法中,针对随机访问和顺序访问,会有不同的性能。算法能够识别出这个接口标记的数据结构是随机访问的。

Cloneable

复制能力,实现该接口的类可以通过 Object.clone() 对该类的实例进行逐个字段的复制。

Serializable

序列化能力。

底层实现

transient Object[] elementData; 

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}
复制代码

ArrayList 内部真实存储数据的结构是一个数组。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
复制代码

添加数据是顺序的在数组尾部添加新元素,当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。

扩容逻辑

Add 方法中,第一个调用的是 ensureCapacityInternal(int) :

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
复制代码

calculateCapacity 方法是用来计算扩容数量的:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
复制代码

elementData 为空数组时,初次扩容是返回 10 和 minCapacity 较大一方的值。 否则返回的是 minCapacity。 而 add 方法中是 size + 1 也就是说,以每次扩容一个进行的。这样始终保持了元素数量 = 数组容量,不会造成内存空间的浪费。

真正的扩容方法在 ensureExplicitCapacity 中:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 数组修改次数增加
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity); // 真正扩容方法
}
复制代码

内部逻辑也很简单,当数组需要的最小容量 minCapacity 大于 elementData 时,进行扩容,通过真正的扩容方法 grow(int) 来进行:

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);
}
复制代码

ArrayList 还对外提供了节省内存空间的自动调整容量方法:

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA // EMPTY_ELEMENTDATA = {}
          : Arrays.copyOf(elementData, size);
    }
}
复制代码

Fail-Fast机制

ArrayList 也采用了快速失败的机制,通过记录 modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

时间复杂度

因为底层实现是数组,所以它的 sizeisEmptygetset 的时间复杂度都是 O (1) 级的。

Vector

继承关系

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
复制代码

继承关系和 ArrayList 一模一样。

底层实现

protected Object[] elementData;

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector() {
    this(10);
}
// ...
复制代码

底层实现也和 ArrayList 相同。

扩容逻辑

扩容逻辑也是从 add 开始,这里有个不同是,add 加了 synchronized 关键字修饰:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
// 和 ArrayList 逻辑相同
private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
复制代码

与 ArrayList 的区别

底层实现都基本相同,唯一的区别是 Vector 是线程安全的。

栈结构的实现

Vector 有一个子类 Stack,也就是栈结构类型。表示对象的后进先出堆栈。Stack 继承自 Vector ,并拓展了五个允许将容器视为栈结构的操作。 包括常见的 poppush 操作、以及查看栈顶元素的方法、检查栈是否为空的方法以及从栈顶向下进行搜索某个元素,并获取该元素在栈内深度的方法。

Deque 接口及其实现提供了一组更完整的 LIFO 堆栈操作能力,应该优先考虑使用 Deque 及其实现。例如:

Deque<Integer> stack = new ArrayDeque<Integer>();
复制代码

LinkedList

基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。

继承关系

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
复制代码
  • AbstractSequentialList:这是一个 List 接口的骨架实现,数据处理逻辑是基于顺序访问的数据结构(例如链表)来实现的。
  • Deque:双向队列,支持从两端对元素进行插入和移除。
  • Cloneable:对象复制。
  • Serializable:序列化能力。

底层实现

transient Node<E> first;
transient Node<E> last;

public LinkedList() {
}
复制代码

通过 firstlast 表示双向链表的两个方向的头节点。Node 的数据结构是:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
复制代码

添加操作:

public boolean add(E e) {
    linkLast(e);
    return true;
}
复制代码

真实的添加操作:

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
复制代码

删除操作:

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x); // x 的 next 和 prev 置为null
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
复制代码

查找操作:

Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
复制代码

查找操作的实现是进行了优化的,size >> 1 的值 size / 2 ,如果是在链表的左侧,则从前查找,否则则从尾部向前查找。

时间复杂度

等同于链表结构操作的时间复杂度, O(n) 。

其他能力

因为 LinkedList 实现了 Deque,所以它可以看作是一个队双向列,也可以当栈结构去用。(Java 已声明不建议使用 Stack 类)。栈或队列,现在的首选是 ArrayDeque,它有着比 LinkedList (当作栈或队列使用时)有着更好的性能。

另一方面,它的操作方法并没有进行同步处理,所以不是线程安全的。

LinkedList 基于双向队列的能力,提供了一些获取头节点和尾部节点的方法,这些方法后续在 Queue 接口中详细讲解。这里了解一下基本内容:

boolean offer(E e); // 将元素插入到队列尾部中,返回插入结果
E peek(); // 检索队列头部元素但不删除,如果队列为空,返回 null
void push(E e); // 将元素插入到队列头部 等效于 addFirst
E pop(); // 将队列第一个元素删除,等效于 removeFirst
复制代码

总结

  1. Vector 和 ArrayList 的实现基本一致,底层数据结构都是数组,最大的区别是 Vector 是线程安全的。
  2. LinkedList 不是线程安全的,它的底层实现是双向链表。
  3. LinkedList 不光可以当作双向队列使用,还可以当作栈。

猜你喜欢

转载自juejin.im/post/7097225402774454308