棧的介紹
棧(Stack)是一種後入先出(LIFO)的數據結構。經常用來形象說明棧的結構的例子是書箱:把書平放到書箱裏,先取出來的書是最後放進去的。
應該使用Stack類嗎?
相信大多數人(也包括我),在調用Java中封裝好的數據結構以在代碼中使用棧結構時,往往使用java.util.Stack類。
// import java.util.*;
Stack<Integer> stack = new Stack<>();
stack.push(1);
while (!stack.empty()){
System.out.println(stack.pop());
}
這難道不是很正常嗎?就像使用集合時調用Set,使用列表時調用List一樣。既然是Java中所定義好的以Stack命名的結構,那就應該是Java設計者們所設計的最合適、最好用的結構了。
然而並不是這樣。
甚至你去查看JDK的官方文檔,都能發現這麼一段話:
“Deque接口及其實現提供了更完整和一致的LIFO堆棧操作集,這些接口應優先於此類”
令人驚訝,Java官方竟然推薦使用隊列(Deque)來實現棧!這一切的原因都在文檔中的另一句話:“從以下版本開始:JDK1.0”
爲什麼不應該使用Stack類?
Stack類繼承自Vector類。Vector是一個動態數組,因而Vector可以在數組中操作任意一個位置的元素。
而因爲Stack繼承自Vector,所以Vector的所有公有方法Stack都可以使用——這就意味着,Stack也可以在任意位置添加或刪除元素!
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.add(1,3);
System.out.println(stack);
[1, 3, 2]
上面的代碼是可以運行的。一個設計爲後入先出的結構,竟然可以在其他位置插入元素?你無法想象其他人在使用棧時會不會有意或無意地做出這些操作——一旦做出,代碼的安全性蕩然無存。
Java的設計者在設計Stack時犯了如此荒唐的錯誤。如果Stack和Vector的關係不是繼承而是組合(Stack裏應該擁有一個動態數組,而不是繼承自一個數組!),那麼今天我們就不會在這裏討論這個糟糕的問題。
現在,爲了保證Java代碼的兼容性,這段糟糕的設計仍然保留在JDK中。但是我們應該瞭解Stack的缺陷,並在自己的代碼中避免它。
用什麼結構來替代Stack類?
1. Deque
用一個ArrayDeque可以實現和Stack相同的功能,這也是Java官方所推薦的做法。
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
while (!stack.isEmpty())
System.out.println(stack.pop());
3
2
1
2. LinkedList
沒錯,用LinkedList也可以實現同樣的功能。只不過,LinkedList的接口不那麼直接,或許需要你編寫一個自己的Stack類來包裝一下。
import java.util.LinkedList;
public class Stack<T> {
LinkedList<T> list;
public Stack(){
list = new LinkedList<>();
}
public void push(T t){
list.addFirst(t);
}
public <T> T pop(){
return (T) list.removeFirst();
}
public <T> T peek(){
return (T) list.getFirst();
}
public boolean empty(){
return list.isEmpty();
}
}
用泛型可以很容易地寫出通用的數據結構。而且,通過LinkedList,可以自定義不同的棧,比如雙向棧。你可以自己實現一下。
結語
不要使用java.util.Stack!這是本篇文章的核心論點。使用Stack可能會導致你被面試官掛掉,從今天起培養用Deque代替Stack的習慣。