1. 概述

Java 中的 Stack 类实现了栈数据结构。从 Java 1.6 开始,引入了 Deque 接口,用于实现一个“双端队列”,支持在两端进行元素的插入和删除操作。

现在,我们也可以将 Deque 接口用作 LIFO(后进先出)的栈。此外,如果你查看 Java 官方文档中对 Stack 类的说明,你会发现这样一句话:

更完整且一致的一组 LIFO 栈操作由 Deque 接口及其具体实现提供,应优先使用这些类而不是本类。

本文将对比 Java 中的 Stack 类和 Deque 接口,并探讨 为什么在实现 LIFO 栈时应该优先选择 Deque 而不是 Stack

2. 类 vs 接口

Java 的 Stack 是一个类:

public class Stack<E> extends Vector<E> { ... }

也就是说,如果我们要自定义自己的 Stack 类型,就必须继承 java.util.Stack 类。

由于 Java 不支持多重继承,在我们的类已经继承了其他类的情况下,扩展 Stack 类可能会变得很困难

public class UserActivityStack extends ActivityCollection { ... }

在上面的例子中,UserActivityStack 已经是 ActivityCollection 的子类,因此它无法再继承 java.util.Stack 类。为了使用 Java 原生的 Stack 类,我们可能需要重新设计数据模型。

而 Java 的 Deque 是一个接口:

public interface Deque<E> extends Queue<E> { ... }

我们知道,在 Java 中一个类可以实现多个接口。因此,实现接口比继承类更加灵活。

例如,我们可以轻松地让 UserActivityStack 实现 Deque 接口:

public class UserActivityStack extends ActivityCollection implements Deque<UserActivity> { ... }

✅ 所以从面向对象设计的角度来看,使用 Deque 接口比 Stack 类更灵活

3. 同步方法与性能

我们已经知道 Stack 类是 java.util.Vector 的子类。而 Vector 类是线程安全的,它采用传统方式实现线程安全:所有方法都加上了 synchronized 关键字

作为其子类,Stack 类自然也是同步的

而另一方面,Deque 接口本身并不保证线程安全

⚠️ 因此,如果不需要线程安全特性,使用 Deque 通常比 Stack 性能更好

4. 迭代顺序差异

虽然 StackDeque 都是 java.util.Collection 的子类型,因此也都支持迭代(Iterable),但它们的迭代顺序却不同。

如果我们以相同顺序向 Stack 对象和 Deque 实例中压入相同的元素,它们的迭代顺序是不一样的。

让我们通过示例来看清楚这一点。

4.1. Stack 的迭代顺序是从底部到顶部

@Test
void givenAStack_whenIterate_thenFromBottomToTop() {
    Stack<String> myStack = new Stack<>();
    myStack.push("I am at the bottom.");
    myStack.push("I am in the middle.");
    myStack.push("I am at the top.");

    Iterator<String> it = myStack.iterator();

    assertThat(it).toIterable().containsExactly(
      "I am at the bottom.",
      "I am in the middle.",
      "I am at the top.");
}

运行这个测试会通过。也就是说,当我们遍历 Stack 中的元素时,顺序是从栈底到栈顶

4.2. Deque 的迭代顺序是从顶部到底部

@Test
void givenADeque_whenIterate_thenFromTopToBottom() {
    Deque<String> myStack = new ArrayDeque<>();
    myStack.push("I am at the bottom.");
    myStack.push("I am in the middle.");
    myStack.push("I am at the top.");

    Iterator<String> it = myStack.iterator();

    assertThat(it).toIterable().containsExactly(
      "I am at the top.",
      "I am in the middle.",
      "I am at the bottom.");
}

这个测试也会通过。

✅ 所以说,Deque 的迭代顺序是从栈顶到栈底

📌 对于一个标准的 LIFO 栈来说,正确的迭代顺序应该是从栈顶到底部。从这个角度看,Deque 的行为更符合我们对栈的预期

5. 非法的 LIFO 栈操作

标准的 LIFO 栈只支持三个核心操作:push()pop()peek()Stack 类和 Deque 接口都支持这三个方法。这点上没有问题。

但问题在于,LIFO 栈不应该允许通过索引访问或操作元素,因为这会破坏栈的 LIFO 特性。

接下来我们看看 StackDeque 是否允许这种非法操作。

5.1. java.util.Stack 类允许索引操作 ❌

由于其父类 Vector 是基于数组的结构,Stack 类具备通过索引访问元素的能力:

@Test
void givenAStack_whenAccessByIndex_thenElementCanBeRead() {
    Stack<String> myStack = new Stack<>();
    myStack.push("I am the 1st element."); //index 0
    myStack.push("I am the 2nd element."); //index 1
    myStack.push("I am the 3rd element."); //index 2
 
    assertThat(myStack.get(0)).isEqualTo("I am the 1st element.");
}

运行测试会通过。

也就是说,我们可以通过索引直接访问栈底元素,而不需要遵循 LIFO 的弹出规则。

更离谱的是,我们甚至可以通过索引插入和删除元素

@Test
void givenAStack_whenAddOrRemoveByIndex_thenElementCanBeAddedOrRemoved() {
    Stack<String> myStack = new Stack<>();
    myStack.push("I am the 1st element.");
    myStack.push("I am the 3rd element.");

    assertThat(myStack.size()).isEqualTo(2);

    myStack.add(1, "I am the 2nd element.");
    assertThat(myStack.size()).isEqualTo(3);
    assertThat(myStack.get(1)).isEqualTo("I am the 2nd element.");

    myStack.remove(1);
    assertThat(myStack.size()).isEqualTo(2);
}

这个测试同样可以通过。

📌 所以说,Stack 类允许我们像操作数组一样操作栈元素,完全破坏了 LIFO 的语义

5.2. java.util.Deque 接口不支持索引操作 ✅

Deque 不允许通过索引访问、插入或删除元素,这比 Stack 更合理。

但由于 Deque 是“双端队列”,它可以在两端插入或删除元素。

也就是说,当我们把 Deque 当作栈使用时,仍然可以直接操作栈底元素

@Test
void givenADeque_whenAddOrRemoveLastElement_thenTheLastElementCanBeAddedOrRemoved() {
    Deque<String> myStack = new ArrayDeque<>();
    myStack.push("I am the 1st element.");
    myStack.push("I am the 2nd element.");
    myStack.push("I am the 3rd element.");

    assertThat(myStack.size()).isEqualTo(3);

    // 插入到栈底
    myStack.addLast("I am the NEW element.");
    assertThat(myStack.size()).isEqualTo(4);
    assertThat(myStack.peek()).isEqualTo("I am the 3rd element.");

    // 从栈底删除
    String removedStr = myStack.removeLast();
    assertThat(myStack.size()).isEqualTo(3);
    assertThat(removedStr).isEqualTo("I am the NEW element.");
}

运行测试会通过。

📌 所以说,Deque 也并没有完全遵守 LIFO 的语义

5.3. 基于 Deque 实现一个严格的 LIFO 栈 ✅

我们可以通过封装 Deque 来实现一个严格遵循 LIFO 规则的栈接口:

public interface LifoStack<E> extends Collection<E> {
    E peek();
    E pop();
    void push(E item);
}

然后我们可以在实现类中封装一个 Deque 实例,比如 ArrayDeque

public class ArrayLifoStack<E> implements LifoStack<E> {
    private final Deque<E> deque = new ArrayDeque<>();

    @Override
    public void push(E item) {
        deque.addFirst(item);
    }

    @Override
    public E pop() {
        return deque.removeFirst();
    }

    @Override
    public E peek() {
        return deque.peekFirst();
    }

    // 转发 Collection 接口方法
    @Override
    public int size() {
        return deque.size();
    }
...
}

📌 通过这种方式,我们可以确保只暴露栈应有的操作,从而真正遵守 LIFO 的语义

6. 结论

自 Java 1.6 起,java.util.Stackjava.util.Deque 都可以用来实现 LIFO 栈。本文对比了它们之间的差异,并分析了为什么推荐使用 Deque 而不是 Stack

此外,我们也通过示例展示了 StackDeque 在某些场景下都会破坏 LIFO 语义。

最后,我们还演示了如何基于 Deque 实现一个严格遵守 LIFO 规则的栈接口。

📌 总结一下:

  • ✅ 优先使用 Deque 实现栈,而不是 Stack
  • ⚠️ Stack 是同步的,性能不如 Deque
  • StackDeque 都不完全符合 LIFO 语义
  • ✅ 可以封装 Deque 实现真正的 LIFO 栈

完整代码示例可在 GitHub 项目 中找到。


原始标题:Java Deque vs. Stack | Baeldung