java集合【13】——— Stack原始碼分析走一波

秦懷雜貨店發表於2022-01-10

前言

集合原始碼分析系列:Java集合原始碼分析

前面已經把Vector,ArrayList,LinkedList分析完了,本來是想開始Map這一塊,但是看了下面這個介面設計框架圖:整個介面框架關係如下(來自百度百科):

原來還有一個漏網之魚,Stack棧的是掛在Vector下,前面我們已經分析過Vector了,那麼順便把Stack分析一遍。再不寫就2022年了:

Stack介紹

棧是一種資料結構,並不是Java特有的,在Java裡面體現是Stack類。它的本質是先進後出,就像是一個桶,只能不斷的放在上面,取出來的時候,也只能不斷的取出最上面的資料。要想取出底層的資料,只有等到上面的資料都取出來,才能做到。當然,如果有這種需求,我們一般會使用雙向佇列。

以下是棧的特性演示:

StackJava中是繼承於Vector,這裡說的是1.8版本,共用了Vector底層的資料結構,底層都是使用陣列實現的,具有以下的特點:

  • 先進後出(``FILO`)
  • 繼承於Vector,同樣基於陣列實現
  • 由於使用的幾乎都是VectorVector的操作都是執行緒安全的,那麼Stack操作也是執行緒安全的。

類定義原始碼:

public
class Stack<E> extends Vector<E> {
}

成員變數只有一個序列化的變數:

    private static final long serialVersionUID = 1224463164541339165L;

方法解讀

Stack沒有太多自己的方法,幾乎都是繼承於Vector,我們可以看看它自己擴充的 5 個方法:

  • E push(E item): 壓棧
  • synchronized E pop(): 出棧
  • synchronized E peek():獲取棧頂元素
  • boolean empty():判斷棧是不是為空
  • int search(Object o): 搜尋某個物件在棧中的索引位置

push 方法

在底層實際上呼叫的是addElement()方法,這是Vector的方法:

    public E push(E item) {
        addElement(item);

        return item;
    }

在前面我們已經分析過Vecor的原始碼,感興趣可以參考:http://aphysia.cn/archives/java-ji-he-11vector-chao-ji-xiang-xi-yuan-ma-jie-xi

addElement是執行緒安全的,在底層實際上就是往陣列後面新增了一個元素,但是它幫我們保障了容量,如果容量不足,會觸發自動擴容機制。

    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

pop 方法

底層是先呼叫peek()方法,獲取到棧頂元素,再呼叫removeElementAt()方法移除掉棧頂元素,實現出棧效果。

    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

removeElementAt(int index)也是Vector的方法,synchronized修飾,也是執行緒安全的,由於移除的是陣列最後的元素,所以在這裡不會觸發元素複製,也就是System.arraycopy(elementData, index + 1, elementData, index, j);:

    public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }

peek 方法

獲取棧頂元素,先獲取陣列的大小,然後再呼叫VectorE elementAt(int index)獲取該索引的元素:

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

E elementAt(int index)的原始碼如下,裡面邏輯比較簡單,只有陣列越界的判斷:

    public synchronized E elementAt(int index) {
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
        }

        return elementData(index);
    }

empty 方法

主要是用來判空,判斷元素棧裡面有沒有元素,主要呼叫的是size()方法:

    public boolean empty() {
        return size() == 0;
    }

這個size()方法其實也是Vector的方法,返回的其實也是一個類變數,元素的個數:

    public synchronized int size() {
        return elementCount;
    }

search方法

這個方法主要用來查詢某個元素的索引,如果裡面存在多個,那麼將會返回最後一個元素的索引:


   public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

使用synchronized修飾,也是執行緒安全的,為什麼需要這個方法呢?

我們知道棧是先進先出的,如果需要查詢一個元素在其中的位置,那麼需要一個個取出來再判斷,那就太麻煩了,而底層使用陣列進行儲存,可以直接利用這個特性,就可以快速查詢到該元素的索引位置。

至此,回頭一看,你是否會感到疑惑,``Stack裡面沒有任何的資料,但是卻不斷的在運算元據,這得益於Vector`的資料結構:

		// 底層陣列
    protected Object[] elementData;

    // 元素數量
    protected int elementCount;

底層使用陣列,儲存了元素的個數,那麼操作元素其實只是不斷往陣列中插入元素,或者取出最後一個元素即可。

總結

stack 由於繼承了Vector,因此也是執行緒安全的,底層是使用陣列儲存資料,大多數API都是使用Vector來儲存。它最重要的屬性是先進先出。至於陣列擴容,沿用了Vector中的擴容邏輯。

如果讓我們自己實現,底層不一定使用陣列,使用連結串列也是能實現相同的功能的,只是在整個集合原始碼體系中,共有相同的部分,是不錯的選擇。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,個人網站:http://aphysia.cn,技術之路不在一時,山高水長,縱使緩慢,馳而不息。

劍指Offer全部題解PDF

開源程式設計筆記

相關文章