Java Stack解析

Stack继承自Vector

Stack是数据结构中栈的实现,栈的特点是后进先出。

借用一张图片来描述栈的入栈、出栈行为

虽然Stack的可以由Vector封装实现,且底层数据结构都是数组。但是为什么Stack作为一个基础的数据结构,没有像List、Queue以接口的形式继承自Collection,而是Vector的一个子类,个人认为这个逻辑性不是很合理;Set接口的实现如HashSet也是基于HashMap,但是再对外的接口暴露和抽象层级关系上,彼此是隔离的,当然不包含重复元素的列表和哈希表并没有直接关系,彼此是独立的数据结构,声明也表明了这种关系;Stack的实现可以由LinkedList、ArrayList、Vector作为基底集合,但是栈的数据结构的声明应该是独立,不应是一个直接的、继承了List的某实现类的派生类。

Stack是一个线程安全的类,不仅仅由于它的基类是线程安全的,它的子类方法也都是同步方法。为什么Stack需要是线程安全的?这一点我也不理解

Stack的API:

1.push()
入栈

2.pop()
出栈

3.peek()
返回栈顶元素,不出栈

4.empty()
是否为空
为什么不叫isEmpty()呢?父类已存在该方法。两者只是实现上前者利用了Vector的封装方法size(),后者直接判断元素个数,empty()方法的存在意义下文会叙述。

5.search()
从栈顶元素向下,循环该元素第一次出现的位置

Stack的数据结构、API很容易理解,但是JDK里对Stack的实现却让人很费解,网上的讨论多认为这是早期设计不规范所致

举例说明Stack的设计错误引起的问题:

        Stack<Integer> stack = new Stack<>();
        stack.push(1);
        stack.add(2);
        System.out.println(stack.pop());
        stack.add(1,3);
        System.out.println(stack.pop());

add()是List接口的抽象方法之一,Stack可以调用父类Vector的add()实现,子类调用父类方法是很符合面向对象规则的。但是以上代码会输出add()的元素2,也就是add()入栈成功。但是从API声明的角度,这是职责模糊的,明明push()是入栈方法,add()又是什么?
不仅仅存在声明模糊的问题,父类的某些方法还会破坏Stack的数据结构的约束。比如

    public void add(int index, E element) {
        insertElementAt(element, index);
    }

以上是Vector的方法,在某个位置添加元素。上述实验代码第二次输出元素3,但是从API的声明角度来看,对入栈一次的栈执行两次出栈行为是有问题的,而且该方法还破坏了栈先进后出的特点,因为它是在指定位置插入元素,因此破坏了Stack的数据结构的约束。
也许empty()方法的意义就是类库开发人员不希望使用者主动调用父类的isEmpty()方法
如果想要正确的使用Stack,建议不要使用其父类方法,只使用Stack的自实现方法。优秀的类库不应该让使用者必须阅读实现代码才能正确使用。
这里的Stack实现确实有些问题,应该实现独立的、继承自Collection的栈接口。猜测当时的设计人员可能图一时方便,做了快速的实现,但是现在木已成舟了。所以接口化、声明和实现相分离的意义是很重大的;这里如果遵循接口化的实现方法,不会出现这些问题。

JDK上Stack的注释上有这么一段

<p>A more complete and consistent set of LIFO stack operations is
 * provided by the {@link Deque} interface and its implementations, which
 * should be used in preference to this class.  For example:
 * <pre>   {@code
 *   Deque<Integer> stack = new ArrayDeque<Integer>();}</pre>

大意是创建栈应优先使用Deque这个接口,ArrayDeque作为实现类,而不是使用Stack。可见JDK也在逐渐的修正这个问题。

posted @ 2019-08-24 20:26  Elinlinlin  阅读(428)  评论(0编辑  收藏  举报