大白話+畫圖 從原始碼角度一步步搞懂ArrayList和LinkedList的使用

簡熵發表於2023-03-06

1.說說ArrayList

1.基本原理

ArrayList,原理就是底層基於陣列來實現。

01.基本原理:

陣列的長度是固定的,java裡面陣列都是定長陣列,比如陣列大小設定為100,此時你不停的往ArrayList裡面塞入這個資料,此時元素數量超過了100以後,此時就會發生一個陣列的擴容,就會搞一個更大的陣列,把以前的陣列複製到新的陣列裡面去。

這個陣列擴容+元素複製的過程,相對來說會慢一些

02.缺點:

01:不要頻繁的往arralist裡面去塞資料,導致他頻繁的陣列擴容,避免擴容的時候較差的效能影響了系統的執行。

02: 陣列來實現,陣列你要是往陣列的中間加一個元素,是不是要把陣列中那個新增元素後面的元素全部往後面挪動一位,所以說,如果你是往ArrayList中間插入一個元素,或者隨機刪除某個元素,效能比較差,會導致他後面的大量的元素挪動一個位置

03.優點:

基於陣列來實現,非常適合隨機讀,你可以隨機的去讀陣列中的某個元素

例如:list.get(10),相當於是在獲取第11個元素,這個隨機讀的效能是比較高的,隨機讀,list.get(2),list.get(20),隨機讀list裡任何一個元素。

因為基於陣列來實現,他在隨機獲取陣列裡的某個元素的時候,效能很高,他可以基於他底層對陣列的實現來快速的隨機讀取到某個元素,直接可以透過記憶體地址來定位某個元素。

04.常用場景:

ArrayList,常用,如果你不會頻繁的在裡面插入一些元素,不會導致頻繁的元素的位置移動、陣列擴容,就是有一批資料,查詢出來,灌入ArrayList中,後面不會頻繁插入元素了,主要就是遍歷這個集合,或者是透過索引隨機讀取某個元素。

如果果你涉及到了頻繁的插入元素到list中的話,儘量還是不要用ArrayList,陣列,定長陣列,長度是固定的,元素大量的移動,陣列的擴容+元素的複製。

05.場景示例:

開發系統的時候,大量的場景,需要一個集合,裡面可以按照順序灌入一些資料,ArrayList的話呢,他的最最主要的功能作用,就是說他裡面的元素是有順序的,我們在系統裡的一些資料,都是需要按照我插入的順序來排列的。

2.原始碼剖析

01.核心方法的剖析

我們們來啟動一個demo工程,在裡面寫寫集合的程式碼,跟進去看看各種集合的實現原理,直接可以看JDK底層的原始碼。

(1).示例程式碼:
public class ArrayListDemo {
    public static void main(String[] args) {
        ArrayList<String> sayLove =new ArrayList<String>();
       sayLove.add("老婆,早上好");
        sayLove.add("老婆,下午好");
        sayLove.add("老婆,下班啦,我去找你");
        sayLove.set(0,"老婆,早上好,我們一起吃早飯吧");
        sayLove.add(2,"老婆,注意坐姿,不要久坐哦");
    }
}
(2).建構函式分析:

預設的建構函式,直接初始化一個ArrayList例項的話,會將內部的陣列做成一個預設的空陣列,{},Object[],他有一個預設的初始化的陣列的大小的數值,是10,也就是我們可以認為他預設的陣列初始化的大小就是隻有10個元素。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//空陣列
​
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
​
 private static final int DEFAULT_CAPACITY = 10;//初始容量 10

注意點:

ArrayList的話,玩兒好的話,一般來說,你應該都不是使用這個預設的建構函式,你構造一個ArrayList的話,基本上來說就是預設他裡面不會有太頻繁的插入、移除元素的操作,大體上他裡面有多少元素,你應該可以推測一下的。

基本上最好是給ArrayList構造的時候,給一個比較靠譜的初始化的陣列的大小,比如說,100個資料,1000,10000,避免陣列太小,往裡面塞入資料的時候,導致資料不斷的擴容,不斷的搞新的陣列。

ensureCapacityInternal(size + 1); // Increments modCount!!

你每次往ArrayList中塞入資料的時候,人家都會判斷一下,當前陣列的元素是否塞滿了,如果塞滿的話,此時就會擴容這個陣列,然後將老陣列中的元素複製到新陣列中去,確保說陣列一定是可以承受足夠多的元素的。

(3).add(E)方法

日常多表白,恩愛不懈怠。

分析如下:

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

#2 ensureCapacityInternal(size + 1);:判斷一下當前的陣列容量是不是滿了,如果滿了會進行擴容;

 if (minCapacity - elementData.length > 0)
            grow(minCapacity);

1).第一次進入這個方法時,minCapacity=10,預設值,而此時底層還是個空陣列,自然會進行陣列的擴容。擴容程式碼如下:

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

透過計算,確認本次陣列增加的容量就是預設大小10,然後透過Arrays.copyOf進行陣列的資料複製和建立新陣列。

#3 elementData[size++] = e;,就是將新增資料資料放到index=0的位置上,並且size+1;

2).然後就返回新增成功了。

陣列的變化:

size=0,elementData={}變成了size=1,elementData=["老婆,早上好"]

依次執行三次,就完成今天的sayLove日常了。

(4).set(index,E)方法

一起起床是靜好,一起吃早飯是餵飽。

sayLove.set(0,"老婆,早上好,我們一起吃早飯吧");

分析如下:

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

#2 rangeCheck(index);:還記得我們那些年學Java的青蔥歲月嗎?都得喊一嗓子:角標越界,錯誤就在這裡了,熟悉的異常,值得的青蔥歲月。

if (index >= size)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

#3 E oldValue = elementData(index);:操作很簡單,返回原來位置上的值,將新值插入想插入的位置,並返回舊值。

就像老婆說,你去換下飲水機的水,換上新桶,拿下舊桶。

E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
(5).add(index,E)方法

牽掛是一根線,每天想的是可以把你栓在身上。

sayLove.add(2,"老婆,注意坐姿,不要久坐哦");

分析如下:

public void add(int index, E element) {
        rangeCheckForAdd(index);
​
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

#2,#4,還是熟悉的檢查角標是否越界以及檢查容量是否已滿。

#5,呼叫native方法System.arraycopy,進行陣列中元素的移位,引數如下所示:

index=2,size=3;

System.arraycopy(elementData, 新增的角標位=2, elementData, 往後移動的起始角標位=3,
                 要往後移動的個數=1);

elementeData這個陣列,從第2位開始(第3個元素),複製1個元素,到elementData這個陣列(還是原來的這個陣列),從第3位開始(第4個元素開始)。

#5,#6,插入新值到指定位置,size+1。

完成指定位置插入資料的操作。

(6).remove(index)方法

程式猿說我下班了,絕對是違背了山盟海誓裡說的,“我絕對不會騙你”。

sayLove.remove(2);//加班,所以,不能去找媳婦了。撤回 “老婆,下班啦,我去找你” 這句話。

分析如下:

public E remove(int index) {
    rangeCheck(index);
​
    modCount++;
    E oldValue = elementData(index);
​
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
    }

#7 int numMoved = size - index - 1;:numoved=4-2-1=1,要移動的個數,當前大於0,呼叫native方法,進行陣列元素移動,System.arraycopy(elementData, 3, elementData, 2,1);,從當前陣列的index=3位置,共一個元素,從index=2位置開始複製。

把elementData陣列中,從index =3開始的元素,一共有1個元素,複製到elementData陣列中(原來的陣列裡),從index = 2開始,進行複製。

說到底,就是刪除位置後的所有元素都往前移位,然後將最後位置上的元素設定為null。so easy。

2.談談LinkedList

1.基本原理

底層是基於連結串列來實現的。

01.基本原理:

LinkedList,連結串列,一個節點掛著另外一個節點。LinkedList是基於雙向連結串列實現的。

01_LinkedList資料結構

02.優點:

往這個裡面中間插入一些元素,或者不停的往list裡插入元素,都沒關係,因為人家是連結串列,中間插入元素,不需要跟ArrayList陣列那樣子,挪動大量的元素的,不需要,人家直接在連結串列里加一個節點就可以了。

如果你不斷的往LinkedList中插入一些元素,大量的插入,就不需要像ArrayList陣列那樣還要去擴容啊什麼的,人家是一個連結串列,就是不斷的把新的節點掛到連結串列上就可以了。

LinkedList的優點,就是非常適合各種元素頻繁的插入裡面去。

03.缺點:

不太適合在隨機的位置,獲取某個隨機的位置的元素,比如LinkedList.get(10),這種操作,效能就很低,因為他需要遍歷這個連結串列,從頭開始遍歷這個連結串列,直到找到index = 10的這個元素為止。

04.常用場景:

適合,頻繁的在list中插入和刪除某個元素,然後尤其是LinkedList他其實是可以當做佇列來用的,這個東西的話呢,我們後面看原始碼的時候,可以來看一下,先進先出,在list尾部懟進去一個元素,從頭部拿出來一個元素。如果要在記憶體裡實現一個基本的佇列的話,可以用LinkedList。

05.場景示例:

系統開發中,凡是用到了記憶體佇列,用LinkedList,他裡面基於連結串列實現,天然就可以做佇列的資料結構,先進先出,連結串列來實現,特別適合頻繁的在裡面插入元素什麼的,也不會導致陣列擴容。

2.原始碼剖析

01.插入元素

在尾部插入元素、在頭部插入元素、在中間插入元素

add(),預設就是在佇列的尾部插入一個元素,在那個雙向連結串列的尾部插入一個元素

add(index, element),是在佇列的中間插入一個元素

addFirst(),在佇列的頭部插入一個元素

addLast(),跟add()方法是一樣的,也是在尾部插入一個元素

(1).示例程式碼:
public static void main(String[] args) {
    LinkedList<String> housework=new LinkedList<String>();
    housework.add("洗菜");
    housework.add("切肉");
    housework.add("炒菜");
    housework.add(1,"給老婆倒杯水");
    }
(2).add(E)方法

家務活要一件件做,幸福要一天天過。

分析如下:

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

#2 linkLast(e);:將元素直接插入到隊尾程式碼如下:

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

1). #2~#4: 第一次last肯定是null,建立了一個Node節點,物件結構Node<preNode,元素,next節點>;並且該節點賦值給了last變數。

2). #5~#8: 第一次last節點是null,所以first節點也被賦值成建立的node節點。而如果是第二次進行add操作的話,就是會將新的Node節點掛在到前一個Node節點的next節點上去。

3).size+1,操作成功。

(3).add(index,E)方法

老婆的事情永遠可以插隊,優先順序No.1

housework.add(1,"給老婆倒杯水");

分析如下:

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

#2 checkPositionIndex(index);:對於指定位置的插入資料,我們都是需要進行角標是否越界的檢查的;

 private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
 }

#4-#7:如果正好是最後一個位置,就直接插入到隊尾;

如果不是,就要進行指定位置的插入,這裡走的是這個分支。

 void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

1).先根據index獲取到之前位置的node;

2).#3~#5: 1. 新建立一個Node,新Node的pre節點設定為原Node節點的pre節點,next節點設定為原node節點。

  1. 將原來的Node節點的pre節點設定為新Node節點。

2).#6~#9:如果原節點的pre節點是null,就把新Node節點設定為first節點;

否則就是將原Node節點的next節點設定為新建立Node節點。

3).size+1,然後就返回新增成功了。

繞來繞去,其實就像我準備栓老婆的繩子不夠長了,我就把繩子剪開,然後呢,拿一節新繩子,接上前半截繩子的頭,再接上後半截繩子的,打好倆個結,ok,繩子升級成功。愛情的繩子拉長啦。

02.獲取元素

(1).get(index)方法

經常翻看照片,一張張翻過的是,回憶的幸福,二哈的自己和氣質的媳婦。

示例程式碼:

public static void main(String[] args) {
        LinkedList<String> loveStory=new LinkedList<String>();
        loveStory.add("看電影");
        loveStory.add("去野餐");
        loveStory.add("摩天輪");
        loveStory.add("去海邊");
        loveStory.get(2);
    }

我們來分析這一行程式碼:

 loveStory.get(2);

分析如下:

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

#2 checkElementIndex;:角標是否越界的檢查。

private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

#3 node(index);:這裡就極為關鍵了,LinkedList底層是如何遍歷查詢指定位置的元素呢?來看如下分析:

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

#4~#6

1).size/2,取了中間數,然後判斷我們要找的位置是在中間數的前部分還是後部分。

如果是前半部分就從first節點開始往後查詢,

如果是後半部分,就從last節點開始往前查詢,有點二分法查詢的感覺。

2).我們這裡index=2, 2>2不成立,走else分支,然後,我們可以看到他透過一個for迴圈進行遍歷,從最後一個節點開始,每次都查詢Node節點的prev節點,直到index+1的位置時,就跳出迴圈了,而index+1節點的prev節點不正是我們要獲取的index節點麼?完美!

03.刪除元素

(1).get(index)方法

既然是二哈屬性,總有惹媳婦生氣的時候,不好的回憶還是適當刪除吧!

示例程式碼:

public static void main(String[] args) {
        LinkedList<String> loveStory=new LinkedList<String>();
        loveStory.add("看電影");
        loveStory.add("去野餐");
        loveStory.add("摩天輪");
        loveStory.add("惹老婆生氣");
        loveStory.add("去海邊");
        loveStory.get(2);
    }

我們來分析這一行程式碼:

loveStory.remove(2);

分析如下:

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

#2 checkElementIndex;:角標是否越界的檢查。

private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

#3 unlink(node(index));:先遍歷找到index對應的Node節點,然後進行刪除工作。

 E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<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;
        size--;
        modCount++;
        return element;
    }

#3~#5:獲取index對應的Node節點的prev節點和next節點;

#7~#12:如果prev節點為null,就把Node節點的next節點設定為first節點;

否則就將prev節點的next節點設定為Node節點的next節點。

#14~#19:如果next節點為null,就把Node節點的prev節點設定為last節點;

否則就將next節點的prev節點設定為Node節點的prev節點。

#21~#24:將node節點的值設定為null,size-1,最終返回原來Node的元素值。

還記得前文說的愛情繩子理論吧,這裡我們只要是換成縮短繩子,剪開,接上的過程就是刪除指定位置Node的過程。

如果生活有煩惱,就把煩惱給刪掉,把生活接上去繼續幸福。

3).對比總結

(1)ArrayList:一般場景,都是用ArrayList來代表一個集合,不適合頻繁的往裡面插入和灌入大量的元素遍歷,或者隨機查,效能都很好。

(2)LinkedList:適合,頻繁的在list中插入和刪除某個元素,然後尤其是LinkedList他其實是可以當做佇列來用的,先進先出,在list尾部懟進去一個元素,從頭部拿出來一個元素。在記憶體裡實現一個基本的佇列的話,可以用LinkedList。

生活中的美好,從一行程式碼的執行開始,從朝夕相伴的愛情開始。

最後謝謝大家閱讀,有不足之處歡迎指出。

相關文章