java stack的詳細實現分析


簡介

    我們最常用的數據結構之一大概就是stack了。在實際的程序執行,方法調用的過程中都離不開stack。那麼,在一個成熟的類庫裏面,它的實現是怎麼樣的呢?也許平時我們實踐的時候也會嘗試着去寫一個stack的實現玩玩。這裏,我們就仔細的分析一下jdk裏的詳細實現。

Stack

    如果我們去查jdk的文檔,我們會發現stack是在Java.util這個包裏。它對應的一個大致的類關係圖如下:

    通過繼承Vector類,Stack類可以很容易的實現他本身的功能。因爲大部分的功能在Vector裏面已經提供支持了。

Stack裏面主要實現的有一下幾個方法:

方法名 返回類型 說明
empty boolean 判斷stack是否爲空。
peek E 返回棧頂端的元素。
pop E 彈出棧頂的元素
push E 將元素壓入棧
search int 返回最靠近頂端的目標元素到頂端的距離。

    因爲前面我們已經提到過,通過繼承Vector,很大一部分功能的實現就由Vector涵蓋了。Vector的詳細實現我們會在後面分析。它實現了很多的輔助方法,給Stack的實現帶來很大的便利。現在,我們按照自己的思路來分析每個方法的具體步驟,再和具體實現代碼對比。

empty

    從我們的思路來說,如果要判斷stack是否爲空,就需要有一個變量來計算當前棧的長度,如果該變量爲0,則表示該棧爲空。或者說我們有一個指向棧頂的變量,如果它開始的時候是設置爲空的,我們可以認爲棧爲空。這部分的實現代碼也很簡單:

Java代碼  收藏代碼
  1. public boolean empty() {  
  2.     return size() == 0;  
  3. }  

 如果更進一步分析的話,是因爲Vector已經實現了size()方法。在Vector裏面有一個變量elementCount來表示容器裏元素的個數。如果爲0,則表示容器空。這部分在Vector裏面的實現如下:

Java代碼  收藏代碼
  1. public synchronized int size() {  
  2.     return elementCount;  
  3. }  

 

peek

    peek是指的返回棧頂端的元素,我們對棧本身不做任何的改動。如果棧裏有元素的話,我們就返回最頂端的那個。而該元素的索引爲棧的長度。如果棧爲空的話,則要拋出異常:

Java代碼  收藏代碼
  1. public synchronized E peek() {  
  2.     int     len = size();  
  3.   
  4.     if (len == 0)  
  5.         throw new EmptyStackException();  
  6.     return elementAt(len - 1);  
  7. }  

 這個elementAt方法也是Vector裏面的一個實現。在Vector裏面,實際上是用一個elementData的Object數組來存儲元素的。所以要找到頂端的元素無非就是訪問棧最上面的那個索引。它的詳細實現如下:

Java代碼  收藏代碼
  1. public synchronized E elementAt(int index) {  
  2.     if (index >= elementCount) {  
  3.         throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);  
  4.     }  
  5.   
  6.     return elementData(index);  
  7. }  
  8.   
  9. @SuppressWarnings("unchecked")  
  10. E elementData(int index) {  
  11.     return (E) elementData[index];  
  12. }  

pop

    pop方法就是將棧頂的元素彈出來,如果棧裏有元素,就取最頂端的那個,否則就要拋出異常:

Java代碼  收藏代碼
  1. public synchronized E pop() {  
  2.     E       obj;  
  3.     int     len = size();  
  4.   
  5.     obj = peek();  
  6.     removeElementAt(len - 1);  
  7.   
  8.     return obj;  
  9. }  

    在這裏,判斷是否可以取棧頂元素在peek方法裏實現了,也將如果棧爲空則拋異常的部分包含在peek方法裏面。這裏有必要注意的一個細節就是,在通過peek()取到頂端的元素之後,我們需要用removeElementAt()方法將最頂端的元素移除。我們平時可能不太會留意到這一點。爲什麼要移除呢?我們反正有一個elementCount來記錄棧的長度,不管它不是也可以嗎?

    實際上,這麼做在程序運行的時候會有一個潛在的內存泄露的問題。因爲在java裏面,如果我們普通定義的類型屬於強引用類型。比如這裏vector就底層用的Object[]這個數組強類型來保存數據。強類型在jvm中做gc的時候,只要程序中有引用到它,它是不會被回收的。這就意味着在這裏,只要我們一直在用着stack,那麼stack裏面所有關聯的元素就都別想釋放了。這樣運行時間一長就會導致內存泄露的問題。那麼,爲了解決這個問題,這裏就是用的removeElementAt()方法。

 

Java代碼  收藏代碼
  1. public synchronized void removeElementAt(int index) {  
  2.     modCount++;  
  3.     if (index >= elementCount) {  
  4.         throw new ArrayIndexOutOfBoundsException(index + " >= " +  
  5.                                                  elementCount);  
  6.         }  
  7.     else if (index < 0) {  
  8.         throw new ArrayIndexOutOfBoundsException(index);  
  9.     }  
  10.     int j = elementCount - index - 1;  
  11.     if (j > 0) {  
  12.         System.arraycopy(elementData, index + 1, elementData, index, j);  
  13.     }  
  14.     elementCount--;  
  15.     elementData[elementCount] = null/* to let gc do its work */  
  16. }  

     這個方法實現的思路也比較簡單。就是用待刪除元素的後面元素依次覆蓋前面一個元素。這樣,就相當於將數組的實際元素長度給縮短了。因爲這裏這個移除元素的方法是定義在vector中間,它所面對的是一個更加普遍的情況,我們移除的元素不一定就是數組尾部的,所以才需要從後面依次覆蓋。如果只是單純對於一個棧的實現來說,我們完全可以直接將要刪除的元素置爲null就可以了。

push

    push的操作也比較直觀。我們只要將要入棧的元素放到數組的末尾,再將數組長度加1就可以了。

Java代碼  收藏代碼
  1. public E push(E item) {  
  2.     addElement(item);  
  3.   
  4.     return item;  
  5. }  

    這裏,addElement方法將後面的細節都封裝了起來。如果我們更加深入的去考慮這個問題的話,我們會發現幾個需要考慮的點。

1. 首先,數組不會是無窮大的 ,所以不可能無限制的讓你添加元素下去。當我們數組長度到達一個最大值的時候,我們不能再添加了,就需要拋出異常來。

2. 如果當前的數組已經滿了,實際上需要擴展數組的長度。常見的手法就是新建一個當前數組長度兩倍的數組,再將當前數組的元素給拷貝過去。

前面討論的這兩點,都讓vector把這份心給操了。我們就本着八卦到底的精神看看它到底是怎麼幹的吧:

Java代碼  收藏代碼
  1. public synchronized void addElement(E obj) {  
  2.     modCount++;  
  3.     ensureCapacityHelper(elementCount + 1);  
  4.     elementData[elementCount++] = obj;  
  5. }  
  6.   
  7. private void ensureCapacityHelper(int minCapacity) {  
  8.     // overflow-conscious code  
  9.     if (minCapacity - elementData.length > 0)  
  10.         grow(minCapacity);  
  11. }  
  12.   
  13. private void grow(int minCapacity) {  
  14.     // overflow-conscious code  
  15.     int oldCapacity = elementData.length;  
  16.     int newCapacity = oldCapacity + ((capacityIncrement > 0) ?  
  17.                                     capacityIncrement : oldCapacity);  
  18.     if (newCapacity - minCapacity < 0)  
  19.         newCapacity = minCapacity;  
  20.     if (newCapacity - MAX_ARRAY_SIZE > 0)  
  21.         newCapacity = hugeCapacity(minCapacity);  
  22.     elementData = Arrays.copyOf(elementData, newCapacity);  
  23. }  
  24.   
  25. private static int hugeCapacity(int minCapacity) {  
  26.     if (minCapacity < 0// overflow  
  27.         throw new OutOfMemoryError();  
  28.     return (minCapacity > MAX_ARRAY_SIZE) ?  
  29.         Integer.MAX_VALUE :  
  30.         MAX_ARRAY_SIZE;  
  31. }  

     看到這部分代碼的時候,我不由得暗暗嘆了口氣。真的是拔了蘿蔔帶出泥。本來想看看stack的細節實現,結果這些細節把vector都深深的出賣了。在vector中間有幾個計數的變量,elementCount表示裏面元素的個數,elementData是保存元素的數組。所以一般情況下數組不一定是滿的,會存在着elementCount <= elementData.length這樣的情況。這也就是爲什麼ensureCapacityHelper方法裏要判斷一下當新增加一個元素導致元素的數量超過數組長度了,我們要做一番調整。這個大的調整就在grow方法裏展現了。

    grow方法和我們所描述的方法有點不一樣。他不一樣的一點在於我們可以用一個capacityIncrement來指示調整數組長度的時候到底增加多少。默認的情況下相當於數組長度翻倍,如果設置了這個變量就增加這個變量指定的這麼多。

search

    search這部分就相當於找到一個最靠近棧頂端的匹配元素,然後返回這個元素到棧頂的距離。

Java代碼  收藏代碼
  1. public synchronized int search(Object o) {  
  2.     int i = lastIndexOf(o);  
  3.   
  4.     if (i >= 0) {  
  5.         return size() - i;  
  6.     }  
  7.     return -1;  
  8. }  

    對應在vector裏面的實現也相對容易理解:

Java代碼  收藏代碼
  1. public synchronized int lastIndexOf(Object o) {  
  2.     return lastIndexOf(o, elementCount-1);  
  3. }  
  4.   
  5. public synchronized int lastIndexOf(Object o, int index) {  
  6.     if (index >= elementCount)  
  7.         throw new IndexOutOfBoundsException(index + " >= "+ elementCount);  
  8.   
  9.     if (o == null) {  
  10.         for (int i = index; i >= 0; i--)  
  11.             if (elementData[i]==null)  
  12.                 return i;  
  13.     } else {  
  14.         for (int i = index; i >= 0; i--)  
  15.             if (o.equals(elementData[i]))  
  16.                 return i;  
  17.     }  
  18.     return -1;  
  19. }  

    這個lastIndexOf的實現無非是從數組的末端往前遍歷,如果找到這個對象就返回。如果到頭了,還找不到對象呢?...不好意思,誰讓你找不到對象的?活該你光棍,那就返回個-1吧。

Vector

    在前面對stack的討論和分析中,我們幾乎也把vector這部分主要的功能以及實現給涵蓋了。vector和相關類以及接口的關係類圖如下: 

    因爲Java沒有內置對List類型的支持,所以Vector內部的實現是採用一個object的array。其定義如下:

Java代碼  收藏代碼
  1. protected Object[] elementData;  

    這裏從某種角度來說可以說是java裏對泛型支持的不足,因爲內部保存數據的是Object[],在存取數據的時候如果不注意的話會出現存取數據類型不一致的錯誤。所以在以下的某些個方法裏需要加上@SuppressWarnings("unchecked")的聲明。

Java代碼  收藏代碼
  1. @SuppressWarnings("unchecked")  
  2.     E elementData(int index) {  
  3.         return (E) elementData[index];  
  4.     }  

    我們前面討論的那些數組的增長,刪除元素,查找元素以及修改等功能就佔據了vector的大部分。如果有興趣看vector的源代碼的話,會發現裏面主要就是這些功能的實現再加上一個迭代器功能。總共的代碼不是很多,1200多行,這裏就不再贅述了。 

    可以說,vector它本身就是一個可以動態增長的數組。和我們常用的ArrayList很像。和ArrayList的不同在於它對元素的訪問都用synchronized修飾,也就是說它是線程安全的。在多線程的環境下,我們可以使用它。

總結

    看前面這些代碼,不但理順了棧和vector的具體實現,還可以從中發現一些其他的東西。比如說,棧最大的長度取決於vector裏面數組能有多長。這裏vector裏面最大能取到Integer.MAX_VALUE。 以前寫c程序的代碼時經常感嘆,要是有那種可以自動增長的數組類型就好了。當然,c99後面確實提供了這個福利。在java裏面,比較典型這一部分就由vector提供了。你看,他可以自動按照需要增長,本身是線程安全的,順便幫你把清除元素時的內存泄露問題都考慮到了。簡直是自動、安全、健康又環保啊:)

參考資料:

http://docs.oracle.com/javase/7/docs/api/java/util/Stack.html

http://docs.oracle.com/javase/7/docs/api/java/util/Vector.html

原文地址:http://shmilyaw-hotmail-com.iteye.com/blog/1825171

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章