今天在網上閑逛時看到了這樣一個言論,說“
Java的Stack類實現List接口的設計是個笑話”。
當然作者這篇文章的重點不是這個,原本我也只是一笑置之,然而看評論里居然還有人附和,說“Java那種Stack的設計作為笑話,差不多可以算公案了”,我就有點不淡定了,為什么、什么時候“作為笑話”的並且“差不多可以算公案”了呢?
因此我決定寫一篇文章來談談這個問題。
Java中Stack類的聲明
首先我們來看看Java中Stack類的聲明:
public
class Stack<E>
extends Vector<E>
Vector:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
也就是說,Stack定義了標准的push/pop等操作,同時也實現了List和RandomAccess接口。List接口在Java中表示的是一個順序列表,RandomAccess則在順序訪問的基礎上又加上了“隨機訪問”的約定。一般來說,隨機訪問的時間復雜度為O(1)。例如數組就是一個“順序且隨機訪問”的列表,而鏈表則是一個“順序非隨機訪問”的列表。
接口是什么
狹義地講,接口就是一個類所定義的方法(方法名、參數、返回值)。一個類提供了Foo方法,其他類就可以調用它。廣義上講,接口可以理解為一個子系統和其他子系統之間為了交互所做的約定。這里的子系統可能是類,也可以是模塊,或其他任何能夠可以跟外界產生交互的實體。
注意:在Java中,我們可以使用Interface來定義一個接口,Interface不做任何事情,僅僅用來為實現提供“規范”,因此Interface比抽象類更純粹,抽象層次往往也要更高一些。但是請注意這里的Interface和泛指的“接口”不同,Interface是Java為了更好地支持“針對接口編程”而提供的一種語言機制,即使不用Interface我們也可以很好地做到“針對接口編程”。
如果一個類完全遵守一個接口,就可以說這個類“實現”了這個接口。實現一個接口在語言層面是任意的,因為語言只能對語法進行檢查,而無法對語意進行檢查。例如你可以讓People類實現Plant接口,於是這個人就可以隨時被當作一顆植物來使用(植物人?)。對動態語言來說,這種情況更加明顯:只要一個對象表現出了某種行為特征,那么這個對象就可以被當作另一個對象來使用。人們給動態語言的這種特性起了一個形象化的名字:鴨子類型(Duck Typing)。也就是說,只要一個對象能夠“呱呱叫”,那么就完全可以把它當成一只鴨子。
但是從設計者的角度來看,一個類實現哪些接口,除了要符合語法之外,更應該符合語意,這樣才能讓使用這個類的人不至於產生迷惑。
因此,“Stack類應不應該實現List接口”是一個
語意問題而不是
語法問題。要回答這個問題,首先要了解Stack是什么,以及在哪些場景下需要用到它。
Stack是什么
Stack,棧,是一種所有程序員都很熟悉的數據機構。棧通常是“后進先出”的(LIFO)。作為一個慣例,棧通常具有名為push和pop的2個操作,push操作將一個元素存入棧中(壓入),pop操作則相反,將一個元素從棧頂取出(彈出)。
棧的應用
多數情況用到棧時,僅僅將其當作一個“棧”來使用即可,即只需使用push/pop操作,例如可以用棧來檢查括號匹配問題,或計算后綴表達式的值。
另一個例子是計算機中的過程調用(procedure call)。在調用一個過程(也稱為子過程、函數)之前,需要先將當前幀指針、返回地址壓入棧中,然后依次壓入各參數,最后執行call指令跳轉到目標過程的起始地址繼續執行。其棧結構如下圖所示。

從過程中返回和調用時正好相反:先從棧上將各參數和臨時變量彈出(一般直接丟棄),然后執行ret指令跳轉到返回地址所在的位置繼續執行。
隨機訪問棧
到目前為止,程序對棧的操作還只是標准的push/pop操作。但在一個過程的內部,除了要用到push/pop操作之外,可能還需要對棧做隨機訪問(類似於數組的訪問方式)。以上圖為例,過程Q中的代碼可能要訪問參數y1,但又不能丟掉y2,因此不能使用pop操作。
事實上,在所有基於棧來實現過程調用的計算機中,編譯器都是使用隨機訪問的方式操作棧上的參數和臨時變量的,只有在call和ret時才需要對棧進行push/pop操作。此時的棧對過程來說更像是一個數組。例如過程Q可能通過下面的方式來訪問y1:
*(SP + 1)
這里SP是棧指針,它指向棧頂,並且假設y2占用1個機器字長(例如在32位系統中4個字節為一個機器字長)。另外之所以是加1而不是減1,是因為棧是由高地址向低地址增長的,因此y2的地址比y1的小。
從這個例子可以看出,根據使用場景的不同,棧有時也需要像數組一樣的隨機訪問方式,在這種情況下,實際上是把棧看成了一個數組。
Java的Stack類實現List到底是不是一個笑話
從上面的例子可以看出,Stack實現List和RandomAccess接口是完全合理的。如果你需要把它當成一個純粹的棧來使用,你只需將其看成一個普通的“Stack”實例即可。但如果你需要對其進行隨機訪問,你可以隨時把它當成一個“RandomAccess的List”來使用。這兩種情況都有可能會遇到。
所以我看了上面的文章和評論后對“Stack實現List是一個笑話”感到非常疑惑,更不明白在什么時候這已經成為了所謂的“公案”了。在我看來,這非常合理,也不會對我正確使用Stack產生任何不良影響。
周星馳在《唐伯虎點秋香》里對奪命書生說:誰說沒有槍頭就捅不死呢?這里借用一下:誰說Stack實現List是個笑話,只是你了解的還不夠而已。