【2w字乾貨】ArrayList與LinkedList的區別以及JDK11中的底層實現

1 概述

本文主要講述了ArrayListLinkedList的相同以及不一樣之處,以及二者的底層實現(環境OpenJDK 11.0.10)。java

2 二者區別

在詳細介紹二者的底層實現以前,先來簡單看一下二者的異同。node

2.1 相同點

  • 二者都實現了List接口,都繼承了AbstractListLinkedList間接繼承,ArrayList直接繼承)
  • 都是線程不安全的
  • 都具備增刪查改方法

2.2 不一樣點

  • 底層數據結構不一樣:ArrayList基於Object[]數組,LinkedList基於LinkedList.Node雙向鏈表
  • 隨機訪問效率不一樣:ArrayList隨機訪問能作到O(1),由於能夠直接經過下標找到元素,而LinkedList須要從頭指針開始遍歷,時間O(n)
  • 初始化操做不一樣:ArrayList初始化時須要指定一個初始化容量(默認爲10),而LinkedList不須要
  • 擴容:ArrayList當長度不足以容納新元素的時候,會進行擴容,而LinkedList不會

3 ArrayList底層

3.1 基本結構

底層使用Object[]數組實現,成員變量以下:數組

private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData;
private int size;
private static final int MAX_ARRAY_SIZE = 2147483639;

默認的初始化容量爲10,接下來是兩個空數組,供默認構造方法以及帶初始化容量的構造方法使用:安全

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

        this.elementData = EMPTY_ELEMENTDATA;
    }
}

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

下面再來看一些重要方法,包括:bash

  • add()
  • remove()
  • indexOf()/lastIndexOf()/contains()

3.2 add()

add()方法有四個:數據結構

  • add(E e)
  • add(int index,E e)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c

3.2.1 單一元素add()

先來看一下add(E e)以及add(int index,E eelment)併發

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) {
        elementData = this.grow();
    }

    elementData[s] = e;
    this.size = s + 1;
}

public boolean add(E e) {
    ++this.modCount;
    this.add(e, this.elementData, this.size);
    return true;
}

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

    System.arraycopy(elementData, index, elementData, index + 1, s - index);
    elementData[index] = element;
    this.size = s + 1;
}

add(E e)實際調用的是一個私有方法,判斷是否須要擴容以後,直接添加到末尾。而add(int index,E element)會首先檢查下標是否合法,合法的話,再判斷是否須要擴容,以後調用System.arraycopy對數組進行復制,最後進行賦值並將長度加1。dom

關於System.arraycopy,官方文檔以下:性能

在這裏插入圖片描述

一共有5個參數:測試

  • 第一個參數:原數組
  • 第二個參數:原數組須要開始複製的位置
  • 第三個參數:目標數組
  • 第四個參數:複製到目標數組的開始位置
  • 第五個參數:須要複製的數目

也就是說:

System.arraycopy(elementData, index, elementData, index + 1, s - index);

的做用是將原數組在index後面的元素「日後挪」,空出一個位置讓index進行插入。

3.2.2 addAll()

下面來看一下兩個addAll()

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    ++this.modCount;
    int numNew = a.length;
    if (numNew == 0) {
        return false;
    } else {
        Object[] elementData;
        int s;
        if (numNew > (elementData = this.elementData).length - (s = this.size)) {
            elementData = this.grow(s + numNew);
        }

        System.arraycopy(a, 0, elementData, s, numNew);
        this.size = s + numNew;
        return true;
    }
}

public boolean addAll(int index, Collection<? extends E> c) {
    this.rangeCheckForAdd(index);
    Object[] a = c.toArray();
    ++this.modCount;
    int numNew = a.length;
    if (numNew == 0) {
        return false;
    } else {
        Object[] elementData;
        int s;
        if (numNew > (elementData = this.elementData).length - (s = this.size)) {
            elementData = this.grow(s + numNew);
        }

        int numMoved = s - index;
        if (numMoved > 0) {
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
        }

        System.arraycopy(a, 0, elementData, index, numNew);
        this.size = s + numNew;
        return true;
    }
}

在第一個addAll中,首先判斷是否須要擴容,接着也是直接調用目標集合添加到尾部。而在第二個addAll中,因爲多了一個下標參數,處理步驟稍微多了一點:

  • 首先判斷下標是否合法
  • 接着判斷是否須要擴容
  • 再計算是否須要把原來的數組元素「日後挪」,也就是if裏面的System.arraycopy
  • 最後把目標數組複製到指定的index位置

3.2.3 擴容

上面的add()方法都涉及到了擴容,也就是grow方法,下面來看一下:

private Object[] grow(int minCapacity) {
    return this.elementData = Arrays.copyOf(this.elementData, this.newCapacity(minCapacity));
}

private Object[] grow() {
    return this.grow(this.size + 1);
}

private int newCapacity(int minCapacity) {
    int oldCapacity = this.elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity <= 0) {
        if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(10, minCapacity);
        } else if (minCapacity < 0) {
            throw new OutOfMemoryError();
        } else {
            return minCapacity;
        }
    } else {
        return newCapacity - 2147483639 <= 0 ? newCapacity : hugeCapacity(minCapacity);
    }
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) {
        throw new OutOfMemoryError();
    } else {
        return minCapacity > 2147483639 ? 2147483647 : 2147483639;
    }
}

grow()首先經過newCapacity計算須要擴容的容量,接着調用Arrays.copyOf將舊元素複製過去,並將返回值覆蓋到原來的數組。而在newCapacity中,有兩個變量:

  • newCapacity:新的容量,默認是舊容量的1.5倍,也就是默認擴容1.5倍
  • minCapacity:最低須要的容量

若是最低容量大於等於新容量,則是以下狀況之一:

  • 經過默認構造初始化的數組:返回minCapacity與10的最大值
  • 溢出:直接拋OOM
  • 不然返回最小容量值

若是不是,則判斷新容量是否達到最大值(這裏有點好奇爲何不用MAX_ARRAY_SIZE,猜想是反編譯的問題),若是沒有到達最大值,則返回新容量,若是到達了最大值,調用hugeCapacity

hugeCapacity一樣會首先判斷最小容量是否小於0,小於則拋OOM,不然將其與最大值(MAX_ARRAY_SIZE)判斷,若是大於返回Integer.MAX_VALUE,不然返回MAX_ARRAY_SIZE

3.3 remove()

remove()包含四個方法:

  • remove(int index)
  • remove(Object o)
  • removeAll()
  • removeIf()

3.3.1 單一元素remove()

也就是remove(int index)以及remove(Object o)

public E remove(int index) {
    Objects.checkIndex(index, this.size);
    Object[] es = this.elementData;
    E oldValue = es[index];
    this.fastRemove(es, index);
    return oldValue;
}

public boolean remove(Object o) {
    Object[] es = this.elementData;
    int size = this.size;
    int i = 0;
    if (o == null) {
        while(true) {
            if (i >= size) {
                return false;
            }

            if (es[i] == null) {
                break;
            }

            ++i;
        }
    } else {
        while(true) {
            if (i >= size) {
                return false;
            }

            if (o.equals(es[i])) {
                break;
            }

            ++i;
        }
    }

    this.fastRemove(es, i);
    return true;
}

其中remove(int index)的邏輯比較簡單,先檢查下標合法性,而後保存須要remove的值,並調用fastRemove()進行移除,而在remove(Object o)中,直接對數組進行遍歷,並判斷是否存在對應的元素,若是不存在直接返回false,若是存在,調用fastRemove(),並返回true

下面看一下fastRemove()

private void fastRemove(Object[] es, int i) {
    ++this.modCount;
    int newSize;
    if ((newSize = this.size - 1) > i) {
        System.arraycopy(es, i + 1, es, i, newSize - i);
    }

    es[this.size = newSize] = null;
}

首先修改次數加1,而後將數組長度減1,並判斷新長度是不是最後一個,若是是最後一個則不須要移動,若是不是,調用System.arraycopy將數組向前「挪」1位,最後將末尾多出來的一個值置爲null

3.3.2 removeAll()

public boolean removeAll(Collection<?> c) {
    return this.batchRemove(c, false, 0, this.size);
}

boolean batchRemove(Collection<?> c, boolean complement, int from, int end) {
    Objects.requireNonNull(c);
    Object[] es = this.elementData;

    for(int r = from; r != end; ++r) {
        if (c.contains(es[r]) != complement) {
            int w = r++;

            try {
                for(; r < end; ++r) {
                    Object e;
                    if (c.contains(e = es[r]) == complement) {
                        es[w++] = e;
                    }
                }
            } catch (Throwable var12) {
                System.arraycopy(es, r, es, w, end - r);
                w += end - r;
                throw var12;
            } finally {
                this.modCount += end - w;
                this.shiftTailOverGap(es, w, end);
            }

            return true;
        }
    }

    return false;
}

removeAll實際上調用的是batchRemove(),在batchRemove()中,有四個參數,含義以下:

  • Collection<?> c:目標集合
  • boolean complement:若是取值true,表示保留數組中包含在目標集合c中的元素,若是爲false,表示刪除數組中包含在目標集合c中的元素
  • from/end:區間範圍,左閉右開

因此傳遞的(c,false,0,this.size)表示刪除數組裏面在目標集合c中的元素。下面簡單說一下執行步驟:

  • 首先進行判空操做
  • 接着找到第一符合要求的元素(這裏是找到第一個須要刪除的元素)
  • 找到後從該元素開始繼續向後查找,同時記錄刪除後的數組中最後一個元素的下標w
  • try/catch是一種保護性行爲,由於contains()AbstractCollection的實現中,會使用Iterator,這裏catch異常後仍然調用System.arraycopy,使得已經處理的元素「挪到」前面
  • 最後會增長修改的次數,並調用shiftTailOverGap,該方法在後面會詳解

3.3.3 removeIf()

public boolean removeIf(Predicate<? super E> filter) {
    return this.removeIf(filter, 0, this.size);
}

boolean removeIf(Predicate<? super E> filter, int i, int end) {
    Objects.requireNonNull(filter);
    int expectedModCount = this.modCount;

    Object[] es;
    for(es = this.elementData; i < end && !filter.test(elementAt(es, i)); ++i) {
    }

    if (i < end) {
        int beg = i;
        long[] deathRow = nBits(end - i);
        deathRow[0] = 1L;
        ++i;

        for(; i < end; ++i) {
            if (filter.test(elementAt(es, i))) {
                setBit(deathRow, i - beg);
            }
        }

        if (this.modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        } else {
            ++this.modCount;
            int w = beg;

            for(i = beg; i < end; ++i) {
                if (isClear(deathRow, i - beg)) {
                    es[w++] = es[i];
                }
            }

            this.shiftTailOverGap(es, w, end);
            return true;
        }
    } else if (this.modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    } else {
        return false;
    }
}

removeIf中,刪除符合條件的元素,首先會進行判空操做,而後找到第一個符合條件的元素下標,若是找不到(i>=end),判斷是否有併發操做問題,沒有的話返回false,若是i<end,也就是正式進入刪除流程:

  • 記錄開始下標beg
  • deathRow是一個標記數組,長度爲(end-i-1)>>6 + 1,從beg開始若是遇到符合條件的元素就對下標進行標記(調用setBit
  • 標記後進行刪除,所謂的刪除其實就是把後面不符合條件的元素逐個移動到beg以後的位置上
  • 調用shiftTailOverGap處理末尾的元素
  • 返回true,表示存在符合條件的元素並進行了刪除操做

3.3.4 shiftTailOverGap()

上面的removeAll()以及removeIf()都涉及到了shiftTailOverGap(),下面來看一下實現:

private void shiftTailOverGap(Object[] es, int lo, int hi) {
    System.arraycopy(es, hi, es, lo, this.size - hi);
    int to = this.size;

    for(int i = this.size -= hi - lo; i < to; ++i) {
        es[i] = null;
    }

}

該方法將es數組中的元素向前移動hi-lo位,並將移動以後的在末尾多出來的那部分元素置爲null

3.4 indexOf()系列

包括:

  • indexOf()
  • lastIndexOf()
  • contains()

3.4.1 indexOf

public int indexOf(Object o) {
    return this.indexOfRange(o, 0, this.size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = this.elementData;
    int i;
    if (o == null) {
        for(i = start; i < end; ++i) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for(i = start; i < end; ++i) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }

    return -1;
}

indexOf()其實是一個包裝好的方法,會調用內部的indexOfRange()進行查找,邏輯很簡單,首先判斷須要查找的值是否爲空,若是不爲空,使用equals()判斷,不然使用==判斷,找到就返回下標,不然返回-1

3.4.2 contains()

contains()其實是indexOf()的包裝:

public boolean contains(Object o) {
    return this.indexOf(o) >= 0;
}

調用indexOf()方法,根據返回的下標判斷是否大於等於0,若是是則返回存在,不然返回不存在。

3.4.3 lastIndexOf()

lastIndexOf()實現與indexOf()相似,只不過是從尾部開始遍歷,內部調用的是lastIndexOfRange()

public int lastIndexOf(Object o) {
    return this.lastIndexOfRange(o, 0, this.size);
}

int lastIndexOfRange(Object o, int start, int end) {
    Object[] es = this.elementData;
    int i;
    if (o == null) {
        for(i = end - 1; i >= start; --i) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for(i = end - 1; i >= start; --i) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }

    return -1;
}

4 LinkedList底層

4.1 基本結構

首先來看一下里面的成員變量:

transient int size;
transient LinkedList.Node<E> first;
transient LinkedList.Node<E> last;
private static final long serialVersionUID = 876323262645176354L;

一個表示長度,一個頭指針和一個尾指針。

其中LinkedList.Node實現以下:

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

    Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

能夠看到LinkedList實際是基於雙鏈表實現的。

下面再來看一些重要方法,包括:

  • add()
  • remove()
  • get()

4.2 add()

add()方法包括6個:

  • add(E e)
  • add(int index,E e)
  • addFirst(E e)
  • addLast(E e)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)

4.2.1 linkFirst/linkLast/linkBefore實現的add()

先看一下比較簡單的四個add()

public void addFirst(E e) {
    this.linkFirst(e);
}

public void addLast(E e) {
    this.linkLast(e);
}

public boolean add(E e) {
    this.linkLast(e);
    return true;
}

public void add(int index, E element) {
    this.checkPositionIndex(index);
    if (index == this.size) {
        this.linkLast(element);
    } else {
        this.linkBefore(element, this.node(index));
    }
}

能夠看到,上面的四個add()不進行任何的添加元素操做,add()只是添加元素的封裝,真正實現add操做的是linkLast()linkFirst()linkBefore(),這些方法顧名思義就是把元素連接到鏈表的末尾或者頭部,或者鏈表某個節點的前面:

void linkLast(E e) {
    LinkedList.Node<E> l = this.last;
    LinkedList.Node<E> newNode = new LinkedList.Node(l, e, (LinkedList.Node)null);
    this.last = newNode;
    if (l == null) {
        this.first = newNode;
    } else {
        l.next = newNode;
    }

    ++this.size;
    ++this.modCount;
}

private void linkFirst(E e) {
    LinkedList.Node<E> f = this.first;
    LinkedList.Node<E> newNode = new LinkedList.Node((LinkedList.Node)null, e, f);
    this.first = newNode;
    if (f == null) {
        this.last = newNode;
    } else {
        f.prev = newNode;
    }

    ++this.size;
    ++this.modCount;
}

void linkBefore(E e, LinkedList.Node<E> succ) {
    LinkedList.Node<E> pred = succ.prev;
    LinkedList.Node<E> newNode = new LinkedList.Node(pred, e, succ);
    succ.prev = newNode;
    if (pred == null) {
        this.first = newNode;
    } else {
        pred.next = newNode;
    }

    ++this.size;
    ++this.modCount;
}

實現大致相同,一個是添加到尾部,一個是添加頭部,一個是插入到前面。另外,三者在方法的最後都有以下操做:

++this.size;
++this.modCount;

第一個表示節點的個數加1,而第二個,則表示對鏈表的修改次數加1。

好比,在unlinkLast方法的最後,有以下代碼:

--this.size;
++this.modCount;

unlinkLast操做就是移除最後一個節點,節點個數減1的同時,對鏈表的修改次數加1。

另外一方面,一般來講鏈表插入操做須要找到鏈表的位置,可是在三個link方法裏面,都看不到for循環找到插入位置的代碼,這是爲何呢?

因爲保存了頭尾指針,linkFirst()以及linkLast()並不須要遍歷找到插入的位置,可是對於linkBefore()來講,須要找到插入的位置,不過linkBefore()並無相似「插入位置/插入下標」之類的參數,而是隻有一個元素值以及一個後繼節點。換句話說,這個後繼節點就是經過循環獲得的插入位置,好比,調用的代碼以下:

this.linkBefore(element, this.node(index));

能夠看到在this.node()中,傳入了一個下標,並返回了一個後繼節點,也就是遍歷操做在該方法完成:

LinkedList.Node<E> node(int index) {
    LinkedList.Node x;
    int i;
    if (index < this.size >> 1) {
        x = this.first;

        for(i = 0; i < index; ++i) {
            x = x.next;
        }

        return x;
    } else {
        x = this.last;

        for(i = this.size - 1; i > index; --i) {
            x = x.prev;
        }

        return x;
    }
}

這裏首先經過判斷下標是位於「哪一邊」,若是靠近頭部,從頭指針開始日後遍歷,若是靠近尾部,從尾指針開始向後遍歷。

4.2.2 遍歷實現的addAll()

public boolean addAll(Collection<? extends E> c) {
    return this.addAll(this.size, c);
}

public boolean addAll(int index, Collection<? extends E> c) {
    this.checkPositionIndex(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0) {
        return false;
    } else {
        LinkedList.Node pred;
        LinkedList.Node succ;
        if (index == this.size) {
            succ = null;
            pred = this.last;
        } else {
            succ = this.node(index);
            pred = succ.prev;
        }

        Object[] var7 = a;
        int var8 = a.length;

        for(int var9 = 0; var9 < var8; ++var9) {
            Object o = var7[var9];
            LinkedList.Node<E> newNode = new LinkedList.Node(pred, o, (LinkedList.Node)null);
            if (pred == null) {
                this.first = newNode;
            } else {
                pred.next = newNode;
            }

            pred = newNode;
        }

        if (succ == null) {
            this.last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        this.size += numNew;
        ++this.modCount;
        return true;
    }
}

首先能夠看到兩個addAll實際上調用的是同一個方法,步驟簡述以下:

  • 首先經過checkPositionIndex判斷下標是否合法
  • 接着把目標集合轉爲Object[]數組
  • 進行一些特判處理,判斷index的範圍是插入中間,仍是在末尾插入
  • for循環遍歷目標數組,並插入到鏈表中
  • 修改節點長度,並返回

4.3 remove()

add()相似,remove包括:

  • remove()
  • remove(int index)
  • remove(Object o)
  • removeFirst()
  • removeLast()
  • removeFirstOccurrence(Object o)
  • removeLastOccurrence(Object o)

固然其實還有兩個removeAllremoveIf,但其實是父類的方法,這裏就不分析了。

4.3.1 unlinkFirst()/unlinkLast()實現的remove()

remove()removeFirst()removeLast()其實是經過調用unlinkFirst()/unlinkLast()進行刪除的,其中remove()只是removeFirst()的一個別名:

public E remove() {
    return this.removeFirst();
}

public E removeFirst() {
    LinkedList.Node<E> f = this.first;
    if (f == null) {
        throw new NoSuchElementException();
    } else {
        return this.unlinkFirst(f);
    }
}

public E removeLast() {
    LinkedList.Node<E> l = this.last;
    if (l == null) {
        throw new NoSuchElementException();
    } else {
        return this.unlinkLast(l);
    }
}

邏輯很簡單,判空以後,調用unlinkFirst()/unlinkLast()

private E unlinkFirst(LinkedList.Node<E> f) {
    E element = f.item;
    LinkedList.Node<E> next = f.next;
    f.item = null;
    f.next = null;
    this.first = next;
    if (next == null) {
        this.last = null;
    } else {
        next.prev = null;
    }

    --this.size;
    ++this.modCount;
    return element;
}

private E unlinkLast(LinkedList.Node<E> l) {
    E element = l.item;
    LinkedList.Node<E> prev = l.prev;
    l.item = null;
    l.prev = null;
    this.last = prev;
    if (prev == null) {
        this.first = null;
    } else {
        prev.next = null;
    }

    --this.size;
    ++this.modCount;
    return element;
}

而在這兩個unlink中,因爲已經保存了頭指針和尾指針的位置,所以二者能夠直接在O(1)內進行移除操做,最後將節點長度減1,修改次數加1,並返回舊元素。

4.3.2 unlink()實現的remove()

再來看一下remove(int index)remove(Object o)removeFirstOccurrence(Object o)removeLastOccurrence(Object o)

public E remove(int index) {
    this.checkElementIndex(index);
    return this.unlink(this.node(index));
}

public boolean remove(Object o) {
    LinkedList.Node x;
    if (o == null) {
        for(x = this.first; x != null; x = x.next) {
            if (x.item == null) {
                this.unlink(x);
                return true;
            }
        }
    } else {
        for(x = this.first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                this.unlink(x);
                return true;
            }
        }
    }

    return false;
}

public boolean removeFirstOccurrence(Object o) {
    return this.remove(o);
}

public boolean removeLastOccurrence(Object o) {
    LinkedList.Node x;
    if (o == null) {
        for(x = this.last; x != null; x = x.prev) {
            if (x.item == null) {
                this.unlink(x);
                return true;
            }
        }
    } else {
        for(x = this.last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                this.unlink(x);
                return true;
            }
        }
    }

    return false;
}

這幾個方法實際上都是調用unlink去移除元素,其中removeFirstOccurrence(Object o)等價於remove(Object o),先說一下remove(int index),該方法邏輯比較簡單,先檢查下標合法性,再經過下標找到節點並進行unlnk

而在remove(Object o)中,須要首先對元素的值是否爲null進行判斷,兩個循環實際上效果等價,會移除遇到的第一個與目標值相同的元素。在removeLastOccurrence(Object o)中,代碼大致一致,只是remove(Object o)從頭指針開始遍歷,而removeLastOccurrence(Object o)從尾指針開始遍歷。

能夠看到,這幾個remove方法其實是找到要刪除的節點,最後調用unlink()進行刪除,下面看一下unlink()

E unlink(LinkedList.Node<E> x) {
    E element = x.item;
    LinkedList.Node<E> next = x.next;
    LinkedList.Node<E> prev = x.prev;
    if (prev == null) {
        this.first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

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

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

實現邏輯與unlinkFirst()/unlinkLast()相似,在O(1)內進行刪除,裏面只是一些比較簡單的特判操做,最後將節點長度減1,並將修改次數加1,最後返回舊值。

4.4 get()

get方法比較簡單,對外提供了三個:

  • get(int index)
  • getFirst()
  • getLast()

其中getFirst()以及getLast()因爲保存了頭尾指針,特判後,直接O(1)返回:

public E getFirst() {
    LinkedList.Node<E> f = this.first;
    if (f == null) {
        throw new NoSuchElementException();
    } else {
        return f.item;
    }
}

public E getLast() {
    LinkedList.Node<E> l = this.last;
    if (l == null) {
        throw new NoSuchElementException();
    } else {
        return l.item;
    }
}

get(int index)毫無疑問須要O(n)時間:

public E get(int index) {
    this.checkElementIndex(index);
    return this.node(index).item;
}

get(int index)判斷下標後,實際上進行操做的是this.node(),因爲該方法是經過下標找到對應的節點,源碼前面也寫上了,這裏就不分析了,須要O(n)的時間。

5 總結

  • ArrayList基於Object[]實現,LinkedList基於雙鏈表實現
  • ArrayList隨機訪問效率要高於LinkedList
  • LinkedList提供了比ArrayList更多的插入方法,並且頭尾插入效率要高於ArrayList
  • 二者的刪除元素方法並不徹底相同,ArrayList提供了獨有的removeIf(),而LinkedList提供了獨有的removeFirstOccurrence()以及removeLastOccurrence()
  • ArrayListget()方法始終爲O(1),而LinkedList只有getFirst()/getLast()O(1)
  • ArrayList中的兩個核心方法是grow()以及System.arraycopy,前者是擴容方法,默認爲1.5倍擴容,後者是複製數組方法,是一個native方法,插入、刪除等等操做都須要使用
  • LinkedList中不少方法須要對頭尾進行特判,建立比ArrayList簡單,無須初始化,不涉及擴容問題

6 附錄:關於插入與刪除的一個實驗

關於插入與刪除,一般認爲LinkedList的效率要比ArrayList高,但實際上並非這樣,下面是一個測試插入與刪除時間的小實驗。

相關說明:

  • 測試次數:1000次
  • 數組長度:4000、40w、4000w
  • 測試數組:隨機生成
  • 插入與刪除下標:隨機生成
  • 結果值:插入與刪除1000次的平均時間

代碼:

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args){
        int len = 40_0000;
        Random random = new Random();
        List<Integer> list = Stream.generate(random::nextInt).limit(len).collect(Collectors.toList());
        LinkedList<Integer> linkedList = new LinkedList<>(list);
        ArrayList<Integer> arrayList = new ArrayList<>(list);

        long start;
        long end;

        double linkedListTotalInsertTime = 0.0;
        double arrayListTotalInsertTime = 0.0;

        int testTimes = 1000;
        for (int i = 0; i < testTimes; i++) {
            int index = random.nextInt(len);
            int element = random.nextInt();
            start = System.nanoTime();
            linkedList.add(index,element);
            end = System.nanoTime();
            linkedListTotalInsertTime += (end-start);

            start = System.nanoTime();
            arrayList.add(index,element);
            end = System.nanoTime();
            arrayListTotalInsertTime += (end-start);
        }
        System.out.println("LinkedList average insert time:"+linkedListTotalInsertTime/testTimes+" ns");
        System.out.println("ArrayList average insert time:"+arrayListTotalInsertTime/testTimes + " ns");

        linkedListTotalInsertTime = arrayListTotalInsertTime = 0.0;

        for (int i = 0; i < testTimes; i++) {
            int index = random.nextInt(len);
            start = System.nanoTime();
            linkedList.remove(index);
            end = System.nanoTime();
            linkedListTotalInsertTime += (end-start);

            start = System.nanoTime();
            arrayList.remove(index);
            end = System.nanoTime();
            arrayListTotalInsertTime += (end-start);
        }
        System.out.println("LinkedList average delete time:"+linkedListTotalInsertTime/testTimes+" ns");
        System.out.println("ArrayList average delete time:"+arrayListTotalInsertTime/testTimes + " ns");
    }
}

在數組長度爲4000的時候,輸出以下:

LinkedList average insert time:4829.938 ns
ArrayList average insert time:745.529 ns
LinkedList average delete time:3142.988 ns
ArrayList average delete time:1703.76 ns

而在數組長度40w的時候(參數-Xmx512m -Xms512m),輸出以下:

LinkedList average insert time:126620.38 ns
ArrayList average insert time:25955.014 ns
LinkedList average delete time:119281.413 ns
ArrayList average delete time:25435.593 ns

而將數組長度調到4000w(參數-Xmx16g -Xms16g),時間以下:

LinkedList average insert time:5.6048377238E7 ns
ArrayList average insert time:2.5303627956E7 ns
LinkedList average delete time:5.4753230158E7 ns
ArrayList average delete time:2.5912990133E7 ns

雖然這個實驗有必定的侷限性,但也是證實了ArrayList的插入以及刪除性能並不會比LinkedList差。實際上,經過源碼(看下面分析)能夠知道,ArrayList插入以及刪除的主要耗時在於System.arraycopy,而LinkedList主要耗時在於this.node(),實際上二者須要的都是O(n)時間。

至於爲何ArrayList的插入和刪除速度要比LinkedList快,筆者猜想,是System.arraycopy的速度快於LinkedList中的for循環遍歷速度,由於LinkedList中找到插入/刪除的位置是經過this.node(),而該方法是使用簡單的for循環實現的(固然底層是首先判斷是位於哪一邊,靠近頭部的話從頭部開始遍歷,靠近尾部的話從尾部開始遍歷)。相對於System.arraycopy的原生C++方法實現,可能會慢於C++,所以形成了速度上的差別。

相關文章
相關標籤/搜索