【Java】栈 Stack 详解:从原理到实战的完整指南
大家好,我是云扬~ 今天想和大家深入聊聊 Java 中的栈(Stack)数据结构。作为编程中最基础也最常用的数据结构之一,栈的 “后进先出” 特性在很多场景中都能发挥关键作用。这篇文章会从栈的核心原理、自定义实现、API 使用到实际应用,带大家全面掌握栈的知识点,文中会穿插完整代码示例,方便大家直接上手实践~
一、栈的核心概念:什么是 “后进先出”?
栈(Stack)是一种遵循 LIFO(Last In First Out,后进先出) 原则的线性数据结构。就像我们叠盘子,最后放上去的盘子会最先被拿走,栈中的元素也遵循同样的逻辑 —— 最后压入的元素,会在弹出时优先处理。
栈的核心操作只有两个:
- push(压入):将元素添加到栈顶
- pop(弹出):从栈顶移除并返回元素
除此之外,栈还有两个辅助判断操作:
- isEmpty(判空):判断栈是否为空
- isFull(判满):判断栈是否达到最大容量(数组实现时需要)
二、自定义栈实现:数组版栈的完整代码
虽然 Java 提供了现成的 Stack 类,但亲手实现一个栈能帮助我们更深入理解其原理。下面用数组实现一个基础的 int 类型栈,包含初始化、压入、弹出、遍历等核心功能:
public class CustomStack {
// 存储栈元素的数组
private int[] stack;
// 栈顶指针(-1表示栈空)
private int top;
// 栈的最大容量
private int capacity;
// 1. 初始化栈
public CustomStack(int capacity) {
this.capacity = capacity;
this.stack = new int[capacity];
this.top = -1; // 初始栈空
}
// 2. 压入元素(push)
public void push(int element) {
if (isFull()) {
throw new RuntimeException("栈已满,无法压入元素!");
}
top++; // 栈顶指针上移
stack[top] = element; // 存入元素
}
// 3. 弹出元素(pop)
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈已空,无法弹出元素!");
}
int element = stack[top]; // 取出栈顶元素
top--; // 栈顶指针下移
return element;
}
// 4. 查看栈顶元素(不弹出)
public int peek() {
if (isEmpty()) {
throw new RuntimeException("栈已空,无元素可查看!");
}
return stack[top];
}
// 5. 判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
// 6. 判断栈是否已满
public boolean isFull() {
return top == capacity - 1;
}
// 7. 获取栈的大小
public int size() {
return top + 1; // 栈顶指针+1即为元素个数
}
// 8. 打印栈中所有元素(从栈底到栈顶)
public void printStack() {
if (isEmpty()) {
System.out.println("栈为空!");
return;
}
System.out.print("栈元素(底->顶):");
for (int i = 0; i top; i++) {
System.out.print(stack[i] + " ");
}
System.out.println();
}
// 测试方法
public static void main(String[] args) {
CustomStack stack = new CustomStack(5); // 初始化容量为5的栈
// 压入元素
stack.push(10);
stack.push(20);
stack.push(30);
stack.printStack(); // 输出:栈元素(底->顶):10 20 30
// 查看栈顶元素
System.out.println("当前栈顶元素:" + stack.peek()); // 输出:30
// 弹出元素
int popElement = stack.pop();
System.out.println("弹出的元素:" + popElement); // 输出:30
stack.printStack(); // 输出:栈元素(底->顶):10 20
// 查看栈大小
System.out.println("当前栈大小:" + stack.size()); // 输出:2
}
}
代码说明:
- 用数组
stack存储元素,top指针标记栈顶位置,capacity限制栈的最大容量 push和pop操作的时间复杂度均为O(1),因为仅需操作栈顶指针,无需遍历数组- 加入了异常处理,避免栈溢出或空栈操作导致的错误
三、Java 中的 Stack 类:API 使用与源码解析
Java 的java.util.Stack类是官方实现的栈结构,它继承自Vector类,因此天然支持线程安全(但性能略低)。下面我们来看看它的核心用法和源码逻辑。
1. Stack 类的核心方法
| 方法名 | 功能描述 |
|---|---|
push(E item) | 压入元素到栈顶,返回该元素 |
pop() | 弹出并返回栈顶元素,栈空时抛EmptyStackException |
peek() | 查看栈顶元素(不弹出),栈空时抛EmptyStackException |
empty() | 判断栈是否为空,返回boolean |
search(Object o) | 从栈顶开始搜索元素,返回索引(栈顶为 1,未找到返回 – 1) |
2. 基础使用示例
import java.util.Stack;
public class StackDemo {
public static void main(String[] args) {
Stack<String> stack = new Stack<>();// 压入元素
stack.push("Java");
stack.push("Python");
stack.push("Golang");
// 查看栈顶
System.out.println("栈顶元素:" + stack.peek()); // 输出:Golang
// 搜索元素
System.out.println("Python的位置:" + stack.search("Python")); // 输出:2
// 弹出元素
System.out.println("弹出元素:" + stack.pop()); // 输出:Golang
// 遍历栈(注意:Stack无迭代器,需通过pop遍历,会清空栈)
System.out.print("栈中剩余元素:");
while (!stack.empty()) {
System.out.print(stack.pop() + " "); // 输出:Python Java
}
}
}
3. 源码关键点解析
- 继承关系:
public class Stack<E> extends Vector<E>的动态数组和同步机制 - push 方法:本质调用
Vector.addElement(item),自带synchronized锁,线程安全
public E push(E item) {
addElement(item);
return item;
}
- pop 方法:先通过
peek()获取栈顶(elementAt(size()-1)),再调用removeElementAt(size()-1)移除
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
四、栈的经典应用场景(附代码示例)
栈的 LIFO 特性让它在很多场景中大放异彩,下面分享 3 个最常用的应用场景:
1. 字符串反转
利用栈的 “先进后出”,将字符依次压入栈,再弹出即可实现反转:
public class StringReverse {
public static String reverse(String str) {
Stack<Character> stack = new Stack<>(); // 压入所有字符
for (char c : str.toCharArray()) {
stack.push(c);
}
// 弹出拼接反转字符串
StringBuilder sb = new StringBuilder();
while (!stack.empty()) {
sb.append(stack.pop());
}
return sb.toString();
}
public static void main(String[] args) {
String original = "Hello, Stack!";
String reversed = reverse(original);
System.out.println("原字符串:" + original);
System.out.println("反转后:" + reversed); // 输出:!katS ,olleH
}
}
2. 浏览器后退功能模拟
用栈存储访问过的 URL,点击后退时弹出栈顶 URL:
public class Browser {
private final Stack<String> history = new Stack<>();
// 访问新页面
public void visit(String url) {
history.push(url);
System.out.println("访问页面:" + url);
}
// 后退操作
public String back() {
if (history.size() == 1) {
System.out.println("已退至初始页面,无法继续后退!");
return history.peek();
}
history.pop(); // 移除当前页面
String prevUrl = history.peek();
System.out.println("后退至:" + prevUrl);
return prevUrl;
}
public static void main(String[] args) {
Browser browser = new Browser();
browser.visit("https://www.google.com");
browser.visit("https://www.github.com");
browser.visit("https://www.csdn.net");
browser.back(); // 后退至:https://www.github.com
browser.back(); // 后退至:https://www.google.com
browser.back(); // 已退至初始页面,无法继续后退!
}
}
3. 简单计算器(后缀表达式求值)
栈可用于处理数学表达式,这里以后缀表达式(逆波兰表达式)为例:
import java.util.Stack;
public class Calculator {
public static int evaluatePostfix(String expression) {
Stack<Integer> stack = new Stack<>();
String[] tokens = expression.split(" ");
for (String token : tokens) {
// 如果是数字,压入栈
if (token.matches("\\d+")) {
stack.push(Integer.parseInt(token));
} else {
// 如果是运算符,弹出两个数计算后压回结果
int num2 = stack.pop();
int num1 = stack.pop();
switch (token) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
default:
throw new IllegalArgumentException("无效运算符:" + token);
}
}
}
return stack.pop();
}
public static void main(String[] args) {
// 后缀表达式 "3 4 + 2 * 7 /" 等价于 ((3+4)*2)/7 = 2
String postfix = "3 4 + 2 * 7 /";
int result = evaluatePostfix(postfix);
System.out.println("后缀表达式结果:" + result); // 输出:2
}
}
五、注意事项:为什么推荐用 ArrayDeque 替代 Stack?
虽然java.util.Stack使用方便,但它继承自Vector,存在两个明显缺点:
- 性能问题:Vector 的所有方法都加了
synchronized锁,单线程场景下会有不必要的性能开销 - 设计冗余:Stack 仅需栈的核心操作,但继承自 Vector 后,暴露了
add(int index, E element)等随机访问方法,破坏了栈的封装性
因此,Java 官方推荐使用 ArrayDeque 替代 Stack,它实现了Deque接口,既可以作为栈使用,也可以作为队列使用,性能更优且 API 更纯粹:
import java.util.ArrayDeque;
import java.util.Deque;
public class ArrayDequeAsStack {
public static void main(String[] args) {
Deque<String> stack = new ArrayDeque<>(); // 压入元素(栈操作)
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println("栈顶元素:" + stack.peek()); // 输出:C
System.out.println("弹出元素:" + stack.pop()); // 输出:C
System.out.println("栈是否为空:" + stack.isEmpty()); // 输出:false
}
}
六、小结
栈作为一种基础数据结构,核心是 “后进先出” 的特性,无论是自定义实现还是使用 Java 提供的 API,都需要掌握其push、pop等核心操作。在实际开发中,栈常用于字符串处理、表达式计算、历史记录回溯等场景,而ArrayDeque是比Stack更推荐的选择。
希望这篇文章能帮助大家全面理解栈的原理和用法,如果有任何疑问或补充,欢迎在评论区留言交流~ 后续我还会分享更多 Java 集合框架的知识点,记得关注我的博客哦!



