从 Vector 继承的 Java 类 Stack 都有哪些负面影响?

Posted

技术标签:

【中文标题】从 Vector 继承的 Java 类 Stack 都有哪些负面影响?【英文标题】:What are the negative aspects of Java class Stack inheriting from Vector?从 Vector 继承的 Java 类 Stack 有哪些负面影响? 【发布时间】:2011-02-24 17:18:39 【问题描述】:

通过扩展类 Vector,Java 的设计者能够快速创建类 Stack。什么是 这种使用继承的负面影响,尤其是对于 Stack 类?

非常感谢。

【问题讨论】:

对我来说听起来很像家庭作业。如果是这样,请标记为这样。 这是书上的问题,但不是作业。 哎呀,我把我的最后一条评论搞砸了......让我们再试一次:在 Java 6 中,你应该使用实现 Deque(如 ArrayDeque)而不是 Stack 的东西,使用addFirst/offerFirstremoveFirst/pollFirstpeekFirst 方法。双端队列:java.sun.com/javase/6/docs/api/java/util/Deque.html 非常好的问题,将其作为适配器可能更有意义 【参考方案1】:

它违反了我们都学到的关于继承的第一条规则:你能不能直截了当地说堆栈是一个向量?显然不是。

另一个更合乎逻辑的操作是使用聚合,但 IMO 最好的选择是让 Stack 成为一个接口,可以通过任何适当的数据结构实现,类似于(但不完全相同)C++ STL可以。

【讨论】:

我想你可以说堆栈是一个向量。只是有一些特殊的规则。【参考方案2】:

除了上面提到的主要有效点之外,Stack 从 Vector 继承的另一个大问题是 Vector 是完全同步的,所以无论你是否需要它都会产生开销(参见 StringBuffer vs. StringBuilder)。我个人倾向于在需要堆栈时使用 ArrayDeque。

【讨论】:

【参考方案3】:

Effective Java 第 2 版,第 16 条:优先组合优于继承

继承仅适用于子类确实是超类的子类型的情况。换句话说,只有当两个类之间存在“is-a”关系时,类 B 才应该扩展类 A。如果你想让一个类B扩展一个类A,问问自己这个问题:每个B真的是一个A?如果你不能如实回答这个问题,B 不应该扩展 A。如果答案是否定的,通常情况是 B 应该包含 A 的私有实例并公开更小更简单的 API; A 不是 B 的重要组成部分,只是其实现的一个细节。

Java 平台库中有许多明显违反此原则的行为。例如,堆栈不是向量,因此Stack 不应扩展Vector。同样,属性列表不是哈希表,因此Properties 不应扩展Hashtable。在这两种情况下,组合都是可取的。

本书更详细,并结合第 17 条:设计和记录继承或禁止继承,建议不要在设计中过度使用和滥用继承。

下面是一个简单的例子,显示了Stack 允许类似Stack 的行为的问题:

    Stack<String> stack = new Stack<String>();
    stack.push("1");
    stack.push("2");
    stack.push("3");
    stack.insertElementAt("squeeze me in!", 1);
    while (!stack.isEmpty()) 
        System.out.println(stack.pop());
    
    // prints "3", "2", "squeeze me in!", "1"

这严重违反了堆栈抽象数据类型。

另见

Wikipedia/Stack (data structure)

在计算机科学中,堆栈是后进先出 (LIFO) 抽象数据类型和数据结构。

【讨论】:

但是如果你放在堆栈上的对象在它在堆栈上时被修改了怎么办?要么我们必须让堆栈在每次推送时都进行深拷贝,要么我们必须更严格地看待 LIFO 的含义。 一个ElectricCar 是一个Car,但是如果你将Car 设为具有IDriveTrain 成员的具体类,你的程序将仍然得到更好的服务,实现通过ElectricDriveTrain。这样,您可以通过组合表示您的ElectricCar,而无需继承带来的紧密耦合(换句话说,您可以分别测试汽车职责和动力传动系统职责)。继承从来都不是适合这项工作的工具。【参考方案4】:

一个问题是 Stack 是一个类,而不是一个接口。这与集合框架的设计不同,您的名词通常表示为接口(例如,List、Tree、Set 等),并且有特定的实现(例如,ArrayList、LinkedList)。如果 Java 可以避免向后兼容,那么更合适的设计是拥有 Stack 接口,然后将 VectorStack 作为实现。

第二个问题是 Stack 现在绑定到 Vector,通常避免使用 ArrayList 等。

第三个问题是您不能轻松地提供自己的堆栈实现,并且堆栈支持非常非堆栈的操作,例如从特定索引中获取元素,包括索引异常的可能性。作为用户,您可能还需要知道堆栈的顶部是在索引 0 处还是在索引 n 处。该接口还公开了诸如容量之类的实现细节。

在原始 Java 类库中的所有决定中,我认为这是比较特殊的决定之一。我怀疑聚合会比继承昂贵得多。

【讨论】:

Sun 建议在 Java 6 及更高版本中使用 Deque(如 ArrayDeque)而不是 Stack。 @Bemrose:确实如此。然而,我实际上并不喜欢它,因为它公开了接口方法来从双方取出东西。 DE 本质对我来说似乎是一个实现细节。我想我是一个 API 纯粹主义者。顺便说一句,我一直讨厌 STL 如何创造“deque”首字母缩写词,因为在大多数口音中,它的发音类似于“dequeue”,导致一些混乱。 旁白:STL 没有发明“双端队列”;这个词已经存在了几十年。快速查看一本 1973 年的教科书,发现索引中有几处提及。【参考方案5】:

拥有Stack 子类Vector 会暴露不适合堆栈的方法,因为堆栈不是向量(它违反了Liskov Substitution Principle)。

例如,堆栈是 LIFO 数据结构,但使用此实现,您可以调用 elementAtget 方法来检索指定索引处的元素。或者你可以使用insertElementAt来颠覆栈合约。

我认为 Joshua Bloch 已经公开表示拥有 Stack 子类 Vector 是一个错误,但不幸的是我找不到参考。

【讨论】:

参见由 Bloch 编写的来自 Effective Java 的 polygenelubricant 的引述。 RE:LSP - 完全不正确。无论您在哪里拥有 java.util.vector,您都可以在不改变函数行为的情况下替换 java.util.stack。作为记录,我相信行为的继承是邪恶的,但 Stack 子类化 Vector 是我遇到的最轻微违反此行为之一。【参考方案6】:

嗯,Stack 应该是一个接口。

Stack 接口应该定义堆栈可以执行的操作。那么Stack 的不同实现可能会在不同情况下表现不同。

但是,由于Stack 是一个具体的类,这不可能发生。我们仅限于堆栈的一种实现。

【讨论】:

以上是关于从 Vector 继承的 Java 类 Stack 都有哪些负面影响?的主要内容,如果未能解决你的问题,请参考以下文章

Java集合系列三Vector-Stack解析

Java 集合深入理解 :stack源码分析,及如何利用vector实现栈

JAVA 集合类(java.util)源码阅读笔记------Stack

Java集合Stack源码深入解析

java常用集合类继承关系

Java中的栈