List 集合

#Java基础 #集合 #List #ArrayList #LinkedList

ArrayList

是动态数组, 能够进行扩容, 但线程不安全. ArrayList 的底层实现是 Object[] 数组, 因此可以存放任何数据类型(包括 null).

构造函数

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_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);
    }
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object [] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

自动扩容

  1. 每次添加元素(调用 add())去检查添加后是否会超过长度(size == elementData.length), 扩容通过 ensureCapacity 来实现, 会扩容至原来的 1.5 倍 大小.
  2. 当扩容时, JVM 会直接在当前 Heap 中寻找一块足够容纳扩容后数组大小的内存, 而不是在原位置进行扩容.
  3. 如果没有满足的连续内存则会触发垃圾回收机制, 如果进行垃圾回收之后依旧没有足够内存则会抛出 OOM (OutOfMemory).

扩容详细过程

grow() (JDK 8)
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

[!NOTE] Title 注意:JDK11 移除了 ensureCapacityInternal()ensureExplicitCapacity() 方法, 直接在 add 内 modCount++

线程安全性

Collections 中绝大多数的实现非线程安全, ArrayList 也不例外.

特性 / 项目 Vector ArrayList
是否可扩容 可以(默认扩容 2 倍 可以(默认扩容 1.5 倍
线程安全性 线程安全(所有 public 方法加 synchronized 非线程安全
运行效率 (同步开销大,即使单线程也加锁) (无同步开销)
Fail-Fast 机制 Fail-Safe(部分版本)或弱 Fail-Fast(早期 JDK 中 Vector 的 Enumeration 不是 fail-fast;Iterator 是 fail-fast) Fail-Fast(通过 modCount 检查并发修改)
初始化容量 - 无参构造:默认容量 10- 有参构造:指定容量 - 无参构造:初始为 0(JDK 8+),首次 add 扩容到 10- 有参构造:指定容量
扩容策略 - 未指定增量:扩容为 2 倍- 指定 capacityIncrement > 0:每次增加该值 - 默认:newCapacity = old + (old >> 1)(1.5 倍)- 若仍不足,则直接扩到所需最小容量
是否实现 RandomAccess

LinkedList

LinkedList 底层实现为双向链表, 在 JDK1.7 之前 LinkedList 底层为双向循环链表. 因为底层为链表, 所以每个 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; 
    }
}

LinkedList 插入与删除元素时间复杂度

LinkedList 默认存储着头节点和尾节点的指针.

  • 在头尾部插入元素的复杂度为 O(1)
  • 在其他位置插入元素时选择距离最近的指针遍历到节点处插入, 总时间复杂度为 O(n)

为什么 LinkedList 不支持快速随机访问?

LinkedList 底层为链表, 链表在物理内存中的分布不连续, 只能通过指针进行定位所以不能实现.

特性 / 操作 LinkedList ArrayList
底层结构 双向链表(每个节点包含数据 + 前后指针) 动态数组(Object [] 数组)
内存占用 较大(每个元素额外存储两个引用:prev 和 next) 较小(仅存储元素本身,但可能有未使用的预留空间)
是否实现 RandomAccess 是(支持快速随机访问)
头部插入/删除 O(1)(直接修改头节点指针) O(n)(需移动所有后续元素)
尾部插入/删除 O(1)(维护了 tail 指针) 摊销 O(1)(通常无需移动;扩容时 O(n))
中间位置插入/删除 O(n)(需遍历到目标位置) O(n)(需移动插入点之后的所有元素)
随机访问(get/set) O(n)(需从头或尾遍历) O(1)(直接通过索引访问数组)
扩容机制 无需扩容(动态链接节点) 需要扩容(默认增长为原容量的 1.5 倍)
迭代性能 略慢(指针跳转,缓存局部性差) 快(连续内存,缓存友好)

RandomAccess 接口

一个标记接口,用于告诉算法(如 Collections.binarySearch)该列表是否支持高效随机访问。ArrayList 实现了它,LinkedList 没有。

Vector

TODO: 待补充