Java 中的栈-Stack类源码解读

本文详细解析了Java中Stack类的源码实现,包括继承关系、核心方法如push和pop的具体实现过程,以及Stack如何利用Vector进行动态扩容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Stack继承自Vector,实现了栈元素的基本操作,最典型的就是push压栈和pop出栈,整个Stack.java的代码很简单:

public class Stack<E> extends Vector<E> {

    public Stack() {
    }

    /**
     * 压入一个元素入栈,实际就是调用Vector的addElement方法
     * @see     java.util.Vector#addElement
     */
    public E push(E item) {
        addElement(item);
        return item;
    }

    /**
     * 弹元素出栈:拿到栈顶元素,从栈顶移除元素,并将元素返回
     */
    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    /**
     * 只通过elementAt方法去取到栈顶元素
     */
    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    /**
     * 返回栈是否为空
     */
    public boolean empty() {
        return size() == 0;
    }

    /**
     * 搜索元素在栈内的index,如果栈内有多个相同元素,则拿到的是距离栈顶最近的index
     */
    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = 1224463164541339165L;
}

栈的实现很大程度上是通过父类Vector来实现的,Vector内部通过一个对象数组来存储元素:

//Vector.java
protected Object[] elementData

elementData 数组满了之后,添加新元素时,会动态扩展数组长度,最大的容量为Integer.MAX_VALUE

入栈

通过代码可以看到,Stack通过父类 Vector 的addElement()方法来实现入栈,这个方法是多线程安全的:

    //Vector.java
    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

实际上这个方法就是每次判断数组是否满了,未满则直接将元素放入栈顶,如果满了则将栈的空间在旧elementData.length的基础上扩充一倍,然后将新的元素放入栈顶。

下面来看具体是怎么实现空间的扩充的,从ensureCapacityHelper入手,代码如下:

//Vector.java
 private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

从上面可以看到 minCapacity 的值为elementCount + 1 ,即当前元素个数+1,此时的elementData数组的长度是多少呢?我们可以看看默认情况下的构造方法,传入的初始容量initialCapacity为10,增量capacityIncrement为0

    public Vector() {
        this(10);
    }
    public Vector(int initialCapacity) {
        this(initialCapacity, 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;
    }

所以第一次stack压入元素时,elementData 数组长度为10,不会扩展空间,只是将数组的elementData[elementCount++]位置指向插入的元素。当元素个数大于数组长度10时,if 里面成立,所以会执行grow方法来扩大数组长度,代码如下:

//Vector.java
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

这里的capacityIncrement是 vector 容量的增量值,如果capacityIncrement值为0,那么通过上面的代码可以知道,newCapacity拿到的是2倍的 oldCapacity,也就是原来的数组长度的2倍。从capacityIncrement的注释文档里可以看到:

//Vector.java
/**
     * The amount by which the capacity of the vector is automatically
     * incremented when its size becomes greater than its capacity.  If
     * the capacity increment is less than or equal to zero, the capacity
     * of the vector is doubled each time it needs to grow.
     * 
     * 翻译:capacityIncrement是vector容量的增量值,当vector的尺寸变大时,
     * 会自动增加capacityIncrement大小,而当capacityIncrement小于等于0,则容量会双倍地增加
     */
    protected int capacityIncrement;

由于 Vector 的默认构造方法里传入的initialCapacity是10,所以最开始的数组长度为10,在超过10后,每次容量翻倍,这点可以举个例子测试下:

/**
* 通过反射拿到Vector里面的elementData数组,打印出数组长度
*/
private Object[] getElements(Vector vector) {

        try {
            Field capacityField = vector.getClass().getDeclaredField("elementData");
            //设置Field可访问,protected类型的elementData无法直接访问
            capacityField.setAccessible(true);
            Object[] elementData = (Object[]) capacityField.get(vector);
            Log.e("lpc","elementData: " + elementData.length) ;
            return elementData ;
        } catch (NoSuchFieldException e) {
            Log.e("lpc","NoSuchFieldException: " + e.getMessage()) ;
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            Log.e("lpc","IllegalAccessException: " + e.getMessage()) ;
            e.printStackTrace();
        }

        return null ;
    }

    public void init() {
        //默认new Vector时,容量为10,elementData数组长度为10
        Vector<Integer> vector = new Vector<>();
        //手动决定初始容量和增量
        //Vector<Integer> vector = new Vector<>(10,1);
        for (int i=0;i<9;i++) {
            vector.add(i);
        }
        //此时有9个元素,数组长度应该为10
        getElements(vector);

        vector.add(10);
        //此时有10个元素,数组长度也应该为10
        getElements(vector);

        vector.add(11);
        //此时有11个元素,数组扩大一倍,因此应该是20
        getElements(vector);
        for (int i=0;i<9;i++) {
            vector.add(i);
        }
        vector.add(12) ;
        //连续添加了9+1个元素,此时有21个元素,数组长度应该再翻一倍,为40
        getElements(vector);

        vector.add(13) ;
        //22个元素,长度为40
        getElements(vector);
   }

日志如下:

E/lpc: elementData: 10
E/lpc: elementData: 10
E/lpc: elementData: 20
E/lpc: elementData: 40
E/lpc: elementData: 40

如我们猜想的一样,当 capacityIncrement = 0时,每次扩容都是在原来基础上加倍,现在我们改动一句话,在Vector的构造方法里传入 initialCapacity 为10,capacityIncrement 为1,意思是初始容量为10,每次扩容数组长度加1 :

Vector<Integer> vector = new Vector<>(10,1);

这种情况下,当数组元素个数为9,10,11,21,22 时的数组长度结果如下,每次扩容时,正好是当前元素的个数+1:

E/lpc: elementData: 10
E/lpc: elementData: 10
E/lpc: elementData: 11
E/lpc: elementData: 21
E/lpc: elementData: 22

再回到栈上面来,由于Stack的构造方法只有一个默认的public Stack() {},所以其存储数据时,遵循的扩容原则就是默认的双倍扩容方式,因此每次扩容时,空间上的浪费还是很大的。
回到 grow 方法,确定扩容后的新容量newCapacity后,调用Arrays.copyOf方法,来复制出一个长度为newCapacity的 elementData 数组。接着回到addElement(E object)方法里,执行elementData[elementCount++] = obj;,将新的元素存入数组最顶部。

出栈

出栈时,先通过peek方法拿到栈顶元素,然后删除栈顶元素,返回元素。pop方法也是线程安全的:

//Stack.java
 public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        //移除栈顶的元素
        removeElementAt(len - 1);

        return obj;
    }

删除元素也是通过父类Vector里面的removeElementAt方法实现,代码如下:

//Vector.java
public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }

这里的System.arraycopy(elementData, index + 1, elementData, index, j);实际上就是将整个elementData数组从index+1开始向前移一位,将index所在位置的值覆盖掉,以此来达到remove的效果,复制后将栈顶置空,方便GC回收内存。在pop方法中,传入的是len-1,因此每次 pop 都只会移除栈顶上的元素。System.arrayCopy方法如下:

//System.java
/**
 *将 源数组src 从起点位置srcPos开始的lenght长度的部分,
 *复制到目标数组的destPos位置
 * @param      src      源数组.
 * @param      srcPos   源数组的起点位置.
 * @param      dest     目标数组.
 * @param      destPos  (复制的内容放在)目标数组的开始位置.
 * @param      length   需要copy的数组长度.
*/
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值