栈的核心思想是后进先出,Java集合框架里并没有一个专门的栈接口,但LinkedList通过实现Deque接口提供了完整的栈操作能力。它底层是双向链表,每个节点持有前驱和后继引用,头尾插入删除的时间复杂度都是O(1),这正是栈操作需要的特性。

LinkedList底层结构如何支撑栈操作
LinkedList基于双向链表实现,内部类Node包含item、prev、next三个字段。对一个链表来说,在头部插入和删除节点不需要移动其他元素,只需要调整指针指向,因此push和pop方法可以在常数时间内完成。栈操作的入口通常都在链表头部,这样能避免遍历整个链表。
从接口设计看,LinkedList同时实现了List和Deque。Deque接口中定义了push、pop、peek三个与栈语义直接对应的方法。push方法等价于addFirst,pop等价于removeFirst,peek等价于peekFirst。这种设计让LinkedList在语义上天然支持栈,不需要额外封装。
需要注意的是,LinkedList允许存入null值,而ArrayDeque不允许。虽然栈操作通常不应放入null以免引起歧义,但如果业务上确实需要存储null,LinkedList可以满足。不过,用peek方法查看栈顶时,如果返回null,无法区分是空栈还是栈顶确实为null,所以最好先判断isEmpty。
Java中几种栈实现方式的对比
Java早期提供了Stack类,但它继承自Vector,方法上都加了同步锁。这样的设计在单线程环境下会带来不必要的性能开销,而且Stack类本身没有严格禁止在中间位置插入删除,破坏了栈的抽象。官方文档也明确建议优先使用Deque实现代替Stack。
实际项目中常用的栈实现有LinkedList和ArrayDeque两种。LinkedList是链表结构,头尾操作O(1),但每个节点都有对象头开销,占用内存较大,而且节点分散在堆中,对CPU缓存不友好。ArrayDeque基于循环数组,均摊插入删除同样是O(1),数组连续存储,空间利用率更高,通常被认为是非并发场景下的栈首选。
ArrayDeque不允许元素为null,而LinkedList允许。ArrayDeque在扩容时会复制数组,但均摊开销很低。综合来看,如果不需要存储null,且追求性能,可以选择ArrayDeque;如果代码中已经由LinkedList承担其他集合角色,或需要频繁在两端操作且不关心额外内存,LinkedList也是合理选择。
| 特性 | Stack | LinkedList | ArrayDeque |
|---|---|---|---|
| 底层结构 | 动态数组 | 双向链表 | 循环数组 |
| 栈操作性能 | O(1),但有锁 | O(1) | 均摊O(1) |
| 是否允许null | 允许 | 允许 | 不允许 |
| 线程安全 | 同步方法 | 不安全 | 不安全 |
LinkedList栈操作的核心方法与使用示例
LinkedList中真正具备栈语义的方法是push、pop、peek。push方法将元素压入栈顶,对应addFirst;pop方法移除并返回栈顶元素,对应removeFirst;peek方法只查看栈顶元素但不移除,对应peekFirst。下面是一个基本示例。
LinkedList<String> stack = new LinkedList<>();
stack.push("搜索请求");
stack.push("用户点击");
stack.push("详情页");
String top = stack.peek(); // 详情页
System.out.println(top);
String removed = stack.pop(); // 详情页
System.out.println(removed);
System.out.println(stack.size()); // 2
这段代码压入三个字符串,栈顶始终是最后压入的元素。peek不会改变栈结构,pop会将栈顶移除。输出顺序符合后进先出。如果改用addFirst和removeFirst组合,效果完全一致,但push和pop的语义更清晰,阅读代码的人不需要思考方向。
空栈处理也很重要。如果栈为空时调用pop或removeFirst,会抛出NoSuchElementException。建议在弹出前使用isEmpty判断,或者按照业务逻辑捕获异常。下面的代码展示了安全弹出方式。
LinkedList<Integer> stack = new LinkedList<>();
while (!stack.isEmpty()) {
int value = stack.pop();
System.out.println(value);
}
常见误用与避坑建议
第一种典型误用是把addLast和removeFirst组合当作栈操作。addLast把元素加到链表尾部,removeFirst从头部移除,这其实是队列的先进先出语义。代码本身不会报错,但方法名的字面意思与栈完全相反,容易造成理解偏差。栈操作应坚持使用push和pop,或者addFirst与removeFirst,让行为与命名保持一致。
第二种问题是遍历过程中调用pop修改结构。LinkedList在迭代时如果发生结构性修改,会触发快速失败机制,抛出ConcurrentModificationException。例如用增强for循环遍历栈的同时弹栈,就会踩到这个坑。正确的做法是使用迭代器显式移除,或者先收集需要处理的元素再统一弹出。
LinkedList<String> stack = new LinkedList<>();
stack.push("A");
stack.push("B");
stack.push("C");
for (String item : stack) {
stack.pop(); // 会抛出 ConcurrentModificationException
}
第三种问题是忽略线程安全。LinkedList不是线程安全的,如果多个线程同时进行push和pop操作,可能出现元素丢失或链表结构损坏。并发场景下可以使用Collections.synchronizedDeque包装,或者使用ConcurrentLinkedDeque,但后者的栈语义需要自己封装,因为ConcurrentLinkedDeque的push和pop方法内部没有对无界处理做严格限制,实际并发栈更常用BlockingDeque体系。
最后,如果只需要纯栈结构,建议优先使用ArrayDeque而不是LinkedList。LinkedList的优势在于既能当栈也能当队列,还能随机访问,但灵活性换来的是更多内存消耗。如果代码中不需要List接口的方法,用ArrayDeque创建栈会是更轻量的选择。

