JDK 竟然是這樣實現棧的?

2021-03-02 腳本之家

前面的文章《動圖演示:手擼堆棧的兩種實現方法!》我們用數組和鍊表來實現了自定義的棧結構,那在 JDK 中官方是如何實現棧的呢?接下來我們一起來看。

這正式開始之前,先給大家再解釋一下「堆棧」一詞的含義,因為之前有讀者對這個詞有一定的疑惑。

Stack 翻譯為中文是堆棧的意思,但為了能和 Heap(堆)區分開,因此我們一般將 Stack 簡稱為棧。因此當「堆棧」連在一起時有可能表示的是 Stack,而當「堆、棧」中間有分號時,則表示 Heap(堆)和 Stack(棧),如下圖所示:


JDK 棧的實現

聊會正題,接下來我們來看 JDK 中是如何實現棧的?

在 JDK 中,棧的實現類是 Stack,它的繼承關係如下圖所示:


Stack 包含的方法如下圖所示:


其中最重要的方法有:

Stack 實現源碼如下:

public class Stack<E> extends Vector<E> {
    /**
     * 創建一個空棧
     */
    public Stack() {
    }

    /**
     * 入棧方法,調用的是 Vector#addElement 的添加方法
     */
    public E push(E item) {
        addElement(item);
        return item;
    }

    /**
     * 出棧並返回當前元素,調用的是 Vector#removeElementAt 的移除元素方法
     */
    public synchronized E pop() {
        E       obj; // 返回當前要移除的棧頂元素信息
        int     len = size();
        obj = peek(); // 查詢當前棧頂元素
        removeElementAt(len - 1); // 移除棧頂元素
        return obj;
    }

    /**
     * 查詢棧頂元素,調用 Vector#elementAt 的查詢方法
     */
    public synchronized E peek() {
        int     len = size(); // 查詢當前棧的長度
        if (len == 0) // 如果為空棧,直接拋出異常
            throw new EmptyStackException();
        return elementAt(len - 1); // 查詢棧頂元素的信息
    }

    /**
     * 判斷棧是否為空
     */
    public boolean empty() {
        return size() == 0;
    }
    // 忽略其他方法...
}

從上述源碼可以看出, Stack 中的核心方法中都調用了父類 Vector 類中的方法,Vector 類的核心源碼:

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
 protected Object[] elementData; // 存儲數據的容器
    protected int elementCount; // 存儲數據的容量值
    
    /**
     * 添加數據
     */
    public synchronized void addElement(E obj) {
        modCount++; // 統計容器被更改的參數
        ensureCapacityHelper(elementCount + 1); // 確認容器大小,如果容量超出則進行擴容
        elementData[elementCount++] = obj; // 將數據存儲到數組
    }
    
    /**
     * 移除元素(根據下標移除)
     */
    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--; // 數組容量 -1
        elementData[elementCount] = null; // 將末尾的元素賦值為 null(刪除尾部元素)
    }
    
    /**
     * 查詢元素(根據下標)
     */
 public synchronized E elementAt(int index) {
     // 安全性驗證
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
        }
        // 根據下標返回數組中的元素
        return elementData(index);
    }
    // 忽略其他方法...
}

對於上述源碼中,可以最不好理解的就是 System#arraycopy 這個方法,它的作用其實就是將刪除的元素(非末尾元素)的後續元素依次往前移動的,比如以下代碼:

Object[] elementData = {"Java", "Hello", "world", "JDK", "JRE"};
int index = 3;
int j = elementData.length - index - 1;
System.arraycopy(elementData, index + 1, elementData, index, j);
//  System.arraycopy(elementData, 4, elementData, 3, 1);
System.out.println(Arrays.toString(elementData));

它的運行結果是:

[Java, Hello, world, JRE, JRE]

也就是說當我們要刪除下標為 3 的元素時,需要把 3 以後的元素往前移動,所以數組的值就從 {"Java", "Hello", "world", "JDK", "JRE"} 變為了 [Java, Hello, world, JRE, JRE],最後我們只需要把尾部元素刪除掉,就可以實現數組中刪除非末尾元素的功能了。

小結

通過以上源碼可以得知,JDK 中的棧(Stack)也是通過物理結構數組實現的,我們通過操作物理數組來實現邏輯結構棧的功能,關於物理結構和邏輯結構詳見《動圖演示:手擼堆棧的兩種實現方法!》。

棧的應用

經過前面的學習我們對棧已經有了一定的了解了,那棧在我們的平常工作中有哪些應用呢?接下裡我們一起來看。

瀏覽器回退

棧的特性為 LIFO(Last In First Out,LIFO)後進先出,因此藉助此特性就可以實現瀏覽器的回退功能,如下圖所示:

函數調用棧

棧在程序中最經典的一個應用就是函數調用棧了(或叫方法調用棧),比如作業系統給每個線程分配了一塊獨立的內存空間,這塊內存被組織成「棧」這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會將臨時變量作為一個棧幀入棧,當被調用函數執行完成,返回之後,將這個函數對應的棧幀出棧。為了讓你更好地理解,我們一塊來看下這段代碼的執行過程。

int main() {
   int a = 1; 
   int ret = 0;
   int res = 0;
   ret = add(3, 5);
   res = a + ret;
   System.out.println(res);
   reuturn 0;
}
int add(int x, int y) {
   int sum = 0;
   sum = x + y;
   return sum;
}

從代碼中我們可以看出, main() 函數調用了 add() 函數,獲取計算結果,並且與臨時變量 a 相加,最後列印 res 的值。為了讓你清晰地看到這個過程對應的函數棧裡出棧、入棧的操作,我畫了一張圖。圖中顯示的是,在執行到 add() 函數時,函數調用棧的情況。

棧的複雜度

複雜度分為兩個維度:

時間維度:是指執行當前算法所消耗的時間,我們通常用「時間複雜度」來描述;空間維度:是指執行當前算法需要佔用多少內存空間,我們通常用「空間複雜度」來描述。

這兩種複雜度都是用大 O 表示法來表示的,比如以下代碼:

int[] arr = {1, 2, 3, 4};
for (int i = 0; i < arr.length; i++) {
    System.out.println(i);
}

用大 O 表示法來表示的話,它的時間複雜度就是 O(n),而如下代碼的時間複雜度卻為 O(1):

int[] arr = {1, 2, 3, 4};
System.out.println(arr[0]); // 通過下標獲取元素

因此如果使用大 O 表示法來表示棧的複雜度的話,結果如下所示:

引用 & 鳴謝

https://time.geekbang.org/column/article/41222

相關焦點

  • 深入分析Java虛擬機堆和棧及OutOfMemory異常產生原因
    jdk1.7和1.8的實現方法區的差異方法區是Java虛擬機規範中的規範,但是具體如何實現並沒有規定,所以虛擬機廠商完全可以採用不同的方式實現方法區的。jdk1.8版本移除了永久代,採用元空間(Metaspace)來實現方法區,所以在jdk1.8中關於永久代的參數-XX:PermSize和-XX:MaxPermSize已經被廢棄卻代之的是參數-XX:MetaspaceSize和-XX:MaxMetaspaceSize。
  • 順序棧與鏈式棧的圖解與實現
    # 順序棧與鏈式棧的圖解與實現棧是一種特殊的線性表,它與線性表的區別體現在增刪操作上棧的特點是先進後出,後進先出,也就是說棧的數據操作只能發生在末端,而不允許在中間節點進行操作如上圖所示,對棧的增刪操作都只能在末端也就是棧頂操作
  • 棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧棧
    對於棧這樣一個數據結構來說,它有兩個常見的動作:push,中文釋義有很多種,我個人更喜歡叫它「壓入」,非常形象。當我們要把一個元素放入棧的頂部,這個動作就叫做 push。pop,同樣的,我個人更喜歡叫它「彈出」,帶有很強烈的動畫效果,有沒有?當我們要從棧中移除一個元素時,這個動作就叫做 pop。
  • 棧:如何實現有效括號的判斷?
    向一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。3.如何實現棧    從剛才棧的定義裡,我們可以看出,棧主要包含兩個操作,入棧和出棧,也就是在棧頂插入一個數據和從棧頂刪除一個數據。理解了棧的定義之後,我們來看一看如何用代碼實現一個棧。
  • 用一個棧實現另一個棧的排序
    【題目】一個棧中元素的類型為整型,現在想將該棧從頂到底按從大到小的順序排序,只許申請一個棧。除此之外,可以申請新的變量,但不能申請額外的數據結構。如何完成排序?【解答】將要排序的棧記為 stack,申請的輔助棧記為help。在 stack上執行pop操作,彈出的元素記為cur。如果cur小於或等於he|p的棧頂元素,則將cur直接壓入help。
  • JDK、JRE、JVM,是什麼關係?
    JDK 目錄結構和作用我們默認安裝完 JDK 會有 jdk1.8.0_45、jre1.8.0_45,兩個文件夾。其實在 JDK 的文件中還會有 JRE 的文件夾,他們兩個 JRE 文件夾的結構是一樣的。JDK 目錄結構bin:一堆 EXE 可執行文件,java.exe、javac.exe、javadoc.exe,已經密鑰管理工具等。
  • Axure教程:如何用Axure實現進棧和遍歷效果
    那些技術們用ppt可以做到的效果,我們用Axure也可以嘗試實現。接下來,我們開始練習吧深度遍歷、棧簡介用Axure實現之前,我們先來學習下深度遍歷和棧的知識: 深度遍歷:深度優先遍歷 沿著樹的深度遍歷結點,儘可能深的搜索樹的分支。
  • shell-安裝jdk腳本
    前言在Linux安裝jdk是很簡單的事情,那就讓shell腳本去做吧!安裝到oracle官網的歸檔網址下載需要的jdk壓縮包,並放到腳本所在的目錄,然後cd到腳本目錄執行就可以,這裡是1.8.172版本為例:http://www.oracle.com/technetwork/java/archive-139210.html#!
  • Java實現單鍊表、棧、隊列三種數據結構
    它裡面的數據元素是以結點為單位,每個結點是由數據元素的數據和下一個結點的地址組成,在java集合框架裡面  LinkedList、HashMap(數組加鍊表)等等的底層都是用鍊表實現的。二、棧(Stack)1、一提到棧我們腦海就會浮現四個字「先進後出」,沒錯,它就是棧的最大特點。
  • win10安裝jdk1.8以及配置環境變量和多個jdk之間的切換
    ~本次的安裝是在新電腦上安裝的jdk1.7 /1.8/11三個版本.均為學習使用.多個版本可能會帶來未知問題.折騰無止境,不用懼怕.先上jdk1.8的安裝過程.jdk1.8下載.2. jdk安裝:安裝過程和普通軟體沒有區別,一路next.(需要注意的一點:不要修改路徑,就讓安裝在系統盤C盤.)3.
  • OpenJdk1.8筆記——java啟動流程
    Jdk中java的入口函數文件為openjdk\jdk\src\share\bin\main.c中的main方法(window上為WinMain),然後調用jdk8u-dev/jdk/src/share/bin/java.c的JLI_Launch方法,啟動一個jvm虛擬機;程序入口
  • python:棧的理解與應用
    如何實現一個「棧」?從剛才棧的定義裡,我們可以看出,棧主要包含兩個操作,入棧和出棧,也就是在棧頂插入一個數據和從棧頂刪除一個數據。理解了棧的定義之後,我們來看一看如何用代碼實現一個棧。實際上,棧既可以用數組來實現,也可以用鍊表來實現。
  • jdk環境變量配置教程
    下面,小編就來跟大家講解jdk環境變量配置的操作技巧。jdk環境變量配置教程1、右擊「我的電腦」,點擊屬性2、點擊:高級系統設置。3、在彈出的系統屬性中,選擇高級,在點擊環境變量。4、新建系統變量JAVA_HOME,變量值為jdk安裝目錄5、找到Path變量,然後點擊編輯。
  • Windows下jdk下載安裝與環境變量配置
    下載安裝jdk百度搜索jdk+版本,以1.8版本為例,百度搜索「jdk1.8」,一般是第一個。百度搜索jdk1.8打開jdk下載頁面,這裡我們下載jdk1.8 x64版本下載地址:https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
  • JS 數據結構與算法——棧 & 隊列
    2.2 棧的實現普通的棧常用的有以下幾個方法:push 添加一個(或幾個)新元素到棧頂pop 溢出棧頂元素,同時返回被移除的元素peek 返回棧頂元素,不對棧做修改isEmpty 棧內無元素返回 true,否則返回 falsesize 返回棧內元素個數clear 清空棧classStack{ constructor(){this.
  • 最新的java(jdk+jre)完整安裝教程——附詳細步驟
    承接上文,本文將介紹java的安裝與配置,也就是jdk+jre的詳細安裝過程,以供大家參考、學習。Java/jdk/jre安裝教程安裝包的獲取注意兩點:>jdk+jre的安裝包命名一般都是這樣的:jdk-8u211-windows-x64.exe如果名稱中有bin字眼,表示只有jdk,沒有jre
  • 線程與棧那些事
    這篇文章是介紹一下線程與棧相關的話題,文章比較長,主要會聊聊下面這些話題:Hotspot 線程棧的 Guard 區域實現原理你可能沒有怎麼聽說過的 Yellow-Zone、Red-ZoneJava StackOverflowError 的實現原理為了講清楚線程與棧的關係,我們要從進程和線程之間的關係講起
  • Java面試必問之Hashmap底層實現原理(JDK1.7)
    Hashmap底層實現原理(get\put\resize)Hashmap怎麼解決hash衝突?Hashmap是線程安全的嗎?綜上所述,可以得出:Hashmap底層是基於數組和鍊表實現的3.bucket位置static int indexFor(int h, int length) {// 根據hash與數組長度mod運算 return h & (length-1);}由源碼可知, jdk
  • 如何在Windows10系統中配置java的JDK環境
    操作步驟如下:1.下載好 jdk 的安裝文件,我下載的是 jdk-10.0.1_windows-x64_bin.exe 這個版本的安裝文件;2.使用滑鼠雙擊該exe文件,該exe文件會運行安裝界面,截圖如下:3.安裝程序自動執行,界面如下:
  • JDK 源碼閱讀 : FileDescriptor
    在/jdk/src/share/native/java/io/FileInputStream.c的開頭,可以看到這樣的代碼:jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream *//*******************************************************