
在数据结构的世界里,栈(Stack)是最基础也最“低调”的线性结构之一,但它却贯穿于计算机科学的方方面面——从JVM的方法调用栈到浏览器的前进后退功能,从表达式求值到括号匹配校验,栈的“后进先出”(LIFO)特性如同一条隐形的线索,支撑着无数核心场景的运转。本文将从栈的底层逻辑出发,拆解其实现原理、核心操作与实战应用,让你不仅“知其然”,更“知其所以然”。
栈是一种限定仅在表尾进行插入和删除操作的线性表,表尾被称为“栈顶(Top)”,表头被称为“栈底(Bottom)”。由于插入和删除只能在栈顶进行,最早进入栈的元素会最后被取出,形成“后进先出”(Last In First Out,LIFO)的特性。
打个通俗的比方:栈就像你往笔筒里放笔——最后放进去的笔总是最先被拿出来,而最先放进去的笔则藏在最底部,需要等上面的笔都拿完才能取出。
栈的特性可总结为“三限定一特性”:
栈的基本操作(ISO/IEC 14882:2020(Java SE 17)标准定义):
push(E e):将元素e压入栈顶(插入);pop():移除并返回栈顶元素(删除);peek():返回栈顶元素但不移除(访问);isEmpty():判断栈是否为空;size():返回栈中元素个数。很多人会把栈和队列混淆,这里用表格明确区分:
结构 | 操作特性 | 典型应用场景 |
|---|---|---|
栈(Stack) | 后进先出(LIFO) | 函数调用、括号匹配 |
队列(Queue) | 先进先出(FIFO) | 任务排队、消息队列 |
栈的实现有两种经典方式:基于数组的“数组栈”和基于链表的“链表栈”。两者各有优劣,需根据场景选择。
数组栈的核心是用数组存储元素,栈顶指针指向数组末尾的元素。由于数组长度固定,需要实现“动态扩容”机制(权威参考:《算法(第4版)》Robert Sedgewick)。
package com.ken.stack;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.util.Arrays;
/**
* 数组实现的栈(动态扩容)
* 作者:ken
*/
@Slf4j
public class ArrayStack<E> {
/**
* 存储元素的数组
*/
private Object[] elements;
/**
* 栈顶指针(初始为-1,表示空栈)
*/
private int top;
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 最大容量(避免OOM)
*/
private static final int MAX_CAPACITY = Integer.MAX_VALUE - 8;
/**
* 构造方法:初始化默认容量
*/
public ArrayStack() {
elements = new Object[DEFAULT_CAPACITY];
top = -1;
}
/**
* 构造方法:指定初始容量
* @param initialCapacity 初始容量
* @throws IllegalArgumentException 初始容量非法时抛出
*/
public ArrayStack(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("初始容量不能为负数:" + initialCapacity);
}
int capacity = initialCapacity == 0 ? DEFAULT_CAPACITY : initialCapacity;
elements = new Object[capacity];
top = -1;
}
/**
* 压入元素到栈顶
* @param e 待压入的元素
*/
public void push(E e) {
ensureCapacity(top + 2); // 检查容量,栈顶+1是新元素位置,需保证数组长度≥top+2
elements[++top] = e;
log.debug("元素{}已压入栈顶,当前栈顶指针:{}", e, top);
}
/**
* 弹出栈顶元素
* @return 栈顶元素
* @throws IllegalStateException 栈为空时抛出
*/
@SuppressWarnings("unchecked")
public E pop() {
if (isEmpty()) {
throw new IllegalStateException("栈为空,无法执行pop操作");
}
E topElement = (E) elements[top];
elements[top--] = null; // 清空引用,避免内存泄漏
log.debug("元素{}已弹出栈顶,当前栈顶指针:{}", topElement, top);
return topElement;
}
/**
* 查看栈顶元素(不移除)
* @return 栈顶元素
* @throws IllegalStateException 栈为空时抛出
*/
@SuppressWarnings("unchecked")
public E peek() {
if (isEmpty()) {
throw new IllegalStateException("栈为空,无法执行peek操作");
}
return (E) elements[top];
}
/**
* 判断栈是否为空
* @return true=空栈,false=非空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 获取栈的大小
* @return 元素个数
*/
public int size() {
return top + 1;
}
/**
* 确保数组容量足够,不足则扩容
* @param minCapacity 最小需要的容量
*/
private void ensureCapacity(int minCapacity) {
int oldCapacity = elements.length;
if (minCapacity > oldCapacity) {
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容50%
// 检查扩容后的容量是否超过最大限制,或仍不足
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_CAPACITY > 0) {
newCapacity = handleMaxCapacity(minCapacity);
}
// 数组拷贝扩容
elements = Arrays.copyOf(elements, newCapacity);
log.debug("数组栈已扩容,旧容量:{},新容量:{}", oldCapacity, newCapacity);
}
}
/**
* 处理最大容量限制
* @param minCapacity 最小需要的容量
* @return 最终容量
* @throws OutOfMemoryError 容量超过最大值时抛出
*/
private int handleMaxCapacity(int minCapacity) {
if (minCapacity < 0) {
throw new OutOfMemoryError("数组栈容量溢出");
}
return minCapacity > MAX_CAPACITY ? Integer.MAX_VALUE : MAX_CAPACITY;
}
/**
* 清空栈
*/
public void clear() {
// 遍历清空元素引用
for (int i = 0; i <= top; i++) {
elements[i] = null;
}
top = -1;
log.debug("数组栈已清空");
}
@Override
public String toString() {
if (isEmpty()) {
return "ArrayStack[]";
}
StringBuilder sb = new StringBuilder("ArrayStack[");
for (int i = 0; i <= top; i++) {
sb.append(elements[i]);
if (i != top) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
}
package com.ken.stack;
import lombok.extern.slf4j.Slf4j;
/**
* 数组栈测试类
* 作者:ken
*/
@Slf4j
public class ArrayStackTest {
public static void main(String[] args) {
ArrayStack<String> stack = new ArrayStack<>();
// 压入元素
stack.push("Java");
stack.push("Python");
stack.push("Go");
log.info("当前栈内容:{}", stack); // 输出:ArrayStack[Java, Python, Go]
log.info("栈顶元素:{}", stack.peek()); // 输出:Go
log.info("栈大小:{}", stack.size()); // 输出:3
// 弹出元素
String popElement = stack.pop();
log.info("弹出的元素:{}", popElement); // 输出:Go
log.info("弹出后栈内容:{}", stack); // 输出:ArrayStack[Java, Python]
// 清空栈
stack.clear();
log.info("清空后栈是否为空:{}", stack.isEmpty()); // 输出:true
}
}
链表栈用单向链表存储元素,栈顶指向链表的头节点(权威参考:《数据结构与算法(Java版)》邓俊辉)。由于链表是动态结构,无需扩容,但需要额外存储节点指针。
package com.ken.stack;
import lombok.extern.slf4j.Slf4j;
/**
* 单向链表实现的栈
* 作者:ken
*/
@Slf4j
public class LinkedStack<E> {
/**
* 链表节点类(static避免外部类引用导致内存泄漏)
*/
private static class Node<E> {
E item;
Node<E> next;
Node(E item, Node<E> next) {
this.item = item;
this.next = next;
}
}
/**
* 栈顶节点(初始为null,表示空栈)
*/
private Node<E> top;
/**
* 栈大小
*/
private int size;
/**
* 构造方法:初始化空栈
*/
public LinkedStack() {
top = null;
size = 0;
}
/**
* 压入元素到栈顶
* @param e 待压入的元素
*/
public void push(E e) {
// 新节点指向原栈顶,栈顶更新为新节点
top = new Node<>(e, top);
size++;
log.debug("元素{}已压入栈顶,当前栈大小:{}", e, size);
}
/**
* 弹出栈顶元素
* @return 栈顶元素
* @throws IllegalStateException 栈为空时抛出
*/
public E pop() {
if (isEmpty()) {
throw new IllegalStateException("栈为空,无法执行pop操作");
}
E topElement = top.item;
Node<E> oldTop = top;
top = top.next; // 栈顶更新为下一个节点
oldTop.item = null; // 清空引用,避免内存泄漏
oldTop.next = null;
size--;
log.debug("元素{}已弹出栈顶,当前栈大小:{}", topElement, size);
return topElement;
}
/**
* 查看栈顶元素(不移除)
* @return 栈顶元素
* @throws IllegalStateException 栈为空时抛出
*/
public E peek() {
if (isEmpty()) {
throw new IllegalStateException("栈为空,无法执行peek操作");
}
return top.item;
}
/**
* 判断栈是否为空
* @return true=空栈,false=非空
*/
public boolean isEmpty() {
return top == null;
}
/**
* 获取栈的大小
* @return 元素个数
*/
public int size() {
return size;
}
/**
* 清空栈
*/
public void clear() {
// 遍历链表,清空所有节点引用
Node<E> current = top;
while (current != null) {
Node<E> next = current.next;
current.item = null;
current.next = null;
current = next;
}
top = null;
size = 0;
log.debug("链表栈已清空");
}
@Override
public String toString() {
if (isEmpty()) {
return "LinkedStack[]";
}
StringBuilder sb = new StringBuilder("LinkedStack[");
Node<E> current = top;
while (current != null) {
sb.append(current.item);
if (current.next != null) {
sb.append(", ");
}
current = current.next;
}
sb.append("]");
return sb.toString();
}
}
package com.ken.stack;
import lombok.extern.slf4j.Slf4j;
/**
* 链表栈测试类
* 作者:ken
*/
@Slf4j
public class LinkedStackTest {
public static void main(String[] args) {
LinkedStack<Integer> stack = new LinkedStack<>();
// 压入元素
stack.push(10);
stack.push(20);
stack.push(30);
log.info("当前栈内容:{}", stack); // 输出:LinkedStack[30, 20, 10]
log.info("栈顶元素:{}", stack.peek()); // 输出:30
log.info("栈大小:{}", stack.size()); // 输出:3
// 弹出元素
Integer popElement = stack.pop();
log.info("弹出的元素:{}", popElement); // 输出:30
log.info("弹出后栈内容:{}", stack); // 输出:LinkedStack[20, 10]
// 清空栈
stack.clear();
log.info("清空后栈是否为空:{}", stack.isEmpty()); // 输出:true
}
}
操作 | 数组栈 | 链表栈 |
|---|---|---|
push | O(1)(扩容时O(n)) | O(1) |
pop | O(1) | O(1) |
peek | O(1) | O(1) |
内存占用 | 可能有空闲空间浪费 | 每个节点额外存储指针 |
访问速度 | 快(数组连续内存) | 慢(链表分散内存) |
选型建议:如果元素数量可预测、追求访问速度,选数组栈;如果元素数量动态变化大、避免空间浪费,选链表栈。
栈的“后进先出”特性使其成为解决特定问题的“利器”,以下是最经典的应用场景。
(、[、{)时压入栈;)、]、})时,弹出栈顶元素并匹配:
package com.ken.stack.application;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.util.Deque;
import java.util.Map;
import java.util.ArrayDeque;
/**
* 括号匹配校验工具类
* 作者:ken
*/
@Slf4j
public class BracketMatcher {
/**
* 右括号到左括号的映射(避免多次if判断)
*/
private static final Map<Character, Character> BRACKET_MAP = Maps.newHashMap();
static {
BRACKET_MAP.put(')', '(');
BRACKET_MAP.put(']', '[');
BRACKET_MAP.put('}', '{');
}
/**
* 校验括号是否匹配
* @param s 待校验的字符串(仅包含括号)
* @return true=匹配,false=不匹配
* @throws IllegalArgumentException 字符串为空时抛出
*/
public static boolean isValid(String s) {
if (org.springframework.util.StringUtils.hasText(s, "待校验字符串不能为空")) {
Deque<Character> stack = new ArrayDeque<>(); // 推荐用Deque代替Stack(Java官方建议)
for (char c : s.toCharArray()) {
if (BRACKET_MAP.containsValue(c)) {
// 左括号压栈
stack.push(c);
log.debug("左括号{}压入栈", c);
} else if (BRACKET_MAP.containsKey(c)) {
// 右括号匹配
if (CollectionUtils.isEmpty(stack) || !stack.pop().equals(BRACKET_MAP.get(c))) {
log.debug("括号不匹配,当前字符:{}", c);
return false;
}
log.debug("右括号{}匹配成功", c);
}
}
boolean result = stack.isEmpty();
log.debug("括号匹配校验结果:{}", result);
return result;
}
return false;
}
public static void main(String[] args) {
String validStr = "({[]})";
String invalidStr = "({[})";
log.info("字符串{}的括号匹配结果:{}", validStr, isValid(validStr)); // true
log.info("字符串{}的括号匹配结果:{}", invalidStr, isValid(invalidStr)); // false
}
}
中缀表达式(如3+4*2)是人类习惯的写法,但计算机难以直接计算,需转为后缀表达式(如3 4 2 * +)。转换规则:
package com.ken.stack.application;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.Deque;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Map;
/**
* 中缀表达式转后缀表达式工具类
* 作者:ken
*/
@Slf4j
public class InfixToPostfixConverter {
/**
* 运算符优先级:越高优先级越大
*/
private static final Map<Character, Integer> OP_PRIORITY = Maps.newHashMap();
static {
OP_PRIORITY.put('+', 1);
OP_PRIORITY.put('-', 1);
OP_PRIORITY.put('*', 2);
OP_PRIORITY.put('/', 2);
OP_PRIORITY.put('(', 0); // 左括号优先级最低
}
/**
* 中缀表达式转后缀表达式
* @param infix 中缀表达式(如"3+4*2-(5+1)")
* @return 后缀表达式(空格分隔)
* @throws IllegalArgumentException 表达式非法时抛出
*/
public static String convert(String infix) {
if (!StringUtils.hasText(infix, "中缀表达式不能为空")) {
throw new IllegalArgumentException("中缀表达式不能为空");
}
Deque<Character> opStack = new ArrayDeque<>();
StringBuilder postfix = new StringBuilder();
List<Character> operators = List.of('+', '-', '*', '/', '(', ')');
for (char c : infix.toCharArray()) {
if (Character.isDigit(c)) {
// 操作数直接输出
postfix.append(c).append(" ");
} else if (c == '(') {
// 左括号压栈
opStack.push(c);
} else if (c == ')') {
// 右括号:弹出直到左括号
while (!opStack.isEmpty() && opStack.peek() != '(') {
postfix.append(opStack.pop()).append(" ");
}
opStack.pop(); // 弹出左括号(不输出)
} else if (operators.contains(c)) {
// 运算符:弹出优先级≥当前的
while (!opStack.isEmpty() && OP_PRIORITY.get(opStack.peek()) >= OP_PRIORITY.get(c)) {
postfix.append(opStack.pop()).append(" ");
}
opStack.push(c);
}
}
// 弹出剩余运算符
while (!opStack.isEmpty()) {
postfix.append(opStack.pop()).append(" ");
}
String result = postfix.toString().trim();
log.debug("中缀表达式{}转为后缀表达式:{}", infix, result);
return result;
}
public static void main(String[] args) {
String infix = "3+4*2-(5+1)";
String postfix = convert(infix);
log.info("转换结果:{}", postfix); // 输出:3 4 2 * + 5 1 + -
}
}
package com.ken.stack.application;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.Deque;
import java.util.ArrayDeque;
/**
* 后缀表达式求值工具类
* 作者:ken
*/
@Slf4j
public class PostfixEvaluator {
/**
* 计算后缀表达式的值
* @param postfix 后缀表达式(空格分隔,如"3 4 2 * + 5 1 + -")
* @return 计算结果
* @throws IllegalArgumentException 表达式非法时抛出
*/
public static int evaluate(String postfix) {
if (!StringUtils.hasText(postfix, "后缀表达式不能为空")) {
throw new IllegalArgumentException("后缀表达式不能为空");
}
Deque<Integer> numStack = new ArrayDeque<>();
String[] tokens = postfix.split(" ");
for (String token : tokens) {
if (token.matches("\\d+")) {
// 操作数压栈
numStack.push(Integer.parseInt(token));
} else {
// 运算符计算
if (numStack.size() < 2) {
throw new IllegalArgumentException("后缀表达式格式错误:操作数不足");
}
int right = numStack.pop();
int left = numStack.pop();
int result = switch (token) {
case "+" -> left + right;
case "-" -> left - right;
case "*" -> left * right;
case "/" -> {
if (right == 0) {
throw new ArithmeticException("除数不能为0");
}
yield left / right;
}
default -> throw new IllegalArgumentException("不支持的运算符:" + token);
};
numStack.push(result);
log.debug("计算{} {} {} = {}", left, token, right, result);
}
}
if (numStack.size() != 1) {
throw new IllegalArgumentException("后缀表达式格式错误:剩余操作数");
}
int finalResult = numStack.pop();
log.debug("后缀表达式{}的计算结果:{}", postfix, finalResult);
return finalResult;
}
public static void main(String[] args) {
String postfix = "3 4 2 * + 5 1 + -";
int result = evaluate(postfix);
log.info("最终结果:{}", result); // 输出:3 + (4*2) - (5+1) = 3+8-6=5
}
}
forwardStack(前进栈)和backStack(后退栈);backStack,清空forwardStack;forwardStack,从backStack弹出页面作为当前页;backStack,从forwardStack弹出页面作为当前页。package com.ken.stack.application;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.Deque;
import java.util.ArrayDeque;
/**
* 模拟浏览器前进后退功能
* 作者:ken
*/
@Slf4j
public class BrowserHistory {
/**
* 后退栈(存储已访问的页面)
*/
private Deque<String> backStack;
/**
* 前进栈(存储可前进的页面)
*/
private Deque<String> forwardStack;
/**
* 当前页面
*/
private String currentPage;
/**
* 构造方法:初始化首页
* @param homepage 首页URL
*/
public BrowserHistory(String homepage) {
if (!StringUtils.hasText(homepage, "首页URL不能为空")) {
throw new IllegalArgumentException("首页URL不能为空");
}
backStack = new ArrayDeque<>();
forwardStack = new ArrayDeque<>();
currentPage = homepage;
log.debug("初始化浏览器,首页:{}", currentPage);
}
/**
* 访问新页面
* @param url 新页面URL
*/
public void visit(String url) {
if (!StringUtils.hasText(url, "页面URL不能为空")) {
throw new IllegalArgumentException("页面URL不能为空");
}
backStack.push(currentPage); // 当前页压入后退栈
currentPage = url;
forwardStack.clear(); // 清空前进栈
log.debug("访问新页面:{},后退栈:{}", currentPage, backStack);
}
/**
* 后退操作
* @param steps 后退步数
* @return 后退后的当前页面
*/
public String back(int steps) {
if (steps < 0) {
throw new IllegalArgumentException("后退步数不能为负数");
}
for (int i = 0; i < steps; i++) {
if (backStack.isEmpty()) {
log.debug("已到最开始页面,无法继续后退");
break;
}
forwardStack.push(currentPage); // 当前页压入前进栈
currentPage = backStack.pop();
log.debug("后退一步,当前页面:{}", currentPage);
}
return currentPage;
}
/**
* 前进操作
* @param steps 前进步数
* @return 前进后的当前页面
*/
public String forward(int steps) {
if (steps < 0) {
throw new IllegalArgumentException("前进步数不能为负数");
}
for (int i = 0; i < steps; i++) {
if (forwardStack.isEmpty()) {
log.debug("已到最新页面,无法继续前进");
break;
}
backStack.push(currentPage); // 当前页压入后退栈
currentPage = forwardStack.pop();
log.debug("前进一步,当前页面:{}", currentPage);
}
return currentPage;
}
/**
* 获取当前页面
* @return 当前页面URL
*/
public String getCurrentPage() {
return currentPage;
}
public static void main(String[] args) {
BrowserHistory browser = new BrowserHistory("https://www.ken.com");
browser.visit("https://www.ken.com/page1");
browser.visit("https://www.ken.com/page2");
log.info("当前页面:{}", browser.getCurrentPage()); // page2
browser.back(1);
log.info("后退1步后页面:{}", browser.getCurrentPage()); // page1
browser.forward(1);
log.info("前进1步后页面:{}", browser.getCurrentPage()); // page2
browser.back(2);
log.info("后退2步后页面:{}", browser.getCurrentPage()); // 首页
}
}
JVM中的“Java虚拟机栈”是栈结构的典型应用(权威参考:《Java虚拟机规范(Java SE 17版)》):
示例:递归求阶乘(栈溢出演示)
package com.ken.stack.application;
import lombok.extern.slf4j.Slf4j;
/**
* JVM方法调用栈演示
* 作者:ken
*/
@Slf4j
public class MethodCallStackDemo {
/**
* 递归求阶乘
* @param n 正整数
* @return n的阶乘
*/
public static long factorial(int n) {
if (n == 1) {
return 1;
}
log.debug("计算{}!,调用{}!,当前栈深度:{}", n, n-1, Thread.currentThread().getStackTrace().length);
return n * factorial(n - 1);
}
public static void main(String[] args) {
try {
// 当n足够大时(如10000),会抛出StackOverflowError
long result = factorial(5);
log.info("5! = {}", result); // 输出:120
} catch (StackOverflowError e) {
log.error("栈溢出:{}", e.getMessage());
}
}
}
以下是一个结合栈的实战项目:用栈存储用户操作日志,实现“撤销”(undo)和“重做”(redo)功能,并将日志持久化到MySQL。
com.ken.stack.logsystem
├── controller // 控制器
├── entity // 实体类
├── mapper // Mapper接口
├── service // 服务层
├── util // 工具类
└── LogSystemApplication.java // 启动类
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.ken</groupId>
<artifactId>stack-log-system</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<springdoc.version>2.3.0</springdoc.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- SpringDoc OpenAPI(Swagger 3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Spring Utils -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.ken.stack.logsystem.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作日志实体类
* 作者:ken
*/
@Data
@TableName("operation_log")
public class OperationLog {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 操作内容
*/
private String operationContent;
/**
* 操作时间
*/
private LocalDateTime operationTime;
}
package com.ken.stack.logsystem.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ken.stack.logsystem.entity.OperationLog;
import org.apache.ibatis.annotations.Mapper;
/**
* 操作日志Mapper
* 作者:ken
*/
@Mapper
public interface OperationLogMapper extends BaseMapper<OperationLog> {
}
package com.ken.stack.logsystem.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ken.stack.logsystem.entity.OperationLog;
import com.ken.stack.logsystem.mapper.OperationLogMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Deque;
import java.util.ArrayDeque;
/**
* 日志服务(包含undo/redo功能)
* 作者:ken
*/
@Slf4j
@Service
public class LogService extends ServiceImpl<OperationLogMapper, OperationLog> {
/**
* 操作栈(存储已执行的操作)
*/
private final Deque<OperationLog> operationStack = new ArrayDeque<>();
/**
* 重做栈(存储已撤销的操作)
*/
private final Deque<OperationLog> redoStack = new ArrayDeque<>();
/**
* 记录操作日志并压入栈
* @param userId 用户ID
* @param content 操作内容
* @return 保存的日志对象
*/
public OperationLog recordLog(Long userId, String content) {
OperationLog log = new OperationLog();
log.setUserId(userId);
log.setOperationContent(content);
log.setOperationTime(LocalDateTime.now());
// 保存到数据库
save(log);
// 压入操作栈
operationStack.push(log);
// 清空重做栈
redoStack.clear();
log.debug("记录操作日志:{},操作栈大小:{}", content, operationStack.size());
return log;
}
/**
* 撤销最后一次操作
* @return 撤销的日志对象
*/
public OperationLog undo() {
if (operationStack.isEmpty()) {
log.warn("操作栈为空,无法撤销");
return null;
}
OperationLog undoneLog = operationStack.pop();
// 压入重做栈
redoStack.push(undoneLog);
// 从数据库删除(模拟撤销)
removeById(undoneLog.getId());
log.debug("撤销操作:{},重做栈大小:{}", undoneLog.getOperationContent(), redoStack.size());
return undoneLog;
}
/**
* 重做最后一次撤销的操作
* @return 重做的日志对象
*/
public OperationLog redo() {
if (redoStack.isEmpty()) {
log.warn("重做栈为空,无法重做");
return null;
}
OperationLog redoneLog = redoStack.pop();
// 重新保存到数据库
save(redoneLog);
// 压入操作栈
operationStack.push(redoneLog);
log.debug("重做操作:{},操作栈大小:{}", redoneLog.getOperationContent(), operationStack.size());
return redoneLog;
}
/**
* 获取当前操作栈的大小
* @return 操作栈大小
*/
public int getOperationStackSize() {
return operationStack.size();
}
/**
* 获取当前重做栈的大小
* @return 重做栈大小
*/
public int getRedoStackSize() {
return redoStack.size();
}
}
package com.ken.stack.logsystem.controller;
import com.ken.stack.logsystem.entity.OperationLog;
import com.ken.stack.logsystem.service.LogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 日志控制器
* 作者:ken
*/
@Slf4j
@RestController
@RequestMapping("/api/log")
@RequiredArgsConstructor
@Tag(name = "日志管理接口", description = "包含记录、撤销、重做日志的接口")
public class LogController {
private final LogService logService;
@PostMapping("/record")
@Operation(summary = "记录操作日志", description = "记录用户操作日志并压入栈")
public OperationLog recordLog(
@Parameter(description = "用户ID", required = true) @RequestParam Long userId,
@Parameter(description = "操作内容", required = true) @RequestParam String content
) {
return logService.recordLog(userId, content);
}
@PostMapping("/undo")
@Operation(summary = "撤销操作", description = "撤销最后一次操作日志")
public OperationLog undo() {
return logService.undo();
}
@PostMapping("/redo")
@Operation(summary = "重做操作", description = "重做最后一次撤销的操作日志")
public OperationLog redo() {
return logService.redo();
}
@GetMapping("/stack/size")
@Operation(summary = "获取栈大小", description = "获取操作栈和重做栈的大小")
public String getStackSize() {
return String.format("操作栈大小:%d,重做栈大小:%d",
logService.getOperationStackSize(), logService.getRedoStackSize());
}
}
package com.ken.stack.logsystem;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 日志系统启动类
* 作者:ken
*/
@SpringBootApplication
@MapperScan("com.ken.stack.logsystem.mapper")
public class LogSystemApplication {
public static void main(String[] args) {
SpringApplication.run(LogSystemApplication.class, args);
}
}
spring:
datasource:
url: jdbc:mysql://localhost:3306/stack_log_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
springdoc:
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
api-docs:
path: /v3/api-docs
CREATE DATABASE IF NOT EXISTS stack_log_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE stack_log_db;
CREATE TABLE IF NOT EXISTS operation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
operation_content VARCHAR(255) NOT NULL COMMENT '操作内容',
operation_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
启动项目后,访问http://localhost:8080/swagger-ui.html,可测试以下接口:
POST /api/log/record:记录操作日志;POST /api/log/undo:撤销最后一次操作;POST /api/log/redo:重做最后一次撤销的操作;GET /api/log/stack/size:查看栈大小。栈作为一种“受限”的线性结构,看似功能单一,却凭借“后进先出”的特性成为解决特定问题的关键。从底层的数组/链表实现,到上层的括号匹配、表达式求值,再到实战中的日志撤销/重做、JVM方法调用,栈的应用无处不在。
掌握栈的核心逻辑,不仅能夯实数据结构基础,更能在实际开发中用“极简”的结构解决复杂问题。正如《算法(第4版)》中所说:“简单的结构往往蕴含着强大的力量”——这正是栈的魅力所在。