Java 中的 Stack 好纠结~

这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

「栈」是每一个程序员都很熟悉的数据结构,英文叫做 Stack,在 Java 中,栈的实现类是 java.util.Stack。如果你了解 Java 中的 Stack 类,就会知道,这里有一个历史遗留问题:

CleanShot 2022-02-22 at 15.50.02@2x.png

这 Java 的源码,在注释中,Java 官方并不推荐使用这个 Stack 类,而是更推荐下面的写法:

Deque<Integer> stack = new ArrayDeque<Integer>();
复制代码

也就是用一个 Deque 来作为「栈」来使用。

先说一下 Stack 的问题,这个问题的起源是 Stack 的继承关系。

Stack.png

它直接继承于 Vector,Vector 是 Java 中的一个动态数组的实现,乍一想,栈也是一个动态的容器结构,这里并没有什么大问题,但其实并非如此。问题主要来自于 Stack 和 Vector 之间的关系是继承关系,因此,Stack 会继承 Vector 中的公开方法,作为一个动态数组,Vector 允许在数组的任意一个位置插入元素,因此 Stack 类也可以进行如下的操作:

Stack<Integer> stack = new Stack<>();

// 正常的栈操作
stack.push(1);
stack.push(2);
stack.pop();

// 这里就很奇怪~
stack.add(0, 3);
复制代码

如上注释中所说,我们可以像操作一个数组或者列表一样,向其中添加元素,这显然是不对的,这并不是一个栈结构应该具备的特性。暴露了不该暴露的行为,既违背了面向对象设计的原则,也会成为漏洞的来源。

简而言之,Java 中的 Stack 类存在很大的问题。

在面向对象设计原则中,有一个很重要的原则叫作「组合优于继承」,但是这条原则常常被忽略。其实在 Stack 类的问题上,我们就可以通过组合的方式来解决这个问题:

public class Stack<E> {
    private Vector<E> elementVector = new Vector<E>();
    
    /*
    栈结构相关的操作
    */
}
复制代码

让 Stack 包含一个 Vector 结构,而不是继承 Vector,然后只暴露于栈结构相关的操作,就可以解决这个问题。那 Java 为什么不这样解决呢?原因是 Java 一直严格遵守自己的向后兼容性承诺,导致这个 Stack 只能原封不动地放在这里,而后,Java 推荐使用 Deque 来作为 Stack 使用。

Deque 是一个双端队列,队列是一个先进先出的结构,而双端队列的意思就是,两端都可以进出元素。我们再来看看 Java 推荐的栈结构写法:

Deque<Integer> stack = new ArrayDeque<Integer>();
复制代码

在 Java 中,Deque 是一个接口,这里使用了 ArrayDeque 作为实现类,从名字可以看出这是一个通过数组实现的双端队列。除此之外,在 Java 中,LinkedList 也实现了 Deque 接口。

通常情况下,我们按照官方推荐的写法去写,应该是没问题的,但是,双端队列是可以两端都进出元素的,而栈结构只能一端进出元素,这……又回到了 Stack 类的问题。为了解决一个遗留问题提出的替代方案存在与遗留问题同样的问题……

因此,如果我们对栈的结构要求严谨的话,只能自己实现一个了,我们依然可以使用 Deque 来保存元素,然后只暴露栈操作的相关接口就可以了:

public interface Stack<T> { 
    void push(T object); 
    T pop(); 
}
public class DequeStack<T> implements Stack<T> { 
    private final Deque<T> deque = new ArrayDeque<T>(); 
    @Override 
    public void push(T object) { 
        deque.addFirst(object); 
    } 
    @Override 
    public T pop() { 
        return deque.removeFirst(); 
    } 
}
复制代码

Guess you like

Origin juejin.im/post/7067454388712767518