【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限制栈的最大容量
  • pushpop操作的时间复杂度均为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,存在两个明显缺点:

  1. 性能问题:Vector 的所有方法都加了synchronized锁,单线程场景下会有不必要的性能开销
  2. 设计冗余: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,都需要掌握其pushpop等核心操作。在实际开发中,栈常用于字符串处理、表达式计算、历史记录回溯等场景,而ArrayDeque是比Stack更推荐的选择。

希望这篇文章能帮助大家全面理解栈的原理和用法,如果有任何疑问或补充,欢迎在评论区留言交流~ 后续我还会分享更多 Java 集合框架的知识点,记得关注我的博客哦!

Tags:

发表回复

Your email address will not be published. Required fields are marked *.

*
*