Java 栈

在 Java 数据结构与算法领域,栈(Stack)作为一种遵循 LIFO(Last-In-First-Out,后进先出)原则的线性数据结构,是解决诸多编程问题的关键工具。无论是表达式求值、函数调用栈的实现,还是回溯算法的应用,栈都发挥着不可替代的作用。

一、Java 栈基础:概念与核心特性

1.1 栈的定义与核心原则

栈是一种特殊的线性表,它仅允许在表的一端(栈顶,Top)进行插入操作(入栈,push)和删除操作(出栈,pop),而在表的另一端(栈底,Bottom)不允许任何操作。这种 “后进先出” 的特性,使其与队列(FIFO)形成鲜明对比。

形象地说,栈就像一摞叠放的盘子:只能从最上面添加盘子(入栈),也只能从最上面拿走盘子(出栈),最下面的盘子往往是最早放入却最后取出的。

1.2 栈的核心操作

Java 中栈的核心操作主要包括以下 4 类,不同实现类的操作方法名称可能略有差异,但核心逻辑一致:

操作类型

核心功能

常见方法(以 Stack 类为例)

异常情况处理

入栈

将元素添加到栈顶

push(E item)

若栈满(部分实现)则抛出异常

出栈

移除并返回栈顶元素

pop()

若栈空则抛出EmptyStackException

查看栈顶

返回栈顶元素,但不移除

peek()

若栈空则抛出EmptyStackException

判断空栈

判断栈是否为空,返回布尔值

empty()

无异常,返回true或false

此外,部分栈实现还提供查找元素位置的方法(如search(Object o)),返回元素到栈顶的距离(栈顶元素距离为 1),若元素不存在则返回 - 1。

1.3 栈的两种存储结构

栈的底层实现主要依赖两种存储结构,不同结构在性能和适用场景上存在差异:

1.3.1 顺序栈(基于数组实现)

顺序栈使用数组作为底层存储容器,栈顶通过一个指针(通常是 int 类型的索引变量)标记。初始时栈顶指针为 - 1(表示栈空),入栈时指针加 1 并将元素存入对应索引,出栈时先取出栈顶元素再将指针减 1。

核心特性

  • 内存连续:数组元素在内存中连续存储,访问速度快(通过索引直接定位);
  • 容量固定(或动态扩容):初始容量固定,若元素满则需触发扩容(如复制到新数组),扩容过程会消耗额外性能;
  • 时间复杂度:入栈、出栈、查看栈顶操作均为 O (1),查找元素为 O (n)。
1.3.2 链式栈(基于链表实现)

链式栈使用链表作为底层存储容器,通常选择单链表实现,栈顶指向链表的头节点。入栈时在链表头部添加新节点,出栈时删除头部节点并返回其值,栈底为链表的尾节点。

核心特性

  • 内存不连续:链表节点在内存中分散存储,通过指针关联,无需预分配内存;
  • 容量动态:无需指定初始容量,元素数量可随需求动态增减,不会出现栈满(除非内存耗尽);
  • 时间复杂度:入栈、出栈、查看栈顶操作均为 O (1),查找元素为 O (n);
  • 内存开销:每个节点需额外存储指针(如 next 指针),内存开销略高于顺序栈。

二、Java 栈核心实现类解析

Java 中栈的实现类主要分为传统栈类(Stack)双端队列实现的栈(Deque 子类),前者基于 Vector 实现,后者基于双向链表或数组实现,两者在线程安全性、性能等方面差异显著。

2.1 传统栈类:java.util.Stack

java.util.Stack是 Java 最早提供的栈实现类,继承自Vector(Vector 是线程安全的动态数组),本质是一种顺序栈

2.1.1 核心特性
  • 线程安全性:继承自 Vector,所有方法均通过synchronized修饰,支持多线程并发操作,但性能较低;
  • 底层存储:基于动态数组,初始容量为 10,扩容时默认翻倍(可通过ensureCapacity(int minCapacity)自定义扩容);
  • 功能扩展:除栈的核心操作外,还继承了 Vector 的所有方法(如add(int index, E element)、remove(int index)),可直接操作栈中间元素,破坏了栈的 LIFO 特性;
  • 禁止 null 元素:虽未显式禁止插入 null,但插入 null 后调用search(null)会抛出NullPointerException,实际使用中建议避免存储 null。
2.1.2 源码关键逻辑(以核心方法为例)
public class Stack<E> extends Vector<E> {

// 构造函数:默认初始化Vector(初始容量10)

public Stack() {

}

// 入栈:将元素添加到栈顶(Vector的末尾)

public E push(E item) {

// 调用Vector的addElement()方法,线程安全(synchronized修饰)

addElement(item);

return item;

}

// 出栈:移除并返回栈顶元素(Vector的最后一个元素)

public synchronized E pop() {

E obj;

int len = size();

// 先获取栈顶元素,若栈空则抛出异常

obj = peek();

// 移除栈顶元素(Vector的最后一个元素)

removeElementAt(len - 1);

return obj;

}

// 查看栈顶元素:返回Vector的最后一个元素

public synchronized E peek() {

int len = size();

if (len == 0)

throw new EmptyStackException();

return elementAt(len - 1);

}

// 判断栈空:若Vector大小为0则返回true

public boolean empty() {

return size() == 0;

}

// 查找元素:返回元素到栈顶的距离,不存在则返回-1

public synchronized int search(Object o) {

// 从栈顶向栈底遍历(Vector的反向遍历)

int i = lastIndexOf(o);

if (i >= 0) {

return size() - i;

}

return -1;

}

}
2.1.3 优缺点与应用场景

优点

  • 线程安全,无需手动处理并发同步;
  • 基于数组实现,访问速度快,适合元素数量相对固定的场景。

缺点

  • 线程安全基于synchronized,高并发场景下性能低下;
  • 继承 Vector 导致可操作中间元素,破坏栈的封装性;
  • 动态扩容时需复制数组,频繁扩容会影响性能。

应用场景

  • 低并发场景下的简单栈操作(如单线程环境下的表达式求值);
  • 需保证线程安全且对性能要求不高的场景。

2.2 推荐栈实现:Deque 接口与 LinkedList/ArrayDeque

由于Stack类存在性能与封装性问题,Java 官方推荐使用java.util.Deque接口(双端队列)及其实现类来模拟栈的功能。Deque支持在两端进行插入和删除操作,通过限制仅在一端操作(如仅使用push()、pop()、peek()方法),即可实现严格的 LIFO 特性。

常用的Deque实现类包括LinkedList(基于双向链表)和ArrayDeque(基于动态数组),两者均为非线程安全,但性能远优于Stack类。

2.2.1 LinkedList:基于双向链表的栈

LinkedList实现了Deque接口,可作为链式栈使用,底层通过双向链表存储元素,栈顶对应链表的头节点。

核心特性

  • 非线程安全:无synchronized修饰,高并发场景需手动加锁(如使用Collections.synchronizedDeque());
  • 底层存储:双向链表,每个节点包含prev、next指针和元素值,无需预分配内存;
  • 容量动态:元素数量可随需求增减,无栈满问题(除非内存耗尽);
  • 性能特点:入栈(push())、出栈(pop())操作均为 O (1),但链表节点的创建和销毁会带来轻微内存开销;
  • 支持 null 元素:允许插入 null,但需注意peek()或pop()返回 null 时无法区分 “栈空” 与 “元素为 null”。

栈操作方法映射(Deque方法与栈操作的对应关系):

栈操作

Deque 方法(推荐使用)

等效 Deque 方法(另一端操作)

入栈

push(E e)(头插)

addFirst(E e)

出栈

pop()(头删)

removeFirst()

查看栈顶

peek()(头查)

peekFirst()

判断空栈

isEmpty()

isEmpty()

源码关键逻辑(以 push () 和 pop () 为例)

public class LinkedList<E> extends AbstractSequentialList<E>

implements List<E>, Deque<E>, Cloneable, java.io.Serializable {

// 入栈:将元素添加到链表头部(栈顶)

public void push(E e) {

addFirst(e);

}

// 添加到头部(核心逻辑)

private void linkFirst(E e) {

final Node<E> f = first;

// 创建新节点,prev为null,next指向原头部节点

final Node<E> newNode = new Node<>(null, e, f);

first = newNode;

// 若原链表为空,则尾部节点也指向新节点

if (f == null)

last = newNode;

else

f.prev = newNode; // 原头部节点的prev指向新节点

size++;

modCount++;

}

// 出栈:移除并返回链表头部元素(栈顶)

public E pop() {

return removeFirst();

}

// 移除头部(核心逻辑)

public E removeFirst() {

final Node<E> f = first;

if (f == null)

throw new NoSuchElementException(); // 栈空时抛出异常

return unlinkFirst(f);

}

// 解除头部节点的链接

private E unlinkFirst(Node<E> f) {

final E element = f.item;

final Node<E> next = f.next;

f.item = null; // 帮助GC回收

f.next = null;

first = next;

if (next == null)

last = null;

else

next.prev = null;

size--;

modCount++;

return element;

}

}
2.2.2 ArrayDeque:基于动态数组的栈

ArrayDeque同样实现了Deque接口,是基于动态数组的双端队列,可作为顺序栈使用,底层通过循环数组(环形缓冲区)存储元素,栈顶通过头指针(head)标记。

核心特性

  • 非线程安全:无synchronized修饰,性能优于Stack和LinkedList;
  • 底层存储:循环数组,初始容量为 16(必须是 2 的幂),扩容时翻倍(保证容量始终为 2 的幂,便于通过位运算计算索引);
  • 容量动态:数组满时自动扩容,扩容过程为 O (n),但因初始容量合理且扩容频率低,实际性能优异;
  • 性能特点:入栈、出栈、查看栈顶操作均为 O (1),数组访问速度快于链表,且无链表节点的额外内存开销;
  • 禁止 null 元素:插入 null 会抛出NullPointerException,避免了 null 值带来的歧义。

栈操作核心逻辑(以 push () 为例)

public class ArrayDeque<E> extends AbstractCollection<E>

implements Deque<E>, Cloneable, java.io.Serializable {

private transient E[] elements; // 底层循环数组

private transient int head; // 头指针(栈顶)

private transient int tail; // 尾指针

private static final int MIN_INITIAL_CAPACITY = 16;

// 入栈:将元素添加到栈顶(head指针位置)

public void push(E e) {

if (e == null)

throw new NullPointerException();

// 计算新的head指针(循环数组:head = (head - 1) & (elements.length - 1))

elements[head = (head - 1) & (elements.length - 1)] = e;

// 若head == tail,说明数组满,触发扩容

if (head == tail)

doubleCapacity(); // 扩容为原容量的2倍

}

// 扩容逻辑:创建新数组,复制原数组元素

private void doubleCapacity() {

assert head == tail; // 仅当数组满时调用

int p = head;

int n = elements.length;

int r = n - p; // 从head到数组末尾的元素个数

int newCapacity = n << 1; // 容量翻倍

if (newCapacity < 0)

throw new IllegalStateException("Deque too big");

Object[] a = new Object[newCapacity];

// 复制两段元素:从head到末尾,从开头到tail

System.arraycopy(elements, p, a, 0, r);

System.arraycopy(elements, 0, a, r, p);

elements = (E[]) a;

head = 0;

tail = n;

}

}
2.2.3 Deque 实现类与 Stack 类的对比

特性

Stack

LinkedList(Deque)

ArrayDeque(Deque)

线程安全

是(synchronized)

底层存储

动态数组

双向链表

循环数组

初始容量

10

无(动态创建节点)

16(2 的幂)

扩容机制

翻倍

无需扩容

翻倍(始终为 2 的幂)

支持 null 元素

是(不推荐)

性能(单线程)

低(synchronized 开销)

中(链表节点操作)

高(数组直接访问)

推荐场景

低并发线程安全需求

元素数量不确定、需 null 元素

高并发、高性能、无 null 元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值