ArrayList 的家族成员

248 阅读6分钟

Hi,大家好。本篇文章我 ( ArrayList )来为大家介绍介绍我的家族成员。

首先登场的是比我年纪还大的 Vector

Vector

我们打开 Vector 的源码,可以看到。他在 JDK1.0 时便被 Lee Boynton, Jonathan Payne 创建了出来。JDK1.2 的时候被修改实现 List 接口。正式成为 Collection 集合的一员。

Vector 的功能

Vector 实现的功能基本与我( ArrayList )类似,只不过他有一项特殊技能 -- 能保证线程安全的操作元素。

为什么现在已经不常用 Vector 了

  1. 由于保证线程安全的特殊技能的代价就是每一次的添加删除都要进行 锁的竞争,导致性能下降。
  2. JUC (java.util.concurrent)包问世之后,多线程竞争问题也有了良好的解决办法。 因此,Vector 已经退出了活跃的舞台,如果你不需要在多线程环境下使用,你可以使用我( ArrayList ),如果你需要在多线程环境下使用,可以找我的远方亲戚 CopyOnWriteArrayList

上文提及的多线程相关的知识如:锁的竞争JUCCopyOnWriteArrayList 将会在后面讲解多线程的时候进行详细概述,这里先挖个坑。

LinkedList

LinkedListJava 世界中对于数据结构 --链表的标准实现。

链表

链表是一种线性数据结构,其中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。

链表可分为以下几种类型:

单向链表

image.png

一个单向链表的节点被分成两个部分。第一个部分保存或者显示关于节点的信息,第二个部分存储下一个节点的地址。单向链表只可向一个方向遍历。

循环链表

image.png

它的最后一个结点指向头结点,形成一个环。因此,从循环链表中的任何一个结点出发都能找到任何其他结点。循环链表的操作和单链表的操作基本一致,差别仅仅在于算法中的循环条件有所不同。

双向链表

image.png

它的每个数据结点中都有两个指针,它们分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

双向循环链表

image.png

大致与双向链表相同,只是将该链表的头节点和尾节点相连。

相比队列链表的优缺点

我们之前已经了解过 ArrayList —— 也就是数组队列的实现。现在我们来总结一下相关的优缺点:

查询

  • 数组队列:可以通过数组下标迅速查询到数据,支持随机访问,其时间复杂度为 O(1)
  • 链表:需要遍历所有的元素才能查询到指定的数据,不支持随机访问,时间复杂度为 O(n)

插入/删除

  • 数组队列:在修改节点后需要平均移动 n/2 个元素,时间复杂度O(n)
  • 链表:在修改节点后直接移动指针即可,时间复杂度O(1)

内存占用

  • 数组队列:简单易用,在实现上使用连续的内存空间,可以借助CPU的缓冲机制预读数组中的数据,所以访问效率更高
  • 链表:在内存中并不是连续存储,所以对CPU缓存不友好,没办法预读。

LinkedList 如何实现

Java 世界中的 LinkedListJDK 1.7 的时候开始就采用双向链表实现。下面我们来一起看下它的源码。

主要结构

LinkedList 内部节点为 Node,其中有三个指针:

  • item :当前节点元素
  • next :下一个节点元素
  • prev :上一个节点元素

可见源码:

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;
    }
}

插入

直接上源码:

/**
 * Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any
 * subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    checkPositionIndex(index);  // (1)

    if (index == size)
        linkLast(element);  // (2)
    else
        linkBefore(element, node(index));  // (3)
}
  • (1)检查指定的 index 是否超出链表的长度,如果超出则抛出异常。
  • (2)如果指定的 index 等于链表的长度,则直接连接到最后一个节点后面。
  • (3)将节点连接到指定index的前面。
linkLast

该方法也是比较显而易懂的。

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;  // (1)
    if (l == null)    // (2) 
        first = newNode;  // (3)
    else
        l.next = newNode; // (4)
    size++;
    modCount++;
}
  • (1)将新的节点赋值给尾节点。
  • (2)如果该链表为空。
  • (3)则设置为头节点。
  • (4)则设置为之前尾节点的下一个节点。
linkBefore

该方法比 linkLast 稍微复杂些,但大体一致。

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;  (1final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode; (2if (pred == null) (3)
        first = newNode;
    else
        pred.next = newNode; (4)
    size++;
    modCount++;
}
  • (1)获取指定 index节点的前一个节点
  • (2)将指定 index 节点的前一个节点设置为新插入的节点
  • (3)如果指定 index 节点的前一个节点不存在( 指定index节点为头节点 )
  • (4)将指定 index 节点的前一个节点的 next 指向我们新插入的节点。

查找

// Positional Access Operations

/**
 * Returns the element at the specified position in this list.
 *
 * @param index index of the element to return
 * @return the element at the specified position in this list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

LinkedList 给了我们一种根据 index 获取元素的能力。

node

该方法展示了如何根据 index 获取我们所需元素。

/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

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

-(1) 在这里使用一次二分法来提升查询效率 -(2) 从头开始遍历,直到获取指定值。 -(3) 从尾开始遍历,直到获取指定值。

删除

LinkedList可以根据index、对象等进行删除。

image.png

由于大致方法基本一致,所以我们以 index 来做讲解。

/**
 * Removes the element at the specified position in this list.  Shifts any
 * subsequent elements to the left (subtracts one from their indices).
 * Returns the element that was removed from the list.
 *
 * @param index the index of the element to be removed
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
unlink
/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item; (1final Node<E> next = x.next; (2final Node<E> prev = x.prev;(3if (prev == null) {
        first = next; (4)
    } else {
        prev.next = next; 
        x.prev = null;(5)
    }

    if (next == null) {
        last = prev;   (6)
    } else {
        next.prev = prev;
        x.next = null;  (7)
    }

    x.item = null; (8)
    size--;
    modCount++;
    return element;
}
  • (1)获取当前要删除的节点
  • (2)获取当前要删除节点的前一个节点 ( 暂且称之为 Prev 节点 )
  • (3)获取当前要删除节点的后一个节点 ( 暂且称之为 Next 节点 )
  • (4)如果 Prev 节点为空 ,则代表我们要删除的节点是头节点,则我们需要把 Next 节点变成头节点。
  • (5)如果 Prev 节点不为空,则把 Prev 节点的 next 指针指向 Next 节点,将要删除节点的 prev 指针变为空 ( GC相关知识 )。
  • (6)如果 Next 节点为空,则代表我们要删除的节点是尾节点,则我们需要把 Prev 节点变成尾节点。
  • (7)如果 Next 节点不为空,则把 Next 节点的 prev 指针指向 Prev 节点,将要删除节点的 next 指针变为空。
  • (8)将要删除的元素变为空,等待 GC 回收该内存。

总结

本节带领大家了解了 List 家族中的一些成员 —— VectorLinkedList。并讲解了相关数据结构 —— 链表的相知识。

下节我们将介绍也被大家常用的一个集合 —— Map 集合