Java常用資料結構之Stack&Vector

奇舞移動發表於2018-12-24

前言

繼續Java常用資料結構分析之路,這次的主角是StackVector。Vector已經不推薦使用了,可以用ArrayList和LinkedList替代,它的主要特色是執行緒安全,代價自然就是效率。Stack則是擁有先進後出的特性,在特定的環境下能很好的工作。這兩個類相較於List和Map的使用頻率要少,但還是需要理解其內部原理的。

類繼承關係

先來看Stack:

public class Stack<E> extends Vector<E>
複製程式碼

原來Stack繼承了Vector,那再看Vector:

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
複製程式碼

又是熟悉的感覺:

  1. 繼承AbstractList抽象類,算是List模板的擴充套件;
  2. 實現List介面,Vector屬於List的一種;
  3. 實現RandomAccess介面,一個空介面,用來標記可以隨機訪問元素;
  4. 實現Cloneable介面,可以被克隆;
  5. 實現Serializable介面,可以被序列化;

總的來說,Stack和Vector其實都是List的一種實現,可以進行隨機訪問,子類中實現自己的特徵邏輯。

Vector原始碼分析

重要屬性

// 用來儲存元素,該陣列的大小就是Vector的容量大小,說明支援null
protected Object[] elementData;

// 當前已儲存元素的數量
protected int elementCount;

// 當容量不夠時,Vector擴充的大小
protected int capacityIncrement;

// Vector的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製程式碼

Vector使用陣列來儲存元素,有意思的是開發人員可以自己控制每次擴容的大小。

建構函式

public Vector() {
        this(10); // 預設容量10
    }
    
public Vector(int initialCapacity) {
        this(initialCapacity, 0); // 預設擴容增量設定為0表示雙倍擴充套件
    }
    
// 可以設定擴容時增量大小
public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }
複製程式碼

使用無參建構函式建立Vector時,預設大小是10,且每次擴容時容量變成原來的兩倍。

重用方法

先來看擴容方法:

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length; // 因為已經滿了,所以是舊容量
        // 如果擴充容量值小於等於0,則直接擴充為原來的兩倍
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        // minCapacity一般是oldCapacity+1,即執行add操作擴容
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
複製程式碼

當執行add操作時,就有可能進行擴容,來看看add方法:

public synchronized boolean add(E e) {
        modCount++; // 記錄修改次數
        add(e, elementData, elementCount);
        return true;
    }
    
private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow(); // 擴容
        elementData[s] = e; // 增加新元素
        elementCount = s + 1; // 元素數量加1
    }
    
private Object[] grow() {
        return grow(elementCount + 1); // 擴充的最小容量是原資料量加1
    }
    
private Object[] grow(int minCapacity) {
        // 呼叫newCapacity獲取新容量,同時進行陣列複製
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
複製程式碼

注意到add(E e)方法增加了synchronized關鍵字,說明是執行緒安全的。其實,Vector大部分公開方法都有synchronized關鍵字,所以說Vector是執行緒安全的。
Vector中除了add(E e)還可以使用addElement(E obj)insertElementAt(E obj, int index)來新增元素,內部實現大同小異。
有擴容,理論上也要有縮容,然而Vector沒有自動縮容邏輯,但提供了一個方法:

public synchronized void trimToSize() {
        modCount++;
        int oldCapacity = elementData.length;
        if (elementCount < oldCapacity) {
            elementData = Arrays.copyOf(elementData, elementCount);
        }
    }
複製程式碼

trimToSize方法可以將Vector的容量調整到元素數量大小。
說到Vector的容量,其實Vector是支援自定義設定大小的,使用setSize(int newSize)即可。

public synchronized void setSize(int newSize) {
        modCount++;
        if (newSize > elementData.length)
            grow(newSize);
        final Object[] es = elementData;
        for (int to = elementCount, i = newSize; i < to; i++)
            es[i] = null; // 不夠則補null,多了則剪去
        elementCount = newSize;
    }
複製程式碼

如果設定的大小大於當前儲存的元素數量,則補null值;如果小於現有元素數量,則會剪去多餘元素。

遍歷方法

對應Vector,可以使用三種方法進行遍歷:

  1. 使用iterator()或者listIterator()方法;
  2. 使用elements()方法;
  3. 使用forEach(Consumer<? super E> action)方法;

第一種方法中,使用iterator時,不可以對Vector進行add和remove操作;第二種方法中,使用elements時,可以使用add操作,但不可以使用remove操作;第三種方法,可以使用lambda表示式。

Vector的主要原始碼分析就這麼多,還有一些導航方法,如indexOflastElement等實現邏輯都很簡單。

Stack原始碼分析

Stack類繼承了Vector,也是使用陣列進行元素儲存,其原始碼很少,就提供了幾個公有方法,下面直接分析這些方法。

  • push方法
public E push(E item) {
        // 直接呼叫Vector的addElement方法,將元素新增到陣列尾部
        addElement(item);
        return item;
    }
複製程式碼
  • pop和peek方法
// 返回棧頂元素,並且在陣列中刪除該元素
public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek(); // 獲取頂部元素
        removeElementAt(len - 1); // 去除
        return obj;
    }
    
public synchronized E peek() {
        int     len = size();
        if (len == 0) // 空異常
            throw new EmptyStackException();
        return elementAt(len - 1); // 隨機訪問陣列中最後一個元素
    }
複製程式碼
  • search方法
// 返回離棧頂最近的指定元素到棧頂的距離
// 從1開始
public synchronized int search(Object o) {
        int i = lastIndexOf(o); // 指定元素在陣列中最後出現的位置
        if (i >= 0) { // 獲取差量
            return size() - i;
        }
        return -1;
    }
複製程式碼

舉個例子:
基礎Stack:7 2 11 -6 5 8 66,執行下面的程式碼:

        // 7 2 11 -6 5 8 66
        // 基本位置為1
        System.out.println("search操作,11距離頂部的距離:" + stack.search(11));
        System.out.println("search 7:" + stack.search(7));
        System.out.println("search 66:" + stack.search(66));
        stack.push(0);
        stack.push(0);
        stack.push(0);
        stack.push(9);
        stack.push(33); // 7 2 11 -6 5 8 66 0 0 0 9 33 
        stack.forEach(integer -> {
            System.out.print(integer + " ");
        });
        System.out.println();
        System.out.println("search 離頂部最近的0:" + stack.search(0));
複製程式碼

示例結果

總結

Stack和Vector的程式碼都很簡單,使用陣列進行資料儲存。Stack的先進後出特性很好用,常在演算法題中得到應用;Vector雖然保證了執行緒安全,但考慮到大部分使用場景都是單執行緒模式,所以對效率稍有影響。

關注微信公眾號,最新技術乾貨實時推送

image

相關文章