Java 栈(Stack)

目录

  1. 栈的核心基础概念
  2. Java 中栈的两种主流实现方式
  3. 手写实现栈:吃透底层原理
  4. 栈的经典实战案例(面试高频)
  5. 面试高频考点&避坑指南
  6. 总结

一、栈的核心基础概念

1.1 什么是栈?

栈(Stack)是一种操作受限的线性数据结构,核心特性是后进先出(LIFO,Last In First Out),也就是最后存入的元素,会被最先取出。

我们可以用生活中最常见的场景类比:

  • 叠盘子:我们只能从最顶部放盘子,也只能从最顶部取盘子,最后放的盘子永远第一个被拿走
  • 枪械弹夹:最后压入的子弹,会被第一个射出

栈的操作被严格限制在栈顶(Top) 一端,栈底(Bottom)是固定不动的,无法直接操作栈底和中间的元素。

1.2 栈的5个核心操作

所有栈的实现都围绕这5个核心能力,也是面试必须记住的基础:

操作方法 核心作用 异常情况
push(E item) 入栈:将元素压入栈顶 栈空间满时(固定容量栈)抛出异常
pop() 出栈:删除并返回栈顶元素 空栈调用时抛出 EmptyStackException
peek() 查看栈顶:仅返回栈顶元素,不删除 空栈调用时抛出 EmptyStackException
isEmpty() 判断栈是否为空,返回布尔值 无异常
size() 获取栈中元素的总个数 无异常

1.3 栈的核心应用场景

栈的后进先出特性,让它在程序开发中有着不可替代的作用,核心应用包括:

  • JVM 虚拟机栈:Java 方法调用的底层就是基于栈实现,每个方法执行都会创建一个栈帧入栈,方法执行完毕栈帧出栈
  • 括号匹配校验:LeetCode 高频题,判断 ()[]{} 格式是否合法
  • 表达式求值:比如逆波兰表达式(后缀表达式)的计算
  • 浏览器的后退功能、编辑器的撤销(Ctrl+Z)功能
  • 二叉树的前/中/后序遍历、回溯算法的底层实现

二、Java 中栈的两种主流实现方式

Java 中栈的实现分为两类,日常开发优先使用第二种,面试两种都必须掌握

2.1 传统实现:java.util.Stack

Stack 是 JDK 早期提供的栈实现,继承自 Vector 类,是线程安全的(所有方法都加了 synchronized 同步锁)。

完整代码示例
import java.util.Stack;

/**
 * Stack 类基础使用演示
 */
public class StackBasicDemo {
    public static void main(String[] args) {
        // 1. 创建一个存储整数的栈
        Stack<Integer> stack = new Stack<>();

        // 2. 入栈操作 push()
        stack.push(10);
        stack.push(20);
        stack.push(30);
        System.out.println("入栈后的栈内容:" + stack); // 输出:[10, 20, 30]

        // 3. 查看栈顶元素 peek() - 不删除元素
        int topElement = stack.peek();
        System.out.println("当前栈顶元素:" + topElement); // 输出:30
        System.out.println("peek后栈内容:" + stack); // 输出:[10, 20, 30]

        // 4. 出栈操作 pop() - 删除并返回栈顶元素
        int popElement = stack.pop();
        System.out.println("出栈的元素:" + popElement); // 输出:30
        System.out.println("pop后栈内容:" + stack); // 输出:[10, 20]

        // 5. 栈基础信息获取
        System.out.println("栈是否为空:" + stack.isEmpty()); // 输出:false
        System.out.println("栈中元素个数:" + stack.size()); // 输出:2

        // 6. 循环清空栈(标准写法,避免空栈异常)
        while (!stack.isEmpty()) {
            System.out.println("依次出栈元素:" + stack.pop());
        }
        System.out.println("清空后栈是否为空:" + stack.isEmpty()); // 输出:true
    }
}
Stack 类的致命缺点

JDK 官方文档明确说明:不推荐使用 Stack 类,优先使用 Deque 接口,原因如下:

  1. 性能差:继承自 Vector,所有方法都加了同步锁,单线程场景下有不必要的性能开销
  2. 设计缺陷:继承了 Vector,可以通过 get(index)set(index,value) 直接操作栈中任意位置的元素,完全破坏了栈“只能操作栈顶”的封装特性
  3. 扩展性差:基于数组实现,扩容时需要数组拷贝,有额外的性能开销

2.2 官方推荐实现:Deque 接口 + ArrayDeque 实现

Deque 是双端队列接口,支持在两端同时进行插入、删除操作,完全可以模拟栈的“仅单端操作”特性,是 JDK 官方推荐的栈实现方案。

方法对应关系

Deque 中模拟栈的方法和 Stack 类完全兼容,无需修改代码习惯:

栈操作 Deque 对应方法 作用
入栈 push(E e) 元素压入双端队列的头部(栈顶)
出栈 pop() 从双端队列的头部删除并返回元素
查看栈顶 peek() 返回双端队列头部元素,不删除
判空 isEmpty() 同 Stack 类
获取大小 size() 同 Stack 类
完整代码示例
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * 官方推荐:Deque 实现栈的基础演示
 */
public class DequeStackDemo {
    public static void main(String[] args) {
        // 核心写法:用 Deque 接口声明,ArrayDeque 实现类实例化
        Deque<String> stack = new ArrayDeque<>();

        // 1. 入栈操作
        stack.push("Java");
        stack.push("Spring");
        stack.push("MyBatis");
        System.out.println("入栈后的栈内容:" + stack); // 输出:[MyBatis, Spring, Java]

        // 2. 查看栈顶元素
        System.out.println("栈顶元素:" + stack.peek()); // 输出:MyBatis

        // 3. 出栈操作
        System.out.println("出栈元素:" + stack.pop()); // 输出:MyBatis
        System.out.println("出栈后栈内容:" + stack); // 输出:[Spring, Java]

        // 4. 基础信息
        System.out.println("栈是否为空:" + stack.isEmpty()); // 输出:false
        System.out.println("栈元素个数:" + stack.size()); // 输出:2

        // 标准循环遍历出栈
        while (!stack.isEmpty()) {
            System.out.println("依次出栈:" + stack.pop());
        }
    }
}
为什么推荐这种写法?
  1. 性能更优ArrayDeque 基于数组实现,没有 synchronized 同步锁,单线程场景下性能远超 Stack 类
  2. 设计严谨:接口隔离,仅暴露栈相关的操作,不会破坏栈的封装特性
  3. 扩展性强:支持动态扩容,底层数组的扩容效率更高,同时也有 LinkedList 链表实现可选
  4. 通用性强:Deque 接口同时支持栈和队列两种数据结构,学习成本低,适用场景更广

补充说明:LinkedList 也实现了 Deque 接口,也能用来实现栈,但优先使用 ArrayDeque。原因是 ArrayDeque 基于数组实现,缓存友好,访问效率更高;LinkedList 基于链表实现,每个元素都有额外的节点对象开销,内存占用更高。


三、手写实现栈:吃透底层原理

面试中经常会要求“手写一个栈”,核心是基于数组或链表实现,这里我们实现基于数组的泛型顺序栈,覆盖所有核心操作,同时支持动态扩容,帮你彻底理解栈的底层逻辑。

完整手写栈代码

/**
 * 手写基于数组的泛型栈,支持动态扩容
 * @param <E> 栈中存储的元素类型
 */
public class MyArrayStack<E> {
    // 底层存储元素的数组
    private Object[] elementData;
    // 栈中当前元素个数
    private int size;
    // 栈的默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    // 栈的最大容量
    private static final int MAX_CAPACITY = Integer.MAX_VALUE - 8;

    // 无参构造:使用默认容量初始化
    public MyArrayStack() {
        elementData = new Object[DEFAULT_CAPACITY];
        size = 0;
    }

    // 有参构造:指定初始容量
    public MyArrayStack(int initCapacity) {
        if (initCapacity < 0) {
            throw new IllegalArgumentException("初始容量不能为负数:" + initCapacity);
        }
        elementData = new Object[initCapacity];
        size = 0;
    }

    /**
     * 入栈操作
     * @param item 要压入栈的元素
     */
    public void push(E item) {
        // 先判断是否需要扩容
        ensureCapacity(size + 1);
        // 元素放入栈顶,size+1
        elementData[size++] = item;
    }

    /**
     * 出栈操作:删除并返回栈顶元素
     * @return 栈顶元素
     */
    @SuppressWarnings("unchecked")
    public E pop() {
        // 空栈校验
        if (size == 0) {
            throw new EmptyStackException();
        }
        // 栈顶元素下标为 size-1
        int index = --size;
        E oldValue = (E) elementData[index];
        // 置空,帮助GC回收,避免内存泄漏
        elementData[index] = null;
        return oldValue;
    }

    /**
     * 查看栈顶元素:不删除
     * @return 栈顶元素
     */
    @SuppressWarnings("unchecked")
    public E peek() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return (E) elementData[size - 1];
    }

    /**
     * 判断栈是否为空
     * @return 空返回true,非空返回false
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 获取栈中元素个数
     * @return 元素个数
     */
    public int size() {
        return size;
    }

    /**
     * 数组扩容校验:容量不足时自动扩容
     * @param minCapacity 所需最小容量
     */
    private void ensureCapacity(int minCapacity) {
        // 如果当前数组容量不足,需要扩容
        if (minCapacity > elementData.length) {
            grow(minCapacity);
        }
    }

    /**
     * 核心扩容方法
     * @param minCapacity 所需最小容量
     */
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 默认扩容为原容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        // 处理1.5倍扩容后仍不足的情况
        if (newCapacity < minCapacity) {
            newCapacity = minCapacity;
        }

        // 处理超过最大容量的情况
        if (newCapacity > MAX_CAPACITY) {
            newCapacity = minCapacity > MAX_CAPACITY ? Integer.MAX_VALUE : MAX_CAPACITY;
        }

        // 数组拷贝,将原数组内容复制到新扩容的数组中
        Object[] newElementData = new Object[newCapacity];
        System.arraycopy(elementData, 0, newElementData, 0, size);
        elementData = newElementData;
    }

    // 自定义空栈异常
    static class EmptyStackException extends RuntimeException {
        public EmptyStackException() {
            super("栈为空,无法执行该操作");
        }
    }

    // 测试方法
    public static void main(String[] args) {
        MyArrayStack<Integer> stack = new MyArrayStack<>();

        // 入栈
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println("栈大小:" + stack.size()); // 输出:3
        System.out.println("栈顶元素:" + stack.peek()); // 输出:3

        // 出栈
        System.out.println("出栈元素:" + stack.pop()); // 输出:3
        System.out.println("出栈后栈大小:" + stack.size()); // 输出:2

        // 循环出栈
        while (!stack.isEmpty()) {
            System.out.println("依次出栈:" + stack.pop());
        }

        // 测试空栈异常
        try {
            stack.pop();
        } catch (EmptyStackException e) {
            System.out.println("异常捕获:" + e.getMessage());
        }
    }
}

四、栈的经典实战案例(面试高频)

4.1 有效的括号(LeetCode 20,面试必考题)

题目描述

给定一个只包括 (){}[] 的字符串,判断字符串是否有效。有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合
  2. 左括号必须以正确的顺序闭合
  3. 每个右括号都有一个对应的相同类型的左括号
解题思路
  • 遇到左括号,就将对应的右括号压入栈中
  • 遇到右括号,判断是否和栈顶元素一致,一致则出栈,不一致直接返回false
  • 遍历结束后,栈必须为空,否则说明有未闭合的左括号
完整代码实现
import java.util.ArrayDeque;
import java.util.Deque;

public class ValidParentheses {
    public boolean isValid(String s) {
        // 字符串长度为奇数,直接不可能有效
        if (s.length() % 2 != 0) {
            return false;
        }

        Deque<Character> stack = new ArrayDeque<>();

        // 遍历字符串中的每个字符
        for (char c : s.toCharArray()) {
            // 遇到左括号,压入对应的右括号
            if (c == '(') {
                stack.push(')');
            } else if (c == '[') {
                stack.push(']');
            } else if (c == '{') {
                stack.push('}');
            } else {
                // 遇到右括号:栈为空 或 与栈顶元素不一致,直接返回false
                if (stack.isEmpty() || stack.pop() != c) {
                    return false;
                }
            }
        }

        // 遍历结束,栈必须为空才是有效字符串
        return stack.isEmpty();
    }

    // 测试
    public static void main(String[] args) {
        ValidParentheses solution = new ValidParentheses();
        System.out.println(solution.isValid("()[]{}")); // 输出:true
        System.out.println(solution.isValid("(]")); // 输出:false
        System.out.println(solution.isValid("({[]})")); // 输出:true
    }
}

4.2 逆波兰表达式求值(LeetCode 150)

逆波兰表达式也叫后缀表达式,运算符写在操作数之后,是栈的经典应用场景,比如 ["2","1","+","3","*"] 对应的计算公式是 ((2+1)*3)=9

完整代码实现
import java.util.ArrayDeque;
import java.util.Deque;

public class EvalRPN {
    public int evalRPN(String[] tokens) {
        Deque<Integer> stack = new ArrayDeque<>();

        for (String token : tokens) {
            // 遇到运算符,取出栈顶两个元素进行计算
            if (token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/")) {
                // 注意:先出栈的是第二个操作数
                int num2 = stack.pop();
                int num1 = stack.pop();
                int result = switch (token) {
                    case "+" -> num1 + num2;
                    case "-" -> num1 - num2;
                    case "*" -> num1 * num2;
                    case "/" -> num1 / num2;
                    default -> 0;
                };
                // 计算结果重新入栈
                stack.push(result);
            } else {
                // 遇到数字,直接入栈
                stack.push(Integer.parseInt(token));
            }
        }

        // 最终栈中剩下的唯一元素就是计算结果
        return stack.pop();
    }

    public static void main(String[] args) {
        EvalRPN solution = new EvalRPN();
        String[] tokens = {"2","1","+","3","*"};
        System.out.println(solution.evalRPN(tokens)); // 输出:9
    }
}

五、面试高频考点&避坑指南

5.1 面试高频考点

  1. 栈和队列的核心区别是什么?
    • 栈:后进先出(LIFO),仅支持栈顶一端的插入和删除操作
    • 队列:先进先出(FIFO),支持队尾插入、队头删除,两端操作
  2. 为什么 Java 官方不推荐使用 Stack 类?
    • 继承自 Vector,同步锁带来不必要的性能开销
    • 设计缺陷,继承了 Vector 的公共方法,可以直接操作中间元素,破坏栈的封装性
    • 推荐使用 Deque 接口的 ArrayDeque 实现,性能更好、设计更严谨
  3. 栈的入栈、出栈操作时间复杂度是多少?
    • 最优/平均时间复杂度:O(1),仅操作栈顶元素,无遍历
    • 最坏时间复杂度:O(n),基于数组的栈扩容时,需要进行数组拷贝,时间复杂度为O(n)
  4. 手写栈的两种实现方式(数组/链表)
    • 数组实现:顺序栈,需要处理扩容,缓存友好,访问效率高
    • 链表实现:链式栈,无需扩容,内存利用率高,但是有节点额外开销
  5. JVM 中的栈内存和数据结构中的栈有什么关系?
    • JVM 虚拟机栈的底层逻辑完全遵循数据结构栈的“后进先出”特性
    • 每个方法执行时,都会创建一个栈帧压入虚拟机栈,方法执行完毕后栈帧出栈,和数据结构栈的操作完全一致

5.2 新手必看避坑指南

  1. 空栈操作异常:调用 pop()peek() 方法前,必须先用 isEmpty() 判断栈是否为空,否则会抛出空栈异常
  2. 破坏栈的封装性:使用 Stack 类时,不要调用父类 Vector 的 get()set()remove() 方法,会完全破坏栈的结构特性
  3. 混淆栈的进出顺序:栈是后进先出,出栈顺序和入栈顺序相反,做算法题时一定要先画图理清顺序,避免逻辑错误
  4. 手写栈的内存泄漏:基于数组实现栈时,出栈操作要将数组对应位置置空,帮助 GC 回收对象,避免内存泄漏

六、总结

本文从基础原理到代码实现,再到面试实战,全面讲解了 Java 中的栈,核心内容可以用3句话总结:

  1. 栈的核心特性是后进先出(LIFO),所有操作都限制在栈顶,核心操作是 pushpoppeekisEmptysize
  2. Java 开发中优先使用 Deque 接口 + ArrayDeque 实现栈,避免使用老旧的 Stack 类
  3. 栈的核心价值在于处理“有先后顺序、需要回溯”的场景,是 JVM 方法调用、括号匹配、表达式求值、回溯算法等场景的底层核心
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐