首先,你應該知道,java.util.Stack
是一個「遺留的collection」,可追溯到Java 1.0中的所有道路。它擴展了java.util.Vector
,這確實是基於陣列的。但是,這通常被認爲是不好的對象設計。這並不意味着基於數組的堆棧是一件壞事,但您應該意識到,僅僅因爲JDK中的某些東西並不意味着這是個好主意。對於較舊的API尤其如此。
一個更現代的類棧的數據結構是java.util.ArrayDeque
。它也是基於數組的。它還包含了許多其他功能,但如果你堅持自己的push
和pop
方法(相當於addFirst
和removeFirst
),它基本上是一個堆棧。請注意,在其文檔中提到,
當用作堆棧時,該類可能會比Stack
更快。
如果你看一下實現,Stack
,像Vector
,是同步的,這樣可以有點慢下來。Stack
的push
和pop
方法按照Vector
方法實施,其也同步。這意味着額外的方法調用加嵌套同步。 (儘管JIT可能會優化大部分。)相比之下,ArrayDeque
未同步,其類似堆棧的方法使用直接在其內部陣列上操作的簡單操作。請注意,我在這裏沒有做任何基準驗證文檔的索賠。
在任何情況下,ArrayDeque
是用於需要類似堆棧行爲的問題的首選Java集合。
但是你問的是鏈接的數據結構,而不是基於數組的數據結構。我們將ArrayDeque
與另一個Java鏈接的數據結構LinkedList
進行比較。這實現了Deque
,所以它也可以用作堆棧。你說,
1)不需要隨機訪問。
是的。請注意,ArrayDeque
不提供隨機訪問,即使它是基於數組的。沒有任何優勢。
2)陣列是低效的,因爲它們必須被調整大小(浪費時間),並且還它們使用存儲低效(一些空間會被浪費)
的任何數據結構會產生一定的低效率。不過,不同的數據結構會有不同的折衷。如果ArrayDeque
的數組大小不符合堆棧的典型容量,則必須調整其大小。但是一旦數組足夠大,就不需要不斷調整大小。如果堆棧收縮,則數組仍將佔用空的空間。這可以被看作是浪費,或者它可以被看作是保留內存,以便在堆棧再次增長時不必調整大小和複製。
將情況與LinkedList
進行比較。在內部,每個列表元素都需要一個Node
實例。 (請參閱源here。)每個實例包含三個引用:一個指向數據元素,一個指向下一個Node
,另一個指向之前的Node
。假設帶有壓縮指針的64位JVM,即每個引用32位。每個對象都有一個包含64位標記字和32位類指針的標題。這總共給出了六個32位字,或每個列表元素24字節。這六個單詞中只有一個是「有效載荷」 - 元素本身 - 因此每個元素有20個字節或83%的開銷!
相比之下,數組中的每個附加元素僅消耗引用該元素的空間或32位。
例如,在LinkedList
中存儲1,000個元素會消耗大約24K的內存,但將它們存儲在ArrayDeque
中僅消耗大約4K的內存。即使陣列的大小是容納1,000個元素的兩倍,那也只有8K - 仍然只是LinkedList
大小的一小部分。
「另一方面一個基於陣列的實現可能有更好的運行時行爲」
這可能指的是遍歷鏈表比遍歷數組慢。有兩個原因。首先,鏈接節點佔用更多的內存,因此必須讀取更多的內存才能獲得相同的信息。每節點24字節,2.67個節點可以放入典型的64字節緩存行。其次,鏈接節點可能會隨機分佈在內存周圍,因此平均而言,每個緩存行的節點數量可能會少於此數量。結果是遍歷一個鏈表將導致很多緩存未命中,因爲這個引用的局部性很差。另一方面,由於數組中的引用密集堆積而沒有開銷,所以16個數組元素可以放入單個64字節緩存行中。遍歷數組會導致更少的緩存未命中。此外,內存子系統針對順序訪問進行了優化,因此它們可能能夠預取下一個高速緩存行,進一步降低內存開銷。
考慮到內存消耗和內存訪問的性能成本,基於陣列的結構通常是優選的。可能有些情況下,鏈接結構表現更好,但似乎比大多數人認爲的要少。
除了設置性能外,鏈接結構對於堆棧的數組結構還有一個明顯的優點:不可變的持久性。在基於陣列的堆棧上推送和彈出元素會固有地改變數組,因此以前的版本不再存在。在鏈接結構上推送和彈出節點不需要改變任何鏈接節點,因此即使其他人在此堆棧上推送和彈出元素,對堆棧過去狀態的引用也可以保持不變,並保持不變。實際上它們是不同的堆棧,共同的部分是共享的。這在函數式編程環境中非常有用。
真棒的答案!非常感謝。如果我有更多的空閒時間去思考你所討論的所有問題,那麼病人必須稍後再讀一遍。如果我有額外的疑慮,我會不好意見。 – Shreyans