在 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 元素 |
5145

被折叠的 条评论
为什么被折叠?



