ArrayList&LinkedList源码解读

出来看星星吗?不看星星出来也行。 / 2024-08-23 / 原文

ArrayList

概述

ArrayList实现了List接口,是顺序容器,允许放入null,底层通过数组实现,线程不安全,容量不足会自动扩容

transient Object[] elementData;//使用transient关键字不序列化某个变量
private int size;

构造函数

可以指定容量,默认为空,所以最少扩容一次,可以提前预估容量提升性能

    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() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

自动扩容逻辑

一般扩容1.5倍,超出最大值后扩容为Integer.MAX_VALUE

在可以预估扩容大小的时候可以使用内部公开方法ensureCapacity,提前扩容,以减少扩容次数。

    private void grow(int minCapacity) {
        //拿到旧的长度
        int oldCapacity = elementData.length;
        //设置新的为旧的1.5倍, 右移运算除以2
        int newCaacity = oldCapacity + (oldCapacity >> 1);
        //如果仍然满足不了最小需求,就设置为最小需求
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果已经超出了最大限制
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //将最大限制设置为Integer.MAX_VALUE
            newCapacity = hugeCapacity(minCapacity);
        //复制数组,本地方法
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

添加逻辑

每次添加之前会检查容量是否足够,不够会使用grow方法进行扩容

如果指定添加的位置,则需要先对元素进行移动,O(n)的复杂度

同样的删除指定位置也需要移动,删除最后一个位置不需要。

需要注意的是为了让GC起作用,防止内存泄漏,需要为删除位置显示置空

    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        modCount++;
        final int s;
        Object[] elementData;
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element;
        size = s + 1;
    }

缩容

指的是可以手动将数组空间调整为当前实际大小,防止空间浪费,也是通过移动数组实现的。

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

快速失败

任何结构性修改操作都会引发modCount++

当迭代器发现modCount被修改后,就会抛出ConcurrentModificationException异常(当前修改异常)

注意是结构性修改,例如add、remove等改变数组大小的操作

set操作修改某个特定位置的引用时,不会触发迭代器快速失败

LinkedList

概述

LinkedList使用双向链表实现了List和Deque接口,因此它可以作为顺序容器、队列和栈,但是这个类几乎不会被使用(但是我经常使用),一般定义栈或者队列可以使用ArrayDeque(我也得改过来),比LinkedList性能好很多。

因为使用了双向链表,所以决定了所有跟下标相关的操作都是线性时间。队首队尾操作是常数时间,线程不安全。

Deque<String> stack1 = new LinkedList<>();
Deque<String> stack2 = new ArrayDeque<>();

内部元素

LinkedList底层通过定义first和last指针指向第一个和最后一个元素,但时候链表为空的时候没有dummy节点,都指向null。

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

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 LinkedList() {
    }

    
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

涉及方法

getFirst()
getLast()
removeFirst()
removeLast()
remove(e)
remove(index)

因为是双向链表,所以删除时只需要unlink就行,由JVM进行垃圾回收

    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final ode<E> prev = x.prev;

        if (prev == null) {// 第一个元素
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {// 最后一个元素
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null; // GC
        size--;
        modCount++;
        return element;
    }

清空链表

LinkedList为了让GC可以更快速回收元素,提供了clear方法,清除所有节点间的引用关系。

    public void clear() {
        // Clearing all of the links between nodes is "unnecessary", but:
        // - helps a generational GC if the discarded nodes inhabit
        //   more than one generation
        // - is sure to free memory even if there is a reachable Iterator
        for (Node<E> x = first; x != null; ) {
            Node<E> next = x.next;
            x.item = null;
            x.next = null;
            x.prev = null;
            x = next;
        }
        first = last = null;
        size = 0;
        modCount++;
    }

快速失败

在遍历过程中对集合进行了结构性修改(如添加或删除元素),LinkedList的迭代器会抛出ConcurrentModificationException异常。

原理仍然是维护了一个modCount变量,每次结构性修改时++。

发散

在ArrayList中大量使用了Arrays.copyOf方法

方法传入(原始数组、新数组长度、新数组类型)进行数组扩容

这个方法底层其实是一个native方法System.arraycopy

System.arraycopy利用了底层系统的内存复制功能进行数组复制

  • 首先创建一个新数组,定义复制参数

    • src:源数组,即要从中复制元素的数组。
    • srcPos:源数组中的起始位置,从这个位置开始复制元素。
    • dest:目标数组,即要将元素复制到的数组。
    • destPos:目标数组中的起始位置,复制到这个位置。
    • length:要复制的元素数量。