从 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
/offerFirst
、removeFirst
/pollFirst
和 peekFirst
方法。双端队列: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 数据结构,但使用此实现,您可以调用 elementAt
或 get
方法来检索指定索引处的元素。或者你可以使用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 集合深入理解 :stack源码分析,及如何利用vector实现栈