把 Java 的 Stack
想象成一个老式的 弹簧自动售货机 (就是那种按一下按钮,最上面的商品被推出来的那种)。这篇文章揭秘的就是这台"老式售货机"的内部构造、工作原理,以及为什么现在大家都更喜欢用新型的"旋转寿司传送带"(ArrayDeque
)当栈用了!
故事主角:你的"老式弹簧售货机"(Stack<E>
)
-
机器来历 (继承关系)
- 这台售货机 (
Stack
) 其实是用一个 老式伸缩货架 (Vector
) 改装的。Vector
是 Java 早期的一种动态数组,像一根可以伸缩的弹簧柱塞。 Stack extends Vector
:这意味着售货机 继承 了货架的所有功能(放东西、取东西、扩容),并 添加 了栈特有的操作(只操作最上面那个商品)。
- 这台售货机 (
-
核心构造 (内部结构)
-
货架 (
Object[] elementData
) : 一个 数组 ,实际存放商品(元素)。初始货架能放 10 个商品。 -
商品计数器 (
int elementCount
) : 记录货架上 实际有多少个 商品。 -
扩容机制 : 当货架满了(
elementCount == elementData.length
),就要 拉长货架弹簧(扩容)!- 新货架大小 = 旧货架大小 * 2(或者按设定的增量
capacityIncrement
增加)。 - 把旧商品 一个个挪到新货架上 (
System.arraycopy
),这比较耗时(O(n)
)。
- 新货架大小 = 旧货架大小 * 2(或者按设定的增量
-
-
核心操作 (工作原理 - 后进先出 LIFO)
-
放入商品 (
push(E item)
):scssjava Copy public E push(E item) { addElement(item); // 调用父类Vector的方法,把商品放到货架最后面(相当于压到最底下) return item; } // Vector.addElement 简化版 public synchronized void addElement(E obj) { ensureCapacityHelper(elementCount + 1); // 确保货架有位置(不够就扩容) elementData[elementCount++] = obj; // 放在计数器位置,计数器+1 }
- 想象:你 用力一压 ,新商品被塞进货架最 底部 。因为货架是数组,新商品实际放在
elementData[elementCount]
,然后elementCount++
。 - 关键点:虽然叫"压栈",但因为底层是数组,新元素其实放在 物理位置上的末尾(数组索引最大处)。但因为栈只关心"最上面",所以逻辑上它是"栈顶"。
- 想象:你 用力一压 ,新商品被塞进货架最 底部 。因为货架是数组,新商品实际放在
-
取出商品 (
pop()
):javajava Copy public synchronized E pop() { E obj = peek(); // 看看最上面是什么(不拿走) removeElementAt(elementCount - 1); // 调用父类Vector的方法,移除货架最顶上的商品(数组最后一个) return obj; } // Vector.removeElementAt 简化版 public synchronized void removeElementAt(int index) { // ...检查索引... modCount++; // 记录结构变化 int numMoved = elementCount - index - 1; if (numMoved > 0) { System.arraycopy(elementData, index + 1, elementData, index, numMoved); // 把后面的商品往前挪?等等!这里是移除最后一个,所以不用挪! } elementData[--elementCount] = null; // 把腾出来的位置清空,商品计数器-1 }
- 想象:你按一下按钮,最顶上 (货架最后面,索引
elementCount - 1
)的商品 被弹簧弹出来。 - 关键点:直接移除数组最后一个元素 (
elementData[elementCount - 1]
),效率很高 (O(1)
)。不需要移动其他商品! (System.arraycopy
在移除最后一个元素时numMoved=0
,不会执行)。
- 想象:你按一下按钮,最顶上 (货架最后面,索引
-
偷看最上面 (
peek()
):scssjava Copy public synchronized E peek() { int len = size(); // 获取当前商品数量 if (len == 0) throw new EmptyStackException(); // 空的?报错! return elementAt(len - 1); // 返回货架最后一个商品(栈顶) }
- 想象:隔着玻璃看一眼最上面的商品是啥,但不拿出来。
- 关键点:直接访问数组最后一个元素 (
elementData[elementCount - 1]
),效率超高 (O(1)
)。
-
-
老式售货机的特点 (Stack 的特性)
- 线程安全 (
synchronized
) : 它的所有关键操作 (push
,pop
,peek
,size
, 内部调用的Vector
方法) 都加了 锁 (synchronized
) 。想象售货机每次只允许一个人操作,其他人要排队。安全但慢! 这是它最大的特点,也是最大的缺点(在单线程下)。 - 拒绝空盒子 (
null
) : 你不能放进一个空商品 (null
),push(null)
会直接报错 (NullPointerException
)。 - 查找商品位置 (
search(Object o)
) : 告诉机器一个商品的样子,它会从 最上面(最后放进去的)开始往下找 ,告诉你这是第几层(从 1 开始计数)。找到返回层数,找不到返回-1
。 - 判断机器是否空 (
empty()
) : 看计数器elementCount
是不是0
。
- 线程安全 (
-
为什么大家更喜欢新型"旋转寿司店"(
ArrayDeque
)当栈用?-
老式售货机 (
Stack
) 的缺点:- 慢! :因为每个操作都要锁门(
synchronized
),在单线程下纯属多余开销。 - 历史包袱重 :继承了
Vector
的所有方法(比如get(int index)
,addElement
),而这些方法在纯粹的栈操作中通常是不需要的,甚至可能被误用破坏栈的结构。
- 慢! :因为每个操作都要锁门(
-
新型寿司店 (
ArrayDeque
) 的优点:- 快! :没有锁 (
synchronized
),单线程下操作头尾 (addFirst
/removeFirst
当栈用) 都是极快的O(1)
。 - 专注 :它就是一个纯粹的双端队列 (
Deque
),用它当栈 (push
/pop
/peek
) 概念更清晰。 - 扩容更聪明 :虽然也扩容 (
O(n)
),但基于循环数组的设计通常更高效。 - 空间效率 :通常比
Stack
(继承自Vector
)占用稍少一点内存。
- 快! :没有锁 (
-
官方建议 :Java 官方文档明确建议用
Deque
接口及其实现(如ArrayDeque
)替代Stack
来当栈使用!
-
-
老式售货机还能用的场景 (Stack 的应用场景)
- 虽然效率不高,但原理简单易懂,理解栈概念时用它演示很直观。
- 在 极少数 需要保证栈操作 线程安全 且对性能要求不高 的古老代码或特定场景中。但现在更好的线程安全选择是
ConcurrentLinkedDeque
或Collections.synchronizedDeque(new ArrayDeque<>())
。
总结一下,这篇文章讲的就是:
-
Stack
是什么? Java 早期提供的、基于 老式动态数组 (Vector
) 实现的 栈 (LIFO) 数据结构。 -
它怎么工作?
- 压栈 (
push
):塞到数组 末尾 (物理位置),逻辑成为 栈顶。 - 弹栈 (
pop
):移除并返回数组 最后一个元素 (栈顶)。 - 查看栈顶 (
peek
):返回数组 最后一个元素。 - 底层数组 动态扩容 (翻倍或按增量)。
- 压栈 (
-
核心特点:
- 线程安全 (
synchronized
): 最大特色也是最大性能负担。 - 不允许
null
。 - 继承
Vector
的方法,可能被误用。 - 操作栈顶 (
pop
/peek
) 时间复杂度是O(1)
(不扩容时),但受同步锁拖累。
- 线程安全 (
-
主要缺点: 同步锁导致 单线程性能差。历史包袱重。
-
现代替代品:
ArrayDeque
:更快、更专注、更现代。官方推荐用Deque
(ArrayDeque
) 替代Stack
! -
学习价值: 理解栈的基本原理和基于数组的实现很好。理解"为什么它被弃用"对写出好代码很重要。
一句话记住 Stack
:它就像一个加了安全锁的老式弹簧售货机,原理简单但效率不高,现代开发中更推荐使用灵活高效的 ArrayDeque
来当栈! 🧾🔒 下次看到 Stack
,知道它是历史遗迹就好,新项目优先考虑 ArrayDeque
。