在Java开发中数据结构是构建高效程序的基础而栈Stack作为一种简单且核心的线性数据结构被广泛应用于表达式求值、方法调用、括号匹配等场景。很多开发者日常会使用Java内置的Stack类却未必深入理解其底层原理、实现细节以及适用场景甚至会混淆栈与其他线性结构如队列、数组的区别。本文将从栈的核心定义出发拆解其“先进后出”的核心特性对比Java中栈的两种实现方式数组实现、链表实现详解Java内置Stack类与Deque接口的使用差异结合实战案例说明栈的实际应用同时梳理常见面试考点与避坑指南帮助开发者从“会用”到“精通”Java栈结构。一、什么是栈核心特性与生活类比栈Stack是一种遵循“先进后出”LIFO, Last In First Out原则的线性数据结构它只允许在一端称为栈顶Top进行插入和删除操作另一端称为栈底Bottom固定不动。可以用生活中的场景直观理解栈的特性叠盘子只能从最上面放盘子入栈也只能从最上面拿盘子出栈先放的盘子会被压在最下面最后才能拿到。浏览器的后退功能每次访问新页面都会“入栈”点击后退按钮时最近访问的页面“出栈”回到上一个页面完全遵循“先进后出”。方法调用栈Java程序运行时方法的调用和返回本质就是一个栈操作调用方法时入栈方法执行完毕后出栈确保程序按顺序执行。1.1 栈的核心操作必记栈的操作非常简洁核心只有4个所有复杂应用都基于这4个操作展开不同实现方式的操作逻辑一致仅底层存储不同push(E e)入栈将元素e添加到栈顶栈的大小加1。若栈已满数组实现会抛出异常或进行扩容。pop()出栈移除并返回栈顶元素栈的大小减1。若栈为空会抛出异常。peek()查看栈顶元素返回栈顶元素但不移除栈的大小不变。若栈为空会抛出异常。isEmpty()判断栈是否为空返回boolean值true为空false为非空。补充说明栈的操作时间复杂度均为O(1)因为所有操作都只针对栈顶无需遍历整个栈效率极高这也是栈被广泛应用的核心原因之一。1.2 栈与其他线性结构的区别很多开发者会将栈与数组、队列混淆这里用一张表格清晰区分明确各自的核心差异数据结构核心原则操作端核心场景栈Stack先进后出LIFO仅栈顶方法调用、括号匹配、表达式求值队列Queue先进先出FIFO队首出、队尾入任务队列、消息队列数组Array随机访问任意索引快速查询、固定大小存储关键总结栈的核心优势是“操作高效”但不支持随机访问数组支持随机访问但插入删除效率低队列遵循“先进先出”与栈的原则完全相反适用场景截然不同。二、Java中栈的两种核心实现方式手写实战Java中栈的实现主要分为两种数组实现顺序栈和链表实现链式栈。两种实现各有优劣适用于不同场景我们分别手写实现理解其底层逻辑这也是面试中高频考察的知识点。2.1 数组实现栈顺序栈数组实现栈的核心思路用一个数组存储栈元素用一个变量top记录栈顶索引初始时top -1表示栈为空入栈时top加1将元素存入对应索引出栈时返回top索引的元素top减1。特点实现简单、访问速度快数组随机访问但存在扩容成本当数组满时需要创建新数组并复制元素适合元素数量固定、访问频繁的场景。import java.util.Arrays; /** * 数组实现栈顺序栈 * param E 泛型支持任意数据类型 */ public class ArrayStackE { // 存储栈元素的数组 private Object[] stack; // 栈顶索引初始为-1表示栈空 private int top; // 栈的初始容量默认10 private static final int DEFAULT_CAPACITY 10; // 无参构造初始化默认容量的栈 public ArrayStack() { stack new Object[DEFAULT_CAPACITY]; top -1; } // 有参构造指定栈的初始容量 public ArrayStack(int capacity) { if (capacity 0) { throw new IllegalArgumentException(栈容量不能小于等于0); } stack new Object[capacity]; top -1; } /** * 入栈操作 * param e 要入栈的元素 */ public void push(E e) { // 栈满进行扩容扩容为原容量的1.5倍 if (top stack.length - 1) { int newCapacity stack.length * 3 / 2; stack Arrays.copyOf(stack, newCapacity); } // 栈顶索引加1存入元素 stack[top] e; } /** * 出栈操作 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ SuppressWarnings(unchecked) public E pop() { if (isEmpty()) { throw new RuntimeException(栈为空无法执行出栈操作); } // 取出栈顶元素栈顶索引减1同时置空原栈顶元素避免内存泄漏 E topElement (E) stack[top]; stack[top--] null; return topElement; } /** * 查看栈顶元素不删除 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ SuppressWarnings(unchecked) public E peek() { if (isEmpty()) { throw new RuntimeException(栈为空无法查看栈顶元素); } return (E) stack[top]; } /** * 判断栈是否为空 * return true空false非空 */ public boolean isEmpty() { return top -1; } /** * 获取栈的大小元素个数 * return 栈的大小 */ public int size() { return top 1; } // 测试方法 public static void main(String[] args) { ArrayStackInteger stack new ArrayStack(); // 入栈 stack.push(1); stack.push(2); stack.push(3); System.out.println(栈的大小 stack.size()); // 输出3 System.out.println(栈顶元素 stack.peek()); // 输出3 // 出栈 int popElement stack.pop(); System.out.println(出栈元素 popElement); // 输出3 System.out.println(出栈后栈顶元素 stack.peek()); // 输出2 // 判断是否为空 System.out.println(栈是否为空 stack.isEmpty()); // 输出false // 清空栈 stack.pop(); stack.pop(); System.out.println(栈是否为空 stack.isEmpty()); // 输出true } }注意点1. 数组扩容时采用“原容量1.5倍”的策略平衡扩容效率和内存占用2. 出栈时需将原栈顶元素置空避免对象引用无法被GC回收导致内存泄漏3. 泛型实现时数组存储的是Object类型取出时需强制转换。2.2 链表实现栈链式栈链表实现栈的核心思路用单链表存储栈元素链表的头节点作为栈顶便于入栈、出栈操作无需记录栈顶索引入栈时将新节点作为头节点出栈时移除头节点并返回其值。特点无需扩容链表可动态增长插入删除效率高但访问速度比数组慢链表需遍历适合元素数量不确定、频繁插入删除的场景。/** * 链表实现栈链式栈 * param E 泛型支持任意数据类型 */ public class LinkedStackE { // 链表节点类内部类 private static class NodeE { E data; // 节点数据 NodeE next; // 下一个节点的引用 public Node(E data, NodeE next) { this.data data; this.next next; } } // 栈顶节点头节点初始为null表示栈空 private NodeE top; // 栈的大小元素个数 private int size; // 无参构造初始化空栈 public LinkedStack() { top null; size 0; } /** * 入栈操作新节点作为头节点栈顶 * param e 要入栈的元素 */ public void push(E e) { // 新节点的next指向原栈顶再将top指向新节点 top new Node(e, top); size; } /** * 出栈操作移除头节点栈顶返回其数据 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ public E pop() { if (isEmpty()) { throw new RuntimeException(栈为空无法执行出栈操作); } // 取出栈顶节点的数据 E topData top.data; // 将top指向原栈顶的下一个节点移除原栈顶节点 top top.next; size--; return topData; } /** * 查看栈顶元素不删除 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ public E peek() { if (isEmpty()) { throw new RuntimeException(栈为空无法查看栈顶元素); } return top.data; } /** * 判断栈是否为空 * return true空false非空 */ public boolean isEmpty() { return size 0; } /** * 获取栈的大小元素个数 * return 栈的大小 */ public int size() { return size; } // 测试方法 public static void main(String[] args) { LinkedStackString stack new LinkedStack(); // 入栈 stack.push(Java); stack.push(数据结构); stack.push(栈); System.out.println(栈的大小 stack.size()); // 输出3 System.out.println(栈顶元素 stack.peek()); // 输出栈 // 出栈 String popElement stack.pop(); System.out.println(出栈元素 popElement); // 输出栈 System.out.println(出栈后栈顶元素 stack.peek()); // 输出数据结构 // 判断是否为空 System.out.println(栈是否为空 stack.isEmpty()); // 输出false // 清空栈 stack.pop(); stack.pop(); System.out.println(栈是否为空 stack.isEmpty()); // 输出true } }2.3 两种实现方式对比选型指南实现方式优点缺点适用场景数组栈顺序栈实现简单、访问速度快O(1)、内存连续需扩容、扩容时有性能损耗、固定容量初始值元素数量固定、访问频繁、对性能要求高链表栈链式栈动态扩容、插入删除高效、无内存浪费访问速度慢需遍历、每个节点有额外内存开销元素数量不确定、频繁插入删除三、Java内置栈Stack类与Deque接口实战常用日常开发中我们很少手动实现栈而是使用Java提供的内置工具类。但很多开发者只知道java.util.Stack类却不知道它存在设计缺陷更推荐使用Deque接口双端队列来实现栈的功能这也是阿里Java开发手册中的推荐规范。3.1 不推荐使用java.util.Stack类Stack类是Java早期提供的栈实现继承自Vector类线程安全类底层基于数组实现提供了push()、pop()、peek()等核心方法看似满足栈的需求但存在两个致命缺陷继承Vector暴露了非栈操作Vector类提供了add(int index, E e)、remove(int index)等方法这些方法可以操作栈的任意位置破坏了栈“只能操作栈顶”的核心原则可能导致数据混乱。线程安全带来的性能损耗Vector类的所有方法都加了synchronized锁线程安全但性能较低大部分场景下栈不需要线程安全使用Stack类会造成不必要的性能浪费。示例Stack类的使用不推荐import java.util.Stack; public class StackDemo { public static void main(String[] args) { StackInteger stack new Stack(); // 入栈 stack.push(1); stack.push(2); // 查看栈顶 System.out.println(stack.peek()); // 输出2 // 出栈 System.out.println(stack.pop()); // 输出2 // 破坏栈原则插入元素到指定位置非栈顶 stack.add(0, 3); // 不推荐违背栈的特性 } }3.2 推荐使用Deque接口优先选LinkedListDequeDouble Ended Queue双端队列是Java集合框架中的接口支持在两端插入和删除元素既可以实现队列先进先出也可以实现栈先进后出。阿里Java开发手册明确推荐优先使用Deque接口及其实现类LinkedList、ArrayDeque替代Stack类。用Deque实现栈的核心约定入栈使用push(E e)方法等价于addFirst(E e)将元素添加到双端队列的头部栈顶。出栈使用pop()方法等价于removeFirst()移除并返回双端队列的头部元素栈顶。查看栈顶使用peek()方法等价于peekFirst()返回双端队列的头部元素栈顶。常用实现类对比LinkedList基于链表实现适合元素数量不确定、频繁插入删除的场景对应我们手写的链式栈。ArrayDeque基于数组实现适合元素数量固定、访问频繁的场景对应我们手写的顺序栈性能比LinkedList更优是实现栈的首选。示例Deque实现栈推荐import java.util.Deque; import java.util.LinkedList; public class DequeStackDemo { public static void main(String[] args) { // 推荐使用LinkedList或ArrayDeque实现栈 DequeInteger stack new LinkedList(); // 入栈push方法 stack.push(1); stack.push(2); stack.push(3); System.out.println(栈的大小 stack.size()); // 输出3 System.out.println(栈顶元素 stack.peek()); // 输出3 // 出栈pop方法 int popElement stack.pop(); System.out.println(出栈元素 popElement); // 输出3 System.out.println(出栈后栈顶元素 stack.peek()); // 输出2 // 判断是否为空 System.out.println(栈是否为空 stack.isEmpty()); // 输出false // 注意Deque也可以操作两端但我们约定只操作头部栈顶避免破坏栈特性 // 不推荐使用addLast()、removeLast()等方法这是队列的操作 } }核心推荐日常开发中优先使用ArrayDeque实现栈性能最优其次使用LinkedList坚决避免使用Stack类避免破坏栈的特性和不必要的性能损耗。四、栈的实战应用场景面试高频栈的“先进后出”特性决定了它在很多场景中有着不可替代的作用以下是Java开发中最常见的4个实战场景结合代码示例说明帮助你快速将栈应用到实际开发中。4.1 场景1括号匹配经典面试题需求判断一个字符串中的括号()、[]、{}是否匹配比如“{[()]}”匹配“{[)]}”不匹配“((()))”匹配。思路使用栈存储左括号遍历字符串遇到左括号则入栈遇到右括号则出栈判断出栈的左括号是否与当前右括号匹配遍历结束后若栈为空且所有括号都匹配则返回true否则返回false。import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.ArrayDeque; /** * 栈的应用括号匹配 */ public class BracketMatching { public static boolean isBracketMatching(String str) { if (str null || str.isEmpty()) { return true; } // 存储括号映射关系右括号 - 左括号 MapCharacter, Character bracketMap new HashMap(); bracketMap.put(), (); bracketMap.put(], [); bracketMap.put(}, {); // 使用ArrayDeque实现栈存储左括号 DequeCharacter stack new ArrayDeque(); for (char c : str.toCharArray()) { if (bracketMap.containsValue(c)) { // 遇到左括号入栈 stack.push(c); } else if (bracketMap.containsKey(c)) { // 遇到右括号判断栈是否为空且出栈的左括号是否匹配 if (stack.isEmpty() || stack.pop() ! bracketMap.get(c)) { return false; } } // 非括号字符忽略 } // 遍历结束后栈为空则所有括号匹配 return stack.isEmpty(); } public static void main(String[] args) { System.out.println(isBracketMatching({[()]})); // true System.out.println(isBracketMatching({[)]})); // false System.out.println(isBracketMatching(((())))); // true System.out.println(isBracketMatching((())); // false } }4.2 场景2表达式求值后缀表达式需求计算后缀表达式逆波兰表达式的值比如“3 4 5 *”计算过程为(34)*535“5 3 - 2 *”计算过程为(5-3)*24。思路后缀表达式的特点是“操作数在前运算符在后”使用栈存储操作数遍历表达式遇到数字则入栈遇到运算符则出栈两个操作数进行运算后将结果入栈遍历结束后栈中剩余的元素即为表达式的结果。import java.util.Deque; import java.util.ArrayDeque; /** * 栈的应用后缀表达式求值 */ public class SuffixExpression { public static int calculateSuffixExpression(String[] expression) { if (expression null || expression.length 0) { throw new IllegalArgumentException(表达式不能为空); } DequeInteger stack new ArrayDeque(); for (String str : expression) { if (isNumber(str)) { // 遇到数字入栈 stack.push(Integer.parseInt(str)); } else { // 遇到运算符出栈两个操作数进行运算 if (stack.size() 2) { throw new IllegalArgumentException(表达式格式错误); } int num2 stack.pop(); // 第二个操作数后出栈 int num1 stack.pop(); // 第一个操作数先出栈 int result calculate(num1, num2, str); stack.push(result); } } // 遍历结束后栈中应只有一个元素结果 if (stack.size() ! 1) { throw new IllegalArgumentException(表达式格式错误); } return stack.pop(); } // 判断字符串是否为数字 private static boolean isNumber(String str) { try { Integer.parseInt(str); return true; } catch (NumberFormatException e) { return false; } } // 执行运算 private static int calculate(int num1, int num2, String operator) { return switch (operator) { case - num1 num2; case - - num1 - num2; case * - num1 * num2; case / - { if (num2 0) { throw new ArithmeticException(除数不能为0); } yield num1 / num2; } default - throw new IllegalArgumentException(不支持的运算符 operator); }; } public static void main(String[] args) { String[] expression1 {3, 4, , 5, *}; System.out.println(calculateSuffixExpression(expression1)); // 35 String[] expression2 {5, 3, -, 2, *}; System.out.println(calculateSuffixExpression(expression2)); // 4 } }4.3 场景3方法调用栈JVM底层应用Java程序运行时JVM会维护一个“方法调用栈”本质就是一个栈结构用于记录方法的调用顺序和执行状态当调用一个方法时JVM会创建一个“栈帧”存储方法的参数、局部变量、返回地址等将栈帧入栈。当方法执行完毕后栈帧出栈程序回到上一个方法的执行位置继续执行。如果方法调用层级过深如递归无终止条件会导致栈帧过多超出栈的容量抛出StackOverflowError栈溢出。示例方法调用栈演示/** * 方法调用栈演示本质是栈的“先进后出” */ public class MethodCallStack { public static void main(String[] args) { System.out.println(main方法开始执行); methodA(); System.out.println(main方法执行完毕); } public static void methodA() { System.out.println(methodA开始执行); methodB(); System.out.println(methodA执行完毕); } public static void methodB() { System.out.println(methodB开始执行); // 模拟方法调用栈深度若递归调用会导致栈溢出 System.out.println(methodB执行完毕); } }执行结果main方法开始执行 methodA开始执行 methodB开始执行 methodB执行完毕 methodA执行完毕 main方法执行完毕执行流程对应栈操作main入栈 → methodA入栈 → methodB入栈 → methodB出栈 → methodA出栈 → main出栈完全遵循“先进后出”原则。4.4 场景4浏览器后退/编辑器撤销生活场景落地浏览器的后退功能、编辑器的撤销功能核心都是用栈实现的浏览器每访问一个新页面就将当前页面地址入栈点击后退就将栈顶的页面地址出栈加载该页面。编辑器每输入一个字符/操作就将当前状态入栈点击撤销就将栈顶的状态出栈恢复到上一个状态。示例简单模拟浏览器后退功能import java.util.Deque; import java.util.ArrayDeque; /** * 栈的应用模拟浏览器后退功能 */ public class BrowserBack { // 用栈存储访问过的页面地址 private DequeString pageStack; // 当前页面 private String currentPage; public BrowserBack() { pageStack new ArrayDeque(); currentPage null; } // 访问新页面 public void visitPage(String page) { if (currentPage ! null) { // 将当前页面入栈再访问新页面 pageStack.push(currentPage); } currentPage page; System.out.println(当前访问页面 currentPage); } // 后退到上一个页面 public void back() { if (pageStack.isEmpty()) { System.out.println(无法后退已到达第一个页面); return; } // 栈顶页面出栈作为当前页面 currentPage pageStack.pop(); System.out.println(后退到页面 currentPage); } public static void main(String[] args) { BrowserBack browser new BrowserBack(); browser.visitPage(首页); browser.visitPage(Java博客); browser.visitPage(栈的实现); browser.back(); // 后退到Java博客 browser.back(); // 后退到首页 browser.back(); // 无法后退 } }五、核心考点与常见误区面试避坑5.1 面试高频考点栈的核心特性先进后出LIFO核心操作及时间复杂度O(1)。栈的两种实现方式数组、链表各自的优缺点及选型场景。Java中Stack类的缺陷为什么推荐用Deque替代Stack栈的实战应用括号匹配、表达式求值、方法调用栈手写代码。栈溢出StackOverflowError的原因及解决方案如递归深度控制、调整JVM栈大小。5.2 常见误区避坑指南误区1Java的Stack类是最优选择。→ 错误Stack类继承Vector暴露非栈操作、线程安全损耗性能推荐用DequeArrayDeque/LinkedList。误区2栈的操作时间复杂度是O(n)。→ 错误栈的push、pop、peek操作都只针对栈顶时间复杂度均为O(1)无需遍历。误区3链表实现的栈比数组实现的栈性能更好。→ 错误需分场景访问频繁选数组栈插入删除频繁选链表栈ArrayDeque的性能通常优于LinkedList。误区4栈只能存储基本数据类型。→ 错误栈可以存储任意数据类型通过泛型实现包括对象、字符串等。六、总结与进阶方向栈作为Java中最基础、最常用的线性数据结构核心优势是“操作高效、逻辑简单”其“先进后出”的特性使其在方法调用、括号匹配、表达式求值等场景中不可或缺。本文从原理、实现、实战三个维度完整覆盖了Java栈的核心知识点重点强调了“手写实现栈”和“用Deque替代Stack”两个核心要点既适合新手入门也适合开发者巩固基础、应对面试。对于有进阶需求的开发者可进一步学习以下内容栈的高级应用单调栈解决滑动窗口、接雨水等算法题这是面试中高频考察的进阶知识点。JVM方法调用栈的底层实现深入理解栈帧的结构、栈溢出的底层原因结合JVM调优解决栈溢出问题。并发场景下的栈如何实现线程安全的栈如使用ConcurrentLinkedDeque或手动加锁。最后数据结构的学习离不开动手实践建议大家亲手实现数组栈和链表栈结合本文的实战场景编写代码真正理解栈的核心逻辑。栈虽然简单但却是后续学习更复杂数据结构如树、图的基础打好栈的基础能让你在Java开发和算法学习中更得心应手
深入理解Java数据结构栈:原理、实现与实战应用
在Java开发中数据结构是构建高效程序的基础而栈Stack作为一种简单且核心的线性数据结构被广泛应用于表达式求值、方法调用、括号匹配等场景。很多开发者日常会使用Java内置的Stack类却未必深入理解其底层原理、实现细节以及适用场景甚至会混淆栈与其他线性结构如队列、数组的区别。本文将从栈的核心定义出发拆解其“先进后出”的核心特性对比Java中栈的两种实现方式数组实现、链表实现详解Java内置Stack类与Deque接口的使用差异结合实战案例说明栈的实际应用同时梳理常见面试考点与避坑指南帮助开发者从“会用”到“精通”Java栈结构。一、什么是栈核心特性与生活类比栈Stack是一种遵循“先进后出”LIFO, Last In First Out原则的线性数据结构它只允许在一端称为栈顶Top进行插入和删除操作另一端称为栈底Bottom固定不动。可以用生活中的场景直观理解栈的特性叠盘子只能从最上面放盘子入栈也只能从最上面拿盘子出栈先放的盘子会被压在最下面最后才能拿到。浏览器的后退功能每次访问新页面都会“入栈”点击后退按钮时最近访问的页面“出栈”回到上一个页面完全遵循“先进后出”。方法调用栈Java程序运行时方法的调用和返回本质就是一个栈操作调用方法时入栈方法执行完毕后出栈确保程序按顺序执行。1.1 栈的核心操作必记栈的操作非常简洁核心只有4个所有复杂应用都基于这4个操作展开不同实现方式的操作逻辑一致仅底层存储不同push(E e)入栈将元素e添加到栈顶栈的大小加1。若栈已满数组实现会抛出异常或进行扩容。pop()出栈移除并返回栈顶元素栈的大小减1。若栈为空会抛出异常。peek()查看栈顶元素返回栈顶元素但不移除栈的大小不变。若栈为空会抛出异常。isEmpty()判断栈是否为空返回boolean值true为空false为非空。补充说明栈的操作时间复杂度均为O(1)因为所有操作都只针对栈顶无需遍历整个栈效率极高这也是栈被广泛应用的核心原因之一。1.2 栈与其他线性结构的区别很多开发者会将栈与数组、队列混淆这里用一张表格清晰区分明确各自的核心差异数据结构核心原则操作端核心场景栈Stack先进后出LIFO仅栈顶方法调用、括号匹配、表达式求值队列Queue先进先出FIFO队首出、队尾入任务队列、消息队列数组Array随机访问任意索引快速查询、固定大小存储关键总结栈的核心优势是“操作高效”但不支持随机访问数组支持随机访问但插入删除效率低队列遵循“先进先出”与栈的原则完全相反适用场景截然不同。二、Java中栈的两种核心实现方式手写实战Java中栈的实现主要分为两种数组实现顺序栈和链表实现链式栈。两种实现各有优劣适用于不同场景我们分别手写实现理解其底层逻辑这也是面试中高频考察的知识点。2.1 数组实现栈顺序栈数组实现栈的核心思路用一个数组存储栈元素用一个变量top记录栈顶索引初始时top -1表示栈为空入栈时top加1将元素存入对应索引出栈时返回top索引的元素top减1。特点实现简单、访问速度快数组随机访问但存在扩容成本当数组满时需要创建新数组并复制元素适合元素数量固定、访问频繁的场景。import java.util.Arrays; /** * 数组实现栈顺序栈 * param E 泛型支持任意数据类型 */ public class ArrayStackE { // 存储栈元素的数组 private Object[] stack; // 栈顶索引初始为-1表示栈空 private int top; // 栈的初始容量默认10 private static final int DEFAULT_CAPACITY 10; // 无参构造初始化默认容量的栈 public ArrayStack() { stack new Object[DEFAULT_CAPACITY]; top -1; } // 有参构造指定栈的初始容量 public ArrayStack(int capacity) { if (capacity 0) { throw new IllegalArgumentException(栈容量不能小于等于0); } stack new Object[capacity]; top -1; } /** * 入栈操作 * param e 要入栈的元素 */ public void push(E e) { // 栈满进行扩容扩容为原容量的1.5倍 if (top stack.length - 1) { int newCapacity stack.length * 3 / 2; stack Arrays.copyOf(stack, newCapacity); } // 栈顶索引加1存入元素 stack[top] e; } /** * 出栈操作 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ SuppressWarnings(unchecked) public E pop() { if (isEmpty()) { throw new RuntimeException(栈为空无法执行出栈操作); } // 取出栈顶元素栈顶索引减1同时置空原栈顶元素避免内存泄漏 E topElement (E) stack[top]; stack[top--] null; return topElement; } /** * 查看栈顶元素不删除 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ SuppressWarnings(unchecked) public E peek() { if (isEmpty()) { throw new RuntimeException(栈为空无法查看栈顶元素); } return (E) stack[top]; } /** * 判断栈是否为空 * return true空false非空 */ public boolean isEmpty() { return top -1; } /** * 获取栈的大小元素个数 * return 栈的大小 */ public int size() { return top 1; } // 测试方法 public static void main(String[] args) { ArrayStackInteger stack new ArrayStack(); // 入栈 stack.push(1); stack.push(2); stack.push(3); System.out.println(栈的大小 stack.size()); // 输出3 System.out.println(栈顶元素 stack.peek()); // 输出3 // 出栈 int popElement stack.pop(); System.out.println(出栈元素 popElement); // 输出3 System.out.println(出栈后栈顶元素 stack.peek()); // 输出2 // 判断是否为空 System.out.println(栈是否为空 stack.isEmpty()); // 输出false // 清空栈 stack.pop(); stack.pop(); System.out.println(栈是否为空 stack.isEmpty()); // 输出true } }注意点1. 数组扩容时采用“原容量1.5倍”的策略平衡扩容效率和内存占用2. 出栈时需将原栈顶元素置空避免对象引用无法被GC回收导致内存泄漏3. 泛型实现时数组存储的是Object类型取出时需强制转换。2.2 链表实现栈链式栈链表实现栈的核心思路用单链表存储栈元素链表的头节点作为栈顶便于入栈、出栈操作无需记录栈顶索引入栈时将新节点作为头节点出栈时移除头节点并返回其值。特点无需扩容链表可动态增长插入删除效率高但访问速度比数组慢链表需遍历适合元素数量不确定、频繁插入删除的场景。/** * 链表实现栈链式栈 * param E 泛型支持任意数据类型 */ public class LinkedStackE { // 链表节点类内部类 private static class NodeE { E data; // 节点数据 NodeE next; // 下一个节点的引用 public Node(E data, NodeE next) { this.data data; this.next next; } } // 栈顶节点头节点初始为null表示栈空 private NodeE top; // 栈的大小元素个数 private int size; // 无参构造初始化空栈 public LinkedStack() { top null; size 0; } /** * 入栈操作新节点作为头节点栈顶 * param e 要入栈的元素 */ public void push(E e) { // 新节点的next指向原栈顶再将top指向新节点 top new Node(e, top); size; } /** * 出栈操作移除头节点栈顶返回其数据 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ public E pop() { if (isEmpty()) { throw new RuntimeException(栈为空无法执行出栈操作); } // 取出栈顶节点的数据 E topData top.data; // 将top指向原栈顶的下一个节点移除原栈顶节点 top top.next; size--; return topData; } /** * 查看栈顶元素不删除 * return 栈顶元素 * throws RuntimeException 栈空时抛出异常 */ public E peek() { if (isEmpty()) { throw new RuntimeException(栈为空无法查看栈顶元素); } return top.data; } /** * 判断栈是否为空 * return true空false非空 */ public boolean isEmpty() { return size 0; } /** * 获取栈的大小元素个数 * return 栈的大小 */ public int size() { return size; } // 测试方法 public static void main(String[] args) { LinkedStackString stack new LinkedStack(); // 入栈 stack.push(Java); stack.push(数据结构); stack.push(栈); System.out.println(栈的大小 stack.size()); // 输出3 System.out.println(栈顶元素 stack.peek()); // 输出栈 // 出栈 String popElement stack.pop(); System.out.println(出栈元素 popElement); // 输出栈 System.out.println(出栈后栈顶元素 stack.peek()); // 输出数据结构 // 判断是否为空 System.out.println(栈是否为空 stack.isEmpty()); // 输出false // 清空栈 stack.pop(); stack.pop(); System.out.println(栈是否为空 stack.isEmpty()); // 输出true } }2.3 两种实现方式对比选型指南实现方式优点缺点适用场景数组栈顺序栈实现简单、访问速度快O(1)、内存连续需扩容、扩容时有性能损耗、固定容量初始值元素数量固定、访问频繁、对性能要求高链表栈链式栈动态扩容、插入删除高效、无内存浪费访问速度慢需遍历、每个节点有额外内存开销元素数量不确定、频繁插入删除三、Java内置栈Stack类与Deque接口实战常用日常开发中我们很少手动实现栈而是使用Java提供的内置工具类。但很多开发者只知道java.util.Stack类却不知道它存在设计缺陷更推荐使用Deque接口双端队列来实现栈的功能这也是阿里Java开发手册中的推荐规范。3.1 不推荐使用java.util.Stack类Stack类是Java早期提供的栈实现继承自Vector类线程安全类底层基于数组实现提供了push()、pop()、peek()等核心方法看似满足栈的需求但存在两个致命缺陷继承Vector暴露了非栈操作Vector类提供了add(int index, E e)、remove(int index)等方法这些方法可以操作栈的任意位置破坏了栈“只能操作栈顶”的核心原则可能导致数据混乱。线程安全带来的性能损耗Vector类的所有方法都加了synchronized锁线程安全但性能较低大部分场景下栈不需要线程安全使用Stack类会造成不必要的性能浪费。示例Stack类的使用不推荐import java.util.Stack; public class StackDemo { public static void main(String[] args) { StackInteger stack new Stack(); // 入栈 stack.push(1); stack.push(2); // 查看栈顶 System.out.println(stack.peek()); // 输出2 // 出栈 System.out.println(stack.pop()); // 输出2 // 破坏栈原则插入元素到指定位置非栈顶 stack.add(0, 3); // 不推荐违背栈的特性 } }3.2 推荐使用Deque接口优先选LinkedListDequeDouble Ended Queue双端队列是Java集合框架中的接口支持在两端插入和删除元素既可以实现队列先进先出也可以实现栈先进后出。阿里Java开发手册明确推荐优先使用Deque接口及其实现类LinkedList、ArrayDeque替代Stack类。用Deque实现栈的核心约定入栈使用push(E e)方法等价于addFirst(E e)将元素添加到双端队列的头部栈顶。出栈使用pop()方法等价于removeFirst()移除并返回双端队列的头部元素栈顶。查看栈顶使用peek()方法等价于peekFirst()返回双端队列的头部元素栈顶。常用实现类对比LinkedList基于链表实现适合元素数量不确定、频繁插入删除的场景对应我们手写的链式栈。ArrayDeque基于数组实现适合元素数量固定、访问频繁的场景对应我们手写的顺序栈性能比LinkedList更优是实现栈的首选。示例Deque实现栈推荐import java.util.Deque; import java.util.LinkedList; public class DequeStackDemo { public static void main(String[] args) { // 推荐使用LinkedList或ArrayDeque实现栈 DequeInteger stack new LinkedList(); // 入栈push方法 stack.push(1); stack.push(2); stack.push(3); System.out.println(栈的大小 stack.size()); // 输出3 System.out.println(栈顶元素 stack.peek()); // 输出3 // 出栈pop方法 int popElement stack.pop(); System.out.println(出栈元素 popElement); // 输出3 System.out.println(出栈后栈顶元素 stack.peek()); // 输出2 // 判断是否为空 System.out.println(栈是否为空 stack.isEmpty()); // 输出false // 注意Deque也可以操作两端但我们约定只操作头部栈顶避免破坏栈特性 // 不推荐使用addLast()、removeLast()等方法这是队列的操作 } }核心推荐日常开发中优先使用ArrayDeque实现栈性能最优其次使用LinkedList坚决避免使用Stack类避免破坏栈的特性和不必要的性能损耗。四、栈的实战应用场景面试高频栈的“先进后出”特性决定了它在很多场景中有着不可替代的作用以下是Java开发中最常见的4个实战场景结合代码示例说明帮助你快速将栈应用到实际开发中。4.1 场景1括号匹配经典面试题需求判断一个字符串中的括号()、[]、{}是否匹配比如“{[()]}”匹配“{[)]}”不匹配“((()))”匹配。思路使用栈存储左括号遍历字符串遇到左括号则入栈遇到右括号则出栈判断出栈的左括号是否与当前右括号匹配遍历结束后若栈为空且所有括号都匹配则返回true否则返回false。import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.ArrayDeque; /** * 栈的应用括号匹配 */ public class BracketMatching { public static boolean isBracketMatching(String str) { if (str null || str.isEmpty()) { return true; } // 存储括号映射关系右括号 - 左括号 MapCharacter, Character bracketMap new HashMap(); bracketMap.put(), (); bracketMap.put(], [); bracketMap.put(}, {); // 使用ArrayDeque实现栈存储左括号 DequeCharacter stack new ArrayDeque(); for (char c : str.toCharArray()) { if (bracketMap.containsValue(c)) { // 遇到左括号入栈 stack.push(c); } else if (bracketMap.containsKey(c)) { // 遇到右括号判断栈是否为空且出栈的左括号是否匹配 if (stack.isEmpty() || stack.pop() ! bracketMap.get(c)) { return false; } } // 非括号字符忽略 } // 遍历结束后栈为空则所有括号匹配 return stack.isEmpty(); } public static void main(String[] args) { System.out.println(isBracketMatching({[()]})); // true System.out.println(isBracketMatching({[)]})); // false System.out.println(isBracketMatching(((())))); // true System.out.println(isBracketMatching((())); // false } }4.2 场景2表达式求值后缀表达式需求计算后缀表达式逆波兰表达式的值比如“3 4 5 *”计算过程为(34)*535“5 3 - 2 *”计算过程为(5-3)*24。思路后缀表达式的特点是“操作数在前运算符在后”使用栈存储操作数遍历表达式遇到数字则入栈遇到运算符则出栈两个操作数进行运算后将结果入栈遍历结束后栈中剩余的元素即为表达式的结果。import java.util.Deque; import java.util.ArrayDeque; /** * 栈的应用后缀表达式求值 */ public class SuffixExpression { public static int calculateSuffixExpression(String[] expression) { if (expression null || expression.length 0) { throw new IllegalArgumentException(表达式不能为空); } DequeInteger stack new ArrayDeque(); for (String str : expression) { if (isNumber(str)) { // 遇到数字入栈 stack.push(Integer.parseInt(str)); } else { // 遇到运算符出栈两个操作数进行运算 if (stack.size() 2) { throw new IllegalArgumentException(表达式格式错误); } int num2 stack.pop(); // 第二个操作数后出栈 int num1 stack.pop(); // 第一个操作数先出栈 int result calculate(num1, num2, str); stack.push(result); } } // 遍历结束后栈中应只有一个元素结果 if (stack.size() ! 1) { throw new IllegalArgumentException(表达式格式错误); } return stack.pop(); } // 判断字符串是否为数字 private static boolean isNumber(String str) { try { Integer.parseInt(str); return true; } catch (NumberFormatException e) { return false; } } // 执行运算 private static int calculate(int num1, int num2, String operator) { return switch (operator) { case - num1 num2; case - - num1 - num2; case * - num1 * num2; case / - { if (num2 0) { throw new ArithmeticException(除数不能为0); } yield num1 / num2; } default - throw new IllegalArgumentException(不支持的运算符 operator); }; } public static void main(String[] args) { String[] expression1 {3, 4, , 5, *}; System.out.println(calculateSuffixExpression(expression1)); // 35 String[] expression2 {5, 3, -, 2, *}; System.out.println(calculateSuffixExpression(expression2)); // 4 } }4.3 场景3方法调用栈JVM底层应用Java程序运行时JVM会维护一个“方法调用栈”本质就是一个栈结构用于记录方法的调用顺序和执行状态当调用一个方法时JVM会创建一个“栈帧”存储方法的参数、局部变量、返回地址等将栈帧入栈。当方法执行完毕后栈帧出栈程序回到上一个方法的执行位置继续执行。如果方法调用层级过深如递归无终止条件会导致栈帧过多超出栈的容量抛出StackOverflowError栈溢出。示例方法调用栈演示/** * 方法调用栈演示本质是栈的“先进后出” */ public class MethodCallStack { public static void main(String[] args) { System.out.println(main方法开始执行); methodA(); System.out.println(main方法执行完毕); } public static void methodA() { System.out.println(methodA开始执行); methodB(); System.out.println(methodA执行完毕); } public static void methodB() { System.out.println(methodB开始执行); // 模拟方法调用栈深度若递归调用会导致栈溢出 System.out.println(methodB执行完毕); } }执行结果main方法开始执行 methodA开始执行 methodB开始执行 methodB执行完毕 methodA执行完毕 main方法执行完毕执行流程对应栈操作main入栈 → methodA入栈 → methodB入栈 → methodB出栈 → methodA出栈 → main出栈完全遵循“先进后出”原则。4.4 场景4浏览器后退/编辑器撤销生活场景落地浏览器的后退功能、编辑器的撤销功能核心都是用栈实现的浏览器每访问一个新页面就将当前页面地址入栈点击后退就将栈顶的页面地址出栈加载该页面。编辑器每输入一个字符/操作就将当前状态入栈点击撤销就将栈顶的状态出栈恢复到上一个状态。示例简单模拟浏览器后退功能import java.util.Deque; import java.util.ArrayDeque; /** * 栈的应用模拟浏览器后退功能 */ public class BrowserBack { // 用栈存储访问过的页面地址 private DequeString pageStack; // 当前页面 private String currentPage; public BrowserBack() { pageStack new ArrayDeque(); currentPage null; } // 访问新页面 public void visitPage(String page) { if (currentPage ! null) { // 将当前页面入栈再访问新页面 pageStack.push(currentPage); } currentPage page; System.out.println(当前访问页面 currentPage); } // 后退到上一个页面 public void back() { if (pageStack.isEmpty()) { System.out.println(无法后退已到达第一个页面); return; } // 栈顶页面出栈作为当前页面 currentPage pageStack.pop(); System.out.println(后退到页面 currentPage); } public static void main(String[] args) { BrowserBack browser new BrowserBack(); browser.visitPage(首页); browser.visitPage(Java博客); browser.visitPage(栈的实现); browser.back(); // 后退到Java博客 browser.back(); // 后退到首页 browser.back(); // 无法后退 } }五、核心考点与常见误区面试避坑5.1 面试高频考点栈的核心特性先进后出LIFO核心操作及时间复杂度O(1)。栈的两种实现方式数组、链表各自的优缺点及选型场景。Java中Stack类的缺陷为什么推荐用Deque替代Stack栈的实战应用括号匹配、表达式求值、方法调用栈手写代码。栈溢出StackOverflowError的原因及解决方案如递归深度控制、调整JVM栈大小。5.2 常见误区避坑指南误区1Java的Stack类是最优选择。→ 错误Stack类继承Vector暴露非栈操作、线程安全损耗性能推荐用DequeArrayDeque/LinkedList。误区2栈的操作时间复杂度是O(n)。→ 错误栈的push、pop、peek操作都只针对栈顶时间复杂度均为O(1)无需遍历。误区3链表实现的栈比数组实现的栈性能更好。→ 错误需分场景访问频繁选数组栈插入删除频繁选链表栈ArrayDeque的性能通常优于LinkedList。误区4栈只能存储基本数据类型。→ 错误栈可以存储任意数据类型通过泛型实现包括对象、字符串等。六、总结与进阶方向栈作为Java中最基础、最常用的线性数据结构核心优势是“操作高效、逻辑简单”其“先进后出”的特性使其在方法调用、括号匹配、表达式求值等场景中不可或缺。本文从原理、实现、实战三个维度完整覆盖了Java栈的核心知识点重点强调了“手写实现栈”和“用Deque替代Stack”两个核心要点既适合新手入门也适合开发者巩固基础、应对面试。对于有进阶需求的开发者可进一步学习以下内容栈的高级应用单调栈解决滑动窗口、接雨水等算法题这是面试中高频考察的进阶知识点。JVM方法调用栈的底层实现深入理解栈帧的结构、栈溢出的底层原因结合JVM调优解决栈溢出问题。并发场景下的栈如何实现线程安全的栈如使用ConcurrentLinkedDeque或手动加锁。最后数据结构的学习离不开动手实践建议大家亲手实现数组栈和链表栈结合本文的实战场景编写代码真正理解栈的核心逻辑。栈虽然简单但却是后续学习更复杂数据结构如树、图的基础打好栈的基础能让你在Java开发和算法学习中更得心应手