深度探秘: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 接口的实现类 ArrayDeque 和 LinkedList。下面简单对比一下它们的特点:
Stack:基于Vector实现,线程安全,但由于使用了同步机制,在单线程环境下性能可能不如ArrayDeque和LinkedList。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方法:首先调用父类Vector的addElement方法将元素添加到数组的末尾,由于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方法查看栈顶元素,然后调用父类Vector的removeElementAt方法移除栈顶元素,最后返回被移除的栈顶元素。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异常。然后调用父类Vector的elementAt方法返回栈顶元素。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方法:首先调用父类Vector的lastIndexOf方法查找元素在数组中的最后一次出现的索引。如果元素存在于数组中,计算元素在栈中的位置,从栈顶开始计数,栈顶元素的位置为 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 类也具备了线程安全的特性。例如,push、pop、peek 等方法都是同步方法,同一时间只能有一个线程访问这些方法,从而避免多个线程同时修改栈导致数据不一致的问题。以下是 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 类。ArrayDeque 是 Deque 接口的一个实现类,它可以作为栈来使用,并且性能比 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方法调用了父类Vector的addElement方法,在大多数情况下,入栈操作的时间复杂度为 。但当数组需要扩容时,需要进行数组的复制操作,时间复杂度为 ,其中 为数组的长度。 - 出栈操作(pop):
pop方法首先调用peek方法查看栈顶元素,时间复杂度为 ,然后调用父类Vector的removeElementAt方法移除栈顶元素,时间复杂度也为 。因此,出栈操作的时间复杂度为 。 - 查看栈顶元素(peek):
peek方法直接返回栈顶元素,时间复杂度为 。 - 判断栈是否为空(empty):
empty方法调用size方法获取栈的元素数量,时间复杂度为 ,然后判断元素数量是否为 0,时间复杂度也为 。因此,判断栈是否为空的时间复杂度为 。 - 查找元素在栈中的位置(search):
search方法调用父类Vector的lastIndexOf方法查找元素在数组中的最后一次出现的索引,时间复杂度为 ,其中 为数组的长度。然后计算元素在栈中的位置,时间复杂度为 。因此,查找元素在栈中的位置的时间复杂度为 。
7.2 空间复杂度分析
Stack 的空间复杂度主要取决于数组的容量和元素的数量。在初始化时,Stack 会创建一个初始容量为 10 的数组,因此空间复杂度为 。随着元素的添加,当数组需要扩容时,空间复杂度会随着数组容量的增加而增加。
7.3 与其他栈实现的性能对比
- 与
ArrayDeque的对比:- 单线程环境:在单线程环境下,
ArrayDeque是非线程安全的,不需要进行锁的获取和释放操作,因此性能比Stack更好。特别是在频繁进行入栈和出栈操作时,ArrayDeque的性能优势更加明显。 - 多线程环境:在多线程环境下,
Stack是线程安全的,而ArrayDeque不是。如果需要在多线程环境下使用栈,可以使用Collections.synchronizedDeque方法将ArrayDeque转换为线程安全的双端队列,或者使用Stack类。
- 单线程环境:在单线程环境下,
- 与
LinkedList的对比:- 插入和删除操作:
LinkedList基于链表实现,在插入和删除操作上具有较好的性能,特别是在链表的头部和尾部进行操作时。但Stack基于数组实现,在插入和删除操作上可能需要移动大量元素,性能相对较差。 - 随机访问性能:
Stack基于数组实现,支持随机访问,时间复杂度为 。而LinkedList基于链表实现,随机访问性能较差,时间复杂度为 。
- 插入和删除操作:
八、应用场景
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);
}
}
在上述示例代码中,使用两个栈 values 和 operators 分别存储操作数和运算符。遍历表达式,根据字符的类型进行相应的处理,最后计算出表达式的值。
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 的入栈、出栈和查看栈顶元素操作在理想情况下时间复杂度为 ,但由于其线程安全机制带来的同步开销,在单线程环境下性能不如非线程安全的栈实现(如 ArrayDeque) 。此外,在数组扩容时,入栈操作的时间复杂度会变为 。
在应用场景上,Stack 凭借其后进先出的特性,在表达式求值、回溯算法、方法调用栈管理以及编辑器的撤销重做功能等领域发挥着重要作用。理解 Stack 的原理,有助于开发者在合适的场景中选择和使用它,从而编写出更高效、逻辑清晰的代码。
9.2 展望
随着 Java 技术的不断发展,虽然 Stack 类本身可能不会有太大的变化,但开发者对于栈这种数据结构的应用和优化会不断演进。例如,在并发编程领域,可能会出现更高效的线程安全栈实现,以减少同步带来的性能损耗 。在一些特定的业务场景中,开发者可能会基于 Stack 的原理,自定义实现更符合需求的栈结构,比如限制栈大小、实现带有优先级的栈等。
同时,随着编程语言和开发框架的不断更新,栈的应用场景也可能会进一步拓展。例如,在新兴的大数据处理、人工智能等领域,栈的特性或许会被应用于更复杂的算法和数据处理流程中。对于开发者而言,持续深入理解数据结构的原理,将有助于在不断变化的技术环境中,灵活运用这些知识解决实际问题,创造出更优秀的软件产品。