《撕爛java集合源碼——List篇》

手撕java集合源碼——List篇

你知道的越多,你不知道的越多
但願你看完這篇文章,當面試官的問你有沒有手撕過源碼,你能夠自信的告訴他,很差意思,我把他撕爛了。
本文 收錄GitHub JavaStudy 歡迎Star和完善,裏面放了學習的一些資料,但願咱們一塊兒學習衝進大廠。java

閱讀list集合觀察它們底層是如何實現的,以及集合面試中提出的問題進行實踐。node

list集合中經常使用的類爲Arraylist、LinkedLIst。git

二者的區別github

區別 Arraylist LinkedList
底層實現 數組 雙向鏈表
適用場景 增刪操做較少,查找較多 增刪效率較高,查找效率較低
容量大小 數組大小不能超過Integer最大值 理論無限增長,實際size範圍爲Integer最大值
線程安全 線程不安全 線程不安全
NUll值處理 容許添加NUll值 容許添加NUll值

下面分別對着源碼來進行逐條比較面試

底層實現

Arraylist
....
    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;
...
    //構造方法1
public ArrayList(int var1) {
    if (var1 > 0) {
        this.elementData = new Object[var1];
    } else {
        if (var1 != 0) {
            throw new IllegalArgumentException("Illegal Capacity: " + var1);
        }

        this.elementData = EMPTY_ELEMENTDATA;
    }

}
	//構造方法2
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
	//構造方法3
public ArrayList(Collection<? extends E> var1) {
        this.elementData = var1.toArray();
        if ((this.size = this.elementData.length) != 0) {
            if (this.elementData.getClass() != Object[].class) {
                this.elementData = Arrays.copyOf(this.elementData, this.size, Object[].class);
            }
        } else {
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
複製代碼

ArrayList的三個構造方法分別初始化了ElementData變量不一樣的值。若傳入參數<=0或者使用無參構造函數會賦值數組element對象一個長度爲0的Object數組。算法

LinkedList
//linkedList集合元素
	transient int size = 0;
    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;
        }
    }
	//構造方法1
 	public LinkedList() {
    }
	//構造方法2
	public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
複製代碼

LinkedList內部實現很簡單隻有一個頭節點,一個尾節點,以及一個size變量來記錄集合中的元素。Ctrl點入Node。發現這是一個泛型類,內部保存兩個指針,指向先後兩個節點,E 類型的變量。若是鏈表數據爲空的話,頭尾節點是同一個節點,自己是 null,指向先後節點的值也是 null。數組

總結

Arraylist和Linkedlist實現底層都跟數據結構有關係,能夠轉換爲鏈表以及數組的優缺點進行二者的回答,鏈表的擴容更加方便,而數組的查找更加便捷。具體如何能夠給面試官講你看到的細節。安全

適用場景

Arraylist

Arraylist底層是使用數組來進行實現,對於數組的查找只要經過下標即可以得到,查找時間複雜度爲O(1);數組的刪除操做爲刪除該下標節點,並將後續節點前移動,具體實現爲使用System.arraycopy()方法時間複雜度爲O(n)。添加方法分爲尾部添加以及指定位置添加,尾部添加時間複雜度爲O(n),指定位置添加與刪除相似。bash

//查找操做
 	public E get(int var1) {
 		//查找範圍判斷 下標是否越界
        this.rangeCheck(var1);
        return this.elementData(var1);
    }
    //添加操做
    public boolean add(E var1) {
    	//檢查數組是否須要擴容
        this.ensureCapacityInternal(this.size + 1);
        this.elementData[this.size++] = var1;
        return true;
    }
    //指定位置添加元素
    public void add(int var1, E var2) {
    	//指定位置 越界判斷
        this.rangeCheckForAdd(var1);
        this.ensureCapacityInternal(this.size + 1);
        //底層native方法 
        System.arraycopy(this.elementData, var1, this.elementData, var1 + 1, this.size - var1);
        this.elementData[var1] = var2;
        ++this.size;
    }
    //刪除方法
    public E remove(int var1) {
        this.rangeCheck(var1);
        //modCount 記錄列表結構修改次數
        ++this.modCount;
        Object var2 = this.elementData(var1);
        int var3 = this.size - var1 - 1;
        if (var3 > 0) {
            System.arraycopy(this.elementData, var1 + 1, this.elementData, var1, var3);
        }

        this.elementData[--this.size] = null;
        return var2;
    }
複製代碼
Linkedlist

Linkedlist底層是使用雙向鏈表來進行實現,因此對於元素的添加更加靈活,對於添加操做,有表首添加表尾添加,以及制定位置添加。對於鏈表的遍歷時間複雜度都爲O(n),表首添加和表尾添加時間複雜度爲O(1),查找操做時間複雜度爲O(n)可是底層有利用二分思想進行細微的優化。刪除操做爲O(n);數據結構

//表首添加 與表尾添加 方法相似 以addfirst爲例
public void addFirst(E e) {
        linkFirst(e);
    }

private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)//表爲空,設置表尾節點也是這個元素
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

//查找操做 node方法中使用一次二分判斷進行優化
public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
Node<E> node(int 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;
        }
    }
//移除操做分爲表首移除和表尾移除,以及指定元素刪除 以指定元素刪除爲例
public boolean remove(Object o) {
    //添加時未對空進行判斷因此移動時進行判斷
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
複製代碼

容量大小

Arraylist 能夠從size獲取到列表中當元素的個數,因此經過Arraylist類中size的大小就能夠判斷數組的大小,其次經過Arraylist的擴容函數也能夠發現這一點,以後會介紹。 同時建立一個數組數組的容量大小也是跟虛擬機heap大小相互關聯。

public int size() {
    return this.size; //返回的元素爲int型 因此數組大小被限制爲最大2147483647
}
複製代碼
//main代碼
public static void main(String[] args) {
    ArrayList<Integer> a=new ArrayList<>(100000);
}
//結果
Error occurred during initialization of VM
GC triggered before VM initialization completed. Try increasing NewSize, current value 1536K.
複製代碼

Linkedlist 底層是雙向鏈表,理論上能夠無限大。但源碼中,LinkedList 實際大小用的是 int 類型,這也說明了 LinkedList 不能超過 Integer 的最大值,否則會溢出。

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    transient int size = 0;
    ......
    ......
複製代碼

線程安全

Arraylist和Linkedlist都不是線程安全的。當鏈表對象做爲一個共享變量,多個線程在任什麼時候刻下均可以對鏈表進行操做致使數值覆蓋等問題。只有當鏈表對象做爲局部變量的時候是沒有線程安全問題的。

若想建立一個線程安全的鏈表可使用Collections.synchronizedList()方法,底層實現是將鏈表轉換爲SynchronizedList類該類的全部方法都帶有Synchronized方法。

Collections.synchronizedList(a);
 //底層實現
 public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
複製代碼

NUll值處理

Arraylist和Linkedlist類中對於添加NUll值時並無特殊判斷因此在刪除時要對null進行判斷。

//Arraylist類 刪除方法
public boolean remove(Object var1) {
        int var2;
        if (var1 == null) {
            for(var2 = 0; var2 < this.size; ++var2) {
                if (this.elementData[var2] == null) {
                    this.fastRemove(var2);
                    return true;
                }
            }
        } else {
            for(var2 = 0; var2 < this.size; ++var2) {
                if (var1.equals(this.elementData[var2])) {
                    this.fastRemove(var2);
                    return true;
                }
            }
        }

        return false;
    }
//linkedlist類的null值remove方法在適用場景處已經舉出,不在重複
複製代碼

難點 :Arraylist擴容操做

瞭解Arraylist擴容的最好辦法就是一路debug下去了解擴容機制總體流程。

public static void main(String[] args) {
        ArrayList<Integer> a=new ArrayList<>();
        a.add(1);
        a.add(2);
    }
複製代碼

Debug分析可得首先進行ensureCapacityInternal()方法進行size判斷,若數組爲第一次添加元素則初始化數組大小爲10,若數組size+1後小於數組容量,就直接添加不然調用grow()擴容函數。

st=>start: add()
e=>end: 結束
op=>operation: ensureCapacityInternal() 容量判斷
cond=>condition: Elements數組是否爲空
io=>operation: minCapacity設置爲10
three=>operation: ensureExplicitCapacity(minCapacity)
panduan=>condition: minCapacity>Elements.length
grow=>operation: grow()

st(right)->op(right)->cond
cond(yes)->io(right)->e
cond(no)->panduan
io->three->panduan
panduan(yes,right)->grow->e
panduan(no)->e
複製代碼
//擴容函數
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
1           newCapacity = minCapacity; 
        if (newCapacity - MAX_ARRAY_SIZE > 0)
2           newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
複製代碼

擴容內部使用1.5倍擴容來實現 其中使用右移運算符來進行0.5倍長度計算,使用右移而不是除法,由於計算機底層右移操做速度更快。若是擴容後的數組大小仍小於要添加元素大小,會將大小設置爲要添加元素大小

舉例:先前咱們已經計算出來數組在加入一個值後,實際大小是 1,最大可用大小是 10 ,現 在須要一會兒加入 15 個值,那咱們指望數組的大小值就是 16,此時數組最大可用大小隻有 10,明顯不夠,須要擴容,擴容後的大小是:10 + 10 /2 = 15,這時候發現擴容後的大小仍 然不到咱們指望的值 16,這時候源碼使用上述策略以下:因此最終數組擴容後的大小爲 16。

若是遇到大數組的狀況:最好一次性添加元素容量使用addAll(Collection<? extends E> c)方法,而不要使用for循環add,由於循環add,會形成屢次擴容,性能下降。

源碼擴容過程有什麼值得借鑑的地方?

  • 是擴容的思想值得學習,經過自動擴容的方式,讓使用者不用關心底層數據結構的變化,封 裝得很好,1.5 倍的擴容速度,可讓擴容速度在前期緩慢上升,在後期增速較快,大部分 工做中要求數組的值並非很大,因此前期增加緩慢有利於節省資源,在後期增速較快時, 也可快速擴容。
  • 擴容過程當中,有數組大小溢出的意識,好比要求擴容後的數組大小,不能小於 0,不能大於 Integer 的最大值。

難點:Arraylist remove方法

先看代碼 猜想結果是多少?

public static void main(String[] args) {
        ArrayList<Integer> a=new ArrayList<>();
        a.add(1);
        a.add(1);
        a.add(1);
        a.add(1);
        for (int i = 0; i < a.size(); i++) {
            if(a.get(i)==1){
                a.remove(i);
            }
        }
        System.out.println(a.size());
    }
複製代碼

輸出結果爲2,而不是0,緣由是底層在remove後調用System.copyOf()方法進行復制,將刪除Element[0]後,放在Element[1]的值放在Element[0]處,使得原先Element[1]元素未被刪除。

若是使用foreach()方法結果如何?

for (int i:a) {
            if(i==1){
                a.remove(i);
            }
        }
複製代碼

輸出結果爲Exception in thread "main" java.util.ConcurrentModificationException

再次Debug進去走一遍流程,哪裏出了問題。

foreach的內部實現是使用Iterator迭代器來實現。由於加強 for 循環過程其實調用的就是迭代器的 next () 方法,當你調用list#remove () 方 法 進 行 刪 除 時 , modCount 的 值 會 +1 , 而 這 時 候 迭 代 器 中 的expectedModCount 的 值 卻 沒 有 變 , 導 致 在 迭 代 器 下 次 執 行 next () 方 法 時 ,expectedModCount != modCount 就會報 ConcurrentModificationException 的錯誤。

那若是使用Iterator.remove () 方法能夠刪除麼,爲何?

能夠的,由於 Iterator.remove () 方法在執行的過程當中,會把最新的 modCount 賦值給expectedModCount,這樣在下次循環過程當中,modCount 和 expectedModCount 二者就會相等。

//Iterator內部remove方法
public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
複製代碼

好了以上就是在閱讀Linkedlist和Arraylist中遇到的問題以及使用Debug觀察後的感覺。

文章每週持續更新,你的關注就是個人動力,本文 GitHub JavaStudy 歡迎Star和完善,裏面放了學習的一些資料,但願咱們一塊兒學習衝進大廠。爭取一個禮拜兩篇博客,每日一道算法題!

相關文章
相關標籤/搜索