深度探秘:Java Stack 使用原理全解析

185 阅读27分钟

深度探秘:Java Stack 使用原理全解析

一、引言

在 Java 编程的广阔领域中,数据结构宛如构建程序大厦的基石,为开发者提供了丰富多样的选择来存储和管理数据。其中,Stack 作为一种经典的数据结构,遵循后进先出(LIFO)的原则,在众多场景中发挥着关键作用。无论是表达式求值、回溯算法,还是实现撤销操作,Stack 都展现出了其独特的魅力。

本文将深入到 Java Stack 的源码层面,全方位剖析其使用原理。我们将从基本概念入手,详细探讨其继承关系、核心属性和构造函数,接着深入分析各种操作方法的实现细节,包括元素的入栈、出栈、查看栈顶元素等。同时,我们还会关注 Stack 的线程安全特性以及性能表现,并与其他类似的数据结构进行对比。通过本文的学习,你将对 Java Stack 有一个全面而深入的理解,能够在实际开发中更加得心应手地运用它。

二、Stack 概述

2.1 基本概念

Stack 是一种线性数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。这意味着最后添加到栈中的元素将最先被移除,就像一摞盘子,最后放上去的盘子总是最先被拿走。在 Java 中,Stack 类是 Vector 类的子类,它提供了一系列方法来实现栈的基本操作,如入栈(push)、出栈(pop)、查看栈顶元素(peek)等。

2.2 继承关系与接口实现

下面是 Stack 类的定义以及它的继承关系和接口实现:

// Stack 类继承自 Vector 类,这意味着它继承了 Vector 类的所有属性和方法
// 同时也继承了 Vector 类的线程安全特性
public class Stack<E> extends Vector<E> {
    // 类的具体实现将在后续详细分析
}

从上述代码可以看出,Stack 类继承自 Vector 类。Vector 是 Java 早期提供的一个动态数组实现,它是线程安全的,这使得 Stack 也具备了线程安全的特性。此外,由于 Vector 实现了 List 接口,Stack 也间接实现了 List 接口,因此可以使用 List 接口提供的一些方法。

2.3 与其他数据结构的对比

在 Java 中,还有其他一些数据结构也可以实现类似栈的功能,如 Deque 接口的实现类 ArrayDequeLinkedList。下面简单对比一下它们的特点:

  • Stack:基于 Vector 实现,线程安全,但由于使用了同步机制,在单线程环境下性能可能不如 ArrayDequeLinkedList
  • ArrayDeque:基于数组实现,不支持线程安全,但在性能上表现较好,特别是在频繁进行入栈和出栈操作时。
  • LinkedList:基于链表实现,同样不支持线程安全,在插入和删除操作上具有较好的性能,但随机访问性能较差。

三、Stack 的内部结构

3.1 核心属性

由于 Stack 类继承自 Vector 类,它继承了 Vector 类的核心属性,用于存储元素和管理数组的状态。以下是 Vector 类的核心属性,也是 Stack 类实际使用的属性:

// 存储元素的数组,实际存储 Stack 中的元素
protected Object[] elementData;
// 数组中实际存储的元素数量
protected int elementCount;
// 数组的容量增量,当数组需要扩容时,容量会增加这个值
protected int capacityIncrement;
  • elementData:这是一个 Object 类型的数组,用于实际存储 Stack 中的元素。由于 Java 数组的长度是固定的,当元素数量超过数组的容量时,需要对数组进行扩容。
  • elementCount:表示数组中实际存储的元素数量,它的值始终小于或等于 elementData 数组的长度。
  • capacityIncrement:数组的容量增量,当数组需要扩容时,容量会增加这个值。如果 capacityIncrement 为 0,则每次扩容时数组的容量会翻倍。

3.2 构造函数

Stack 类提供了一个无参构造函数,用于创建一个空的栈。以下是该构造函数的源码:

// 构造一个空的 Stack
public Stack() {
    // 调用父类 Vector 的无参构造函数
    // 父类的无参构造函数会创建一个初始容量为 10 的数组
    super();
}

从上述代码可以看出,Stack 类的无参构造函数只是简单地调用了父类 Vector 的无参构造函数。这意味着创建一个 Stack 对象时,会创建一个初始容量为 10 的数组来存储元素。

3.3 数组扩容机制

由于 Stack 类继承自 Vector 类,它的数组扩容机制与 Vector 类相同。当 Stack 中的元素数量超过数组的容量时,需要对数组进行扩容。Vector 的扩容机制主要通过 ensureCapacityHelper 方法实现,以下是该方法的源码:

// 确保数组的容量足够容纳指定的最小容量
private void ensureCapacityHelper(int minCapacity) {
    // 如果最小容量超过数组的当前容量
    if (minCapacity - elementData.length > 0)
        // 调用 grow 方法进行扩容
        grow(minCapacity);
}

// 数组的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// 扩容数组
private void grow(int minCapacity) {
    // 获取旧数组的容量
    int oldCapacity = elementData.length;
    // 计算新数组的容量
    int newCapacity = oldCapacity + ((capacityIncrement > 0)?
                                     capacityIncrement : oldCapacity);
    // 如果新容量小于最小容量,则将新容量设置为最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量超过最大数组容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 调用 hugeCapacity 方法处理超大容量的情况
        newCapacity = hugeCapacity(minCapacity);
    // 将旧数组复制到新数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 处理超大容量的情况
private static int hugeCapacity(int minCapacity) {
    // 如果最小容量小于 0,抛出 OutOfMemoryError 异常
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 如果最小容量超过最大数组容量,则返回 Integer.MAX_VALUE,否则返回最大数组容量
    return (minCapacity > MAX_ARRAY_SIZE)?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
  • ensureCapacityHelper 方法:检查数组的当前容量是否足够容纳指定的最小容量,如果不够,则调用 grow 方法进行扩容。
  • grow 方法:计算新数组的容量,通常是旧容量加上容量增量(如果容量增量大于 0),或者是旧容量的两倍(如果容量增量为 0)。如果新容量小于最小容量,则将新容量设置为最小容量。如果新容量超过最大数组容量,则调用 hugeCapacity 方法处理超大容量的情况。最后,使用 Arrays.copyOf 方法将旧数组复制到新数组中。
  • hugeCapacity 方法:处理超大容量的情况,如果最小容量小于 0,抛出 OutOfMemoryError 异常。如果最小容量超过最大数组容量,则返回 Integer.MAX_VALUE,否则返回最大数组容量。

四、基本操作的源码分析

4.1 入栈操作(push)

4.1.1 push(E item) 方法

push(E item) 方法用于将一个元素压入栈顶。以下是该方法的源码:

// 将一个元素压入栈顶
public E push(E item) {
    // 调用父类 Vector 的 addElement 方法将元素添加到数组的末尾
    // 由于 Stack 遵循后进先出原则,添加到末尾就相当于压入栈顶
    addElement(item);
    // 返回被压入栈顶的元素
    return item;
}

// 父类 Vector 的 addElement 方法
public synchronized void addElement(E obj) {
    // 记录结构修改次数,用于迭代器的并发修改检查
    modCount++;
    // 确保数组的容量足够容纳新元素
    ensureCapacityHelper(elementCount + 1);
    // 将新元素添加到数组的末尾
    elementData[elementCount++] = obj;
}
  • push 方法:首先调用父类 VectoraddElement 方法将元素添加到数组的末尾,由于 Stack 遵循后进先出原则,添加到末尾就相当于压入栈顶。然后返回被压入栈顶的元素。
  • addElement 方法:首先记录结构修改次数,然后调用 ensureCapacityHelper 方法确保数组的容量足够容纳新元素。如果容量不足,会进行扩容操作。最后将新元素添加到数组的末尾,并将 elementCount 加 1。

4.2 出栈操作(pop)

4.2.1 pop() 方法

pop() 方法用于移除并返回栈顶元素。以下是该方法的源码:

// 移除并返回栈顶元素
public synchronized E pop() {
    E       obj;
    // 获取栈的元素数量
    int     len = size();
    // 调用 peek 方法查看栈顶元素
    obj = peek();
    // 调用父类 Vector 的 removeElementAt 方法移除栈顶元素
    removeElementAt(len - 1);
    // 返回被移除的栈顶元素
    return obj;
}

// 获取栈的元素数量
public synchronized int size() {
    // 返回 elementCount 的值,即数组中实际存储的元素数量
    return elementCount;
}

// 查看栈顶元素
public synchronized E peek() {
    // 获取栈的元素数量
    int     len = size();
    // 如果栈为空,抛出 EmptyStackException 异常
    if (len == 0)
        throw new EmptyStackException();
    // 返回栈顶元素
    return elementAt(len - 1);
}

// 父类 Vector 的 removeElementAt 方法
public synchronized void removeElementAt(int index) {
    // 记录结构修改次数
    modCount++;
    // 检查索引是否越界
    if (index >= elementCount) {
        // 如果索引越界,抛出 ArrayIndexOutOfBoundsException 异常
        throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                 elementCount);
    }
    else if (index < 0) {
        // 如果索引为负数,抛出 ArrayIndexOutOfBoundsException 异常
        throw new ArrayIndexOutOfBoundsException(index);
    }
    // 计算需要移动的元素数量
    int j = elementCount - index - 1;
    if (j > 0) {
        // 将指定位置后面的元素向前移动一位
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    // 元素数量减 1
    elementCount--;
    // 将数组的最后一个元素置为 null,帮助垃圾回收
    elementData[elementCount] = null; /* to let gc do its work */
}

// 父类 Vector 的 elementAt 方法
public synchronized E elementAt(int index) {
    // 检查索引是否越界
    if (index >= elementCount) {
        // 如果索引越界,抛出 ArrayIndexOutOfBoundsException 异常
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    }
    // 返回指定位置的元素
    return (E) elementData[index];
}
  • pop 方法:首先调用 peek 方法查看栈顶元素,然后调用父类 VectorremoveElementAt 方法移除栈顶元素,最后返回被移除的栈顶元素。
  • size 方法:返回 elementCount 的值,即数组中实际存储的元素数量。
  • peek 方法:首先获取栈的元素数量,如果栈为空,抛出 EmptyStackException 异常。然后返回栈顶元素。
  • removeElementAt 方法:首先记录结构修改次数,然后检查索引是否越界。如果索引越界,抛出 ArrayIndexOutOfBoundsException 异常。接着计算需要移动的元素数量,并使用 System.arraycopy 方法将指定位置后面的元素向前移动一位。最后将元素数量减 1,并将数组的最后一个元素置为 null,帮助垃圾回收。
  • elementAt 方法:首先检查索引是否越界,如果越界,抛出 ArrayIndexOutOfBoundsException 异常。然后返回指定位置的元素。

4.3 查看栈顶元素(peek)

4.3.1 peek() 方法

peek() 方法用于查看栈顶元素,但不移除该元素。以下是该方法的源码:

// 查看栈顶元素
public synchronized E peek() {
    // 获取栈的元素数量
    int     len = size();
    // 如果栈为空,抛出 EmptyStackException 异常
    if (len == 0)
        throw new EmptyStackException();
    // 返回栈顶元素
    return elementAt(len - 1);
}

// 获取栈的元素数量
public synchronized int size() {
    // 返回 elementCount 的值,即数组中实际存储的元素数量
    return elementCount;
}

// 父类 Vector 的 elementAt 方法
public synchronized E elementAt(int index) {
    // 检查索引是否越界
    if (index >= elementCount) {
        // 如果索引越界,抛出 ArrayIndexOutOfBoundsException 异常
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    }
    // 返回指定位置的元素
    return (E) elementData[index];
}
  • peek 方法:首先获取栈的元素数量,如果栈为空,抛出 EmptyStackException 异常。然后调用父类 VectorelementAt 方法返回栈顶元素。
  • size 方法:返回 elementCount 的值,即数组中实际存储的元素数量。
  • elementAt 方法:首先检查索引是否越界,如果越界,抛出 ArrayIndexOutOfBoundsException 异常。然后返回指定位置的元素。

4.4 判断栈是否为空(empty)

4.4.1 empty() 方法

empty() 方法用于判断栈是否为空。以下是该方法的源码:

// 判断栈是否为空
public boolean empty() {
    // 如果栈的元素数量为 0,则返回 true,否则返回 false
    return size() == 0;
}

// 获取栈的元素数量
public synchronized int size() {
    // 返回 elementCount 的值,即数组中实际存储的元素数量
    return elementCount;
}
  • empty 方法:调用 size 方法获取栈的元素数量,如果元素数量为 0,则返回 true,表示栈为空;否则返回 false,表示栈不为空。
  • size 方法:返回 elementCount 的值,即数组中实际存储的元素数量。

4.5 查找元素在栈中的位置(search)

4.5.1 search(Object o) 方法

search(Object o) 方法用于查找元素在栈中的位置,从栈顶开始计数,栈顶元素的位置为 1。如果元素不在栈中,返回 -1。以下是该方法的源码:

// 查找元素在栈中的位置,从栈顶开始计数,栈顶元素的位置为 1
// 如果元素不在栈中,返回 -1
public synchronized int search(Object o) {
    // 调用父类 Vector 的 lastIndexOf 方法查找元素在数组中的最后一次出现的索引
    int i = lastIndexOf(o);
    // 如果元素存在于数组中
    if (i >= 0) {
        // 计算元素在栈中的位置,从栈顶开始计数
        return size() - i;
    }
    // 如果元素不在数组中,返回 -1
    return -1;
}

// 父类 Vector 的 lastIndexOf 方法
public synchronized int lastIndexOf(Object o) {
    // 从数组的最后一个元素开始向前查找
    return lastIndexOf(o, elementCount-1);
}

// 父类 Vector 的 lastIndexOf 方法,从指定位置开始向前查找元素的最后一次出现的索引
public synchronized int lastIndexOf(Object o, int index) {
    // 如果要查找的元素为 null
    if (o == null) {
        // 从指定位置开始向前遍历数组
        for (int i = Math.min(index, elementCount-1); i >= 0; i--)
            if (elementData[i]==null)
                // 如果找到元素,返回其索引
                return i;
    } else {
        // 如果要查找的元素不为 null
        for (int i = Math.min(index, elementCount-1); i >= 0; i--)
            if (o.equals(elementData[i]))
                // 如果找到元素,返回其索引
                return i;
    }
    // 如果未找到元素,返回 -1
    return -1;
}

// 获取栈的元素数量
public synchronized int size() {
    // 返回 elementCount 的值,即数组中实际存储的元素数量
    return elementCount;
}
  • search 方法:首先调用父类 VectorlastIndexOf 方法查找元素在数组中的最后一次出现的索引。如果元素存在于数组中,计算元素在栈中的位置,从栈顶开始计数,栈顶元素的位置为 1。如果元素不在数组中,返回 -1。
  • lastIndexOf 方法:从数组的最后一个元素开始向前查找元素的最后一次出现的索引。
  • lastIndexOf 方法(重载):从指定位置开始向前查找元素的最后一次出现的索引。如果要查找的元素为 null,则遍历数组,找到 null 元素的索引并返回;如果要查找的元素不为 null,则使用 equals 方法比较元素,找到匹配元素的索引并返回。如果未找到元素,返回 -1。
  • size 方法:返回 elementCount 的值,即数组中实际存储的元素数量。

五、迭代器的实现

5.1 迭代器接口

Stack 类继承自 Vector 类,因此可以使用 Vector 类提供的迭代器来遍历栈中的元素。Vector 类实现了 Iterable 接口,因此可以使用 iterator() 方法获取一个迭代器。以下是 iterator() 方法的源码:

// 获取一个迭代器
public Iterator<E> iterator() {
    // 返回一个 Itr 对象
    return new Itr();
}

// 内部类 Itr 实现了 Iterator 接口,用于迭代 Vector 中的元素
private class Itr implements Iterator<E> {
    // 下一个要返回的元素的索引
    int cursor;       // index of next element to return
    // 上一次返回的元素的索引
    int lastRet = -1; // index of last element returned; -1 if no such
    // 记录创建迭代器时 Vector 的修改次数,用于并发修改检查
    int expectedModCount = modCount;

    // 判断是否还有下一个元素
    public boolean hasNext() {
        // 如果下一个要返回的元素的索引小于元素数量,则返回 true,否则返回 false
        return cursor != elementCount;
    }

    // 获取下一个元素
    public E next() {
        // 检查是否有并发修改
        synchronized (Vector.this) {
            checkForComodification();
            // 获取下一个要返回的元素的索引
            int i = cursor;
            if (i >= elementCount)
                // 如果索引越界,抛出 NoSuchElementException 异常
                throw new NoSuchElementException();
            // 更新下一个要返回的元素的索引
            cursor = i + 1;
            // 返回上一次返回的元素的索引
            return elementData(lastRet = i);
        }
    }

    // 删除当前迭代的元素
    public void remove() {
        if (lastRet == -1)
            // 如果上一次返回的元素的索引为 -1,抛出 IllegalStateException 异常
            throw new IllegalStateException();
        // 检查是否有并发修改
        synchronized (Vector.this) {
            checkForComodification();
            try {
                // 调用 Vector 的 remove 方法删除元素
                Vector.this.remove(lastRet);
                // 更新下一个要返回的元素的索引
                cursor = lastRet;
                // 将上一次返回的元素的索引置为 -1
                lastRet = -1;
                // 更新 expectedModCount
                expectedModCount = modCount;
            } catch (ArrayIndexOutOfBoundsException ex) {
                // 如果发生 ArrayIndexOutOfBoundsException 异常,抛出 ConcurrentModificationException 异常
                throw new ConcurrentModificationException();
            }
        }
    }

    // 检查是否有并发修改
    final void checkForComodification() {
        if (modCount != expectedModCount)
            // 如果 Vector 的修改次数与创建迭代器时记录的修改次数不一致,抛出 ConcurrentModificationException 异常
            throw new ConcurrentModificationException();
    }
}
  • iterator() 方法:返回一个 Itr 对象,用于迭代 Vector 中的元素。
  • Itr 类实现了 Iterator 接口,提供了以下方法:
    • hasNext():判断是否还有下一个元素。
    • next():获取下一个元素。在获取元素之前,会检查是否有并发修改,如果有则抛出 ConcurrentModificationException 异常。
    • remove():删除当前迭代的元素。在删除元素之前,会检查是否有并发修改,如果有则抛出 ConcurrentModificationException 异常。
    • checkForComodification():检查是否有并发修改,如果 Vector 的修改次数与创建迭代器时记录的修改次数不一致,抛出 ConcurrentModificationException 异常。

5.2 迭代顺序

需要注意的是,使用 Stack 的迭代器遍历栈中的元素时,迭代顺序是从栈底到栈顶,这与栈的后进先出原则相反。如果需要按照后进先出的顺序遍历栈中的元素,可以使用 Collections.reverse 方法将栈中的元素反转,然后再进行迭代。以下是一个示例代码:

import java.util.Stack;
import java.util.Collections;
import java.util.Iterator;

public class StackIterationExample {
    public static void main(String[] args) {
        // 创建一个 Stack 对象
        Stack<Integer> stack = new Stack<>();
        // 向栈中添加元素
        stack.push(1);
        stack.push(2);
        stack.push(3);

        // 正常迭代,顺序是从栈底到栈顶
        System.out.println("Normal iteration (bottom to top):");
        Iterator<Integer> iterator = stack.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 反转栈中的元素,然后迭代,顺序是从栈顶到栈底
        System.out.println("Reverse iteration (top to bottom):");
        Collections.reverse(stack);
        iterator = stack.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

在上述示例代码中,首先创建一个 Stack 对象,并向栈中添加元素。然后使用 iterator() 方法获取一个迭代器,正常迭代栈中的元素,顺序是从栈底到栈顶。接着使用 Collections.reverse 方法将栈中的元素反转,再使用迭代器迭代栈中的元素,顺序是从栈顶到栈底。

六、线程安全机制

6.1 同步方法

Stack 类继承自 Vector 类,而 Vector 类的方法大多使用了 synchronized 关键字进行同步,这使得 Stack 类也具备了线程安全的特性。例如,pushpoppeek 等方法都是同步方法,同一时间只能有一个线程访问这些方法,从而避免多个线程同时修改栈导致数据不一致的问题。以下是 push 方法的源码示例:

// 将一个元素压入栈顶
public E push(E item) {
    // 调用父类 Vector 的 addElement 方法将元素添加到数组的末尾
    // 由于 Stack 遵循后进先出原则,添加到末尾就相当于压入栈顶
    addElement(item);
    // 返回被压入栈顶的元素
    return item;
}

// 父类 Vector 的 addElement 方法
public synchronized void addElement(E obj) {
    // 记录结构修改次数,用于迭代器的并发修改检查
    modCount++;
    // 确保数组的容量足够容纳新元素
    ensureCapacityHelper(elementCount + 1);
    // 将新元素添加到数组的末尾
    elementData[elementCount++] = obj;
}

从上述代码可以看出,addElement 方法使用了 synchronized 关键字进行同步,这意味着同一时间只能有一个线程调用该方法,从而保证了线程安全。

6.2 性能影响

虽然 synchronized 关键字保证了 Stack 的线程安全,但也带来了一定的性能开销。因为每次调用同步方法时,线程都需要获取锁,这会导致线程之间的竞争和等待,降低了并发性能。在单线程环境下,使用 Stack 会比使用非线程安全的栈实现(如 ArrayDeque)慢,因为非线程安全的栈实现不需要进行锁的获取和释放操作。

6.3 替代方案

在现代 Java 开发中,如果需要在单线程环境下使用栈,可以考虑使用 ArrayDeque 类。ArrayDequeDeque 接口的一个实现类,它可以作为栈来使用,并且性能比 Stack 更好。以下是一个使用 ArrayDeque 作为栈的示例代码:

import java.util.ArrayDeque;

public class ArrayDequeStackExample {
    public static void main(String[] args) {
        // 创建一个 ArrayDeque 对象作为栈使用
        ArrayDeque<Integer> stack = new ArrayDeque<>();
        // 入栈操作
        stack.push(1);
        stack.push(2);
        stack.push(3);
        // 出栈操作
        while (!stack.isEmpty()) {
            System.out.println(stack.pop());
        }
    }
}

在上述示例代码中,创建了一个 ArrayDeque 对象作为栈使用,使用 push 方法进行入栈操作,使用 pop 方法进行出栈操作。由于 ArrayDeque 是非线程安全的,在单线程环境下性能比 Stack 更好。

七、性能分析

7.1 时间复杂度分析

  • 入栈操作(push)push 方法调用了父类 VectoraddElement 方法,在大多数情况下,入栈操作的时间复杂度为 O(1)O(1)。但当数组需要扩容时,需要进行数组的复制操作,时间复杂度为 O(n)O(n),其中 nn 为数组的长度。
  • 出栈操作(pop)pop 方法首先调用 peek 方法查看栈顶元素,时间复杂度为 O(1)O(1),然后调用父类 VectorremoveElementAt 方法移除栈顶元素,时间复杂度也为 O(1)O(1)。因此,出栈操作的时间复杂度为 O(1)O(1)
  • 查看栈顶元素(peek)peek 方法直接返回栈顶元素,时间复杂度为 O(1)O(1)
  • 判断栈是否为空(empty)empty 方法调用 size 方法获取栈的元素数量,时间复杂度为 O(1)O(1),然后判断元素数量是否为 0,时间复杂度也为 O(1)O(1)。因此,判断栈是否为空的时间复杂度为 O(1)O(1)
  • 查找元素在栈中的位置(search)search 方法调用父类 VectorlastIndexOf 方法查找元素在数组中的最后一次出现的索引,时间复杂度为 O(n)O(n),其中 nn 为数组的长度。然后计算元素在栈中的位置,时间复杂度为 O(1)O(1)。因此,查找元素在栈中的位置的时间复杂度为 O(n)O(n)

7.2 空间复杂度分析

Stack 的空间复杂度主要取决于数组的容量和元素的数量。在初始化时,Stack 会创建一个初始容量为 10 的数组,因此空间复杂度为 O(10)O(10)。随着元素的添加,当数组需要扩容时,空间复杂度会随着数组容量的增加而增加。

7.3 与其他栈实现的性能对比

  • ArrayDeque 的对比
    • 单线程环境:在单线程环境下,ArrayDeque 是非线程安全的,不需要进行锁的获取和释放操作,因此性能比 Stack 更好。特别是在频繁进行入栈和出栈操作时,ArrayDeque 的性能优势更加明显。
    • 多线程环境:在多线程环境下,Stack 是线程安全的,而 ArrayDeque 不是。如果需要在多线程环境下使用栈,可以使用 Collections.synchronizedDeque 方法将 ArrayDeque 转换为线程安全的双端队列,或者使用 Stack 类。
  • LinkedList 的对比
    • 插入和删除操作LinkedList 基于链表实现,在插入和删除操作上具有较好的性能,特别是在链表的头部和尾部进行操作时。但 Stack 基于数组实现,在插入和删除操作上可能需要移动大量元素,性能相对较差。
    • 随机访问性能Stack 基于数组实现,支持随机访问,时间复杂度为 O(1)O(1)。而 LinkedList 基于链表实现,随机访问性能较差,时间复杂度为 O(n)O(n)

八、应用场景

8.1 表达式求值

在表达式求值中,栈可以用于处理运算符和操作数。例如,在计算中缀表达式时,可以使用两个栈,一个栈用于存储运算符,另一个栈用于存储操作数。遍历表达式,遇到操作数时将其压入操作数栈,遇到运算符时,根据运算符的优先级进行相应的处理。以下是一个简单的示例代码:

import java.util.Stack;

public class ExpressionEvaluation {
    // 判断字符是否为运算符
    private static boolean isOperator(char c) {
        return c == '+' || c == '-' || c == '*' || c == '/';
    }

    // 获取运算符的优先级
    private static int precedence(char operator) {
        switch (operator) {
            case '+':
            case '-':
                return 1;
            case '*':
            case '/':
                return 2;
            default:
                return -1;
        }
    }

    // 计算表达式的值
    public static int evaluate(String expression) {
        // 创建操作数栈
        Stack<Integer> values = new Stack<>();
        // 创建运算符栈
        Stack<Character> operators = new Stack<>();

        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);
            // 如果是空格,跳过
            if (c == ' ') {
                continue;
            }
            // 如果是数字,将其转换为整数并压入操作数栈
            if (Character.isDigit(c)) {
                StringBuilder sb = new StringBuilder();
                while (i < expression.length() && Character.isDigit(expression.charAt(i))) {
                    sb.append(expression.charAt(i++));
                }
                i--;
                values.push(Integer.parseInt(sb.toString()));
            } 
            // 如果是左括号,压入运算符栈
            else if (c == '(') {
                operators.push(c);
            } 
            // 如果是右括号,计算括号内的表达式
            else if (c == ')') {
                while (!operators.isEmpty() && operators.peek() != '(') {
                    values.push(applyOperation(operators.pop(), values.pop(), values.pop()));
                }
                operators.pop(); // 弹出左括号
            } 
            // 如果是运算符
            else if (isOperator(c)) {
                while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(c)) {
                    values.push(applyOperation(operators.pop(), values.pop(), values.pop()));
                }
                operators.push(c);
            }
        }

        // 处理剩余的运算符
        while (!operators.isEmpty()) {
            values.push(applyOperation(operators.pop(), values.pop(), values.pop()));
        }

        return values.pop();
    }

    // 执行运算操作
    private static int applyOperation(char operator, int b, int a) {
        switch (operator) {
            case '+':
                return a + b;
            case '-':
                return a - b;
            case '*':
                return a * b;
            case '/':
                if (b == 0) {
                    throw new UnsupportedOperationException("Cannot divide by zero");
                }
                return a / b;
        }
        return 0;
    }

    public static void main(String[] args) {
        String expression = "3 + 5 * ( 2 - 8 ) / 2";
        int result = evaluate(expression);
        System.out.println("Result: " + result);
    }
}

在上述示例代码中,使用两个栈 valuesoperators 分别存储操作数和运算符。遍历表达式,根据字符的类型进行相应的处理,最后计算出表达式的值。

8.2 回溯算法

在回溯算法中,栈可以用于记录路径和状态。例如,在迷宫求解问题中,可以使用栈来记录走过的路径,当遇到死路时,通过出栈操作回溯到上一个节点。以下是一个简单的迷宫求解示例代码:

import java.util.Stack;

public class MazeSolver {
    private static final int[][] DIRECTIONS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    public static boolean solveMaze(int[][] maze, int startX, int startY, int endX, int endY) {
        int rows = maze.length;
        int cols = maze[0].length;
        boolean[][] visited = new boolean[rows][cols];
        Stack<int[]> path = new Stack<>();

        path.push(new int[]{startX, startY});
        visited[startX][startY] = true;

        while (!path.isEmpty()) {
            int[] current = path.peek();
import java.util.Stack;

public class MazeSolver {
    private static final int[][] DIRECTIONS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    public static boolean solveMaze(int[][] maze, int startX, int startY, int endX, int endY) {
        int rows = maze.length;
        int cols = maze[0].length;
        boolean[][] visited = new boolean[rows][cols];
        Stack<int[]> path = new Stack<>();

        path.push(new int[]{startX, startY});
        visited[startX][startY] = true;

        while (!path.isEmpty()) {
            int[] current = path.peek();
            int x = current[0];
            int y = current[1];

            // 如果到达终点
            if (x == endX && y == endY) {
                return true;
            }

            boolean foundNext = false;
            for (int[] dir : DIRECTIONS) {
                int newX = x + dir[0];
                int newY = y + dir[1];

                // 检查新位置是否在迷宫范围内、不是墙壁且未被访问过
                if (newX >= 0 && newX < rows && newY >= 0 && newY < cols && 
                    maze[newX][newY] == 0 &&!visited[newX][newY]) {
                    path.push(new int[]{newX, newY});
                    visited[newX][newY] = true;
                    foundNext = true;
                    break;
                }
            }

            // 如果没有找到可走的下一个位置,回溯
            if (!foundNext) {
                path.pop();
            }
        }

        return false;
    }

    public static void main(String[] args) {
        int[][] maze = {
            {0, 1, 0, 0, 0},
            {0, 1, 0, 1, 0},
            {0, 0, 0, 0, 0},
            {0, 1, 1, 1, 0},
            {0, 0, 0, 1, 0}
        };
        System.out.println(solveMaze(maze, 0, 0, 4, 4));
    }
}

在上述代码中,Stack 用于存储路径上的位置。每次探索新的位置时,将新位置压入栈中,并标记为已访问。如果在当前位置找不到可走的路径,则通过 pop 操作回溯到上一个位置,继续探索其他方向。通过这种方式,Stack 帮助实现了回溯算法的核心逻辑,确保在迷宫中能够找到从起点到终点的路径(如果存在的话)。

8.3 方法调用栈

在 Java 程序运行时,虚拟机使用栈来管理方法调用。当一个方法被调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量、操作数栈、动态链接等信息 。当方法执行完毕时,对应的栈帧从栈中弹出。虽然这不是开发者直接使用 Stack 类来实现的,但理解 Stack 的后进先出原理有助于理解方法调用和程序执行的过程。例如:

public class MethodCallStackExample {
    public static void methodA() {
        System.out.println("Entering methodA");
        methodB();
        System.out.println("Exiting methodA");
    }

    public static void methodB() {
        System.out.println("Entering methodB");
        methodC();
        System.out.println("Exiting methodB");
    }

    public static void methodC() {
        System.out.println("Entering methodC");
        System.out.println("Exiting methodC");
    }

    public static void main(String[] args) {
        methodA();
    }
}

在上述代码中,当 main 方法调用 methodA 时,methodA 的栈帧被压入栈中;methodA 调用 methodB 时,methodB 的栈帧被压入栈中;methodB 调用 methodC 时,methodC 的栈帧被压入栈中 。当 methodC 执行完毕,其栈帧弹出;接着 methodB 执行完毕,其栈帧弹出;最后 methodA 执行完毕,其栈帧弹出 。这种调用和返回的过程,完全符合 Stack 后进先出的特性。

8.4 编辑器的撤销与重做功能

在文本编辑器或图形编辑器中,撤销与重做功能可以使用 Stack 来实现。当用户进行操作时,操作记录被压入撤销栈中;当用户执行撤销操作时,从撤销栈中弹出操作记录并反向执行;当用户执行重做操作时,将撤销操作记录压入重做栈中,再次执行时从重做栈中弹出并执行 。以下是一个简化的示例代码:

import java.util.Stack;

public class EditorUndoRedo {
    private Stack<String> undoStack = new Stack<>();
    private Stack<String> redoStack = new Stack<>();

    public void performAction(String action) {
        // 将操作记录压入撤销栈
        undoStack.push(action);
        // 清空重做栈,因为有新操作后,之前的重做记录失效
        redoStack.clear();
        System.out.println("Performed action: " + action);
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            String action = undoStack.pop();
            // 这里可以添加反向执行操作的逻辑,简化示例中仅打印
            System.out.println("Undid action: " + action);
            // 将撤销的操作记录压入重做栈
            redoStack.push(action);
        } else {
            System.out.println("Nothing to undo");
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            String action = redoStack.pop();
            // 这里可以添加执行操作的逻辑,简化示例中仅打印
            System.out.println("Redid action: " + action);
            // 将重做的操作记录压入撤销栈
            undoStack.push(action);
        } else {
            System.out.println("Nothing to redo");
        }
    }

    public static void main(String[] args) {
        EditorUndoRedo editor = new EditorUndoRedo();
        editor.performAction("Type 'Hello'");
        editor.performAction("Add comma");
        editor.undo();
        editor.redo();
    }
}

在上述代码中,undoStack 用于存储操作记录,实现撤销功能;redoStack 用于存储撤销后的操作记录,实现重做功能 。通过 Stack 的入栈和出栈操作,实现了编辑器中撤销与重做的核心逻辑。

九、总结与展望

9.1 总结

通过对 Java Stack 的源码级分析,我们深入了解了其使用原理。Stack 作为 Vector 的子类,继承了 Vector 的线程安全特性和动态数组的存储方式。其核心操作如入栈(push)、出栈(pop)、查看栈顶元素(peek)等,都基于数组的操作实现,并且通过 synchronized 关键字保证了多线程环境下的安全性。

在性能方面,虽然 Stack 的入栈、出栈和查看栈顶元素操作在理想情况下时间复杂度为 O(1)O(1),但由于其线程安全机制带来的同步开销,在单线程环境下性能不如非线程安全的栈实现(如 ArrayDeque) 。此外,在数组扩容时,入栈操作的时间复杂度会变为 O(n)O(n)

在应用场景上,Stack 凭借其后进先出的特性,在表达式求值、回溯算法、方法调用栈管理以及编辑器的撤销重做功能等领域发挥着重要作用。理解 Stack 的原理,有助于开发者在合适的场景中选择和使用它,从而编写出更高效、逻辑清晰的代码。

9.2 展望

随着 Java 技术的不断发展,虽然 Stack 类本身可能不会有太大的变化,但开发者对于栈这种数据结构的应用和优化会不断演进。例如,在并发编程领域,可能会出现更高效的线程安全栈实现,以减少同步带来的性能损耗 。在一些特定的业务场景中,开发者可能会基于 Stack 的原理,自定义实现更符合需求的栈结构,比如限制栈大小、实现带有优先级的栈等。

同时,随着编程语言和开发框架的不断更新,栈的应用场景也可能会进一步拓展。例如,在新兴的大数据处理、人工智能等领域,栈的特性或许会被应用于更复杂的算法和数据处理流程中。对于开发者而言,持续深入理解数据结构的原理,将有助于在不断变化的技术环境中,灵活运用这些知识解决实际问题,创造出更优秀的软件产品。