Java.util中的Stack类是否违反了Liskov替换原则?

3

来自https://docs.oracle.com/javase/7/docs/api/java/util/Stack.html的文档。

public class Stack<E> extends Vector<E>

这不违反里氏替换原则吗?里氏替换原则简单来说是指,同一父类的对象应该可以相互替换而不会导致任何问题。

例如:假设我有一个函数以Vector作为输入。如果在调用函数时我开始传递一个Stack,则可能会出错,因为Stack不允许随机访问元素。

import java.util.*;

class Book {}

class TextBook extends Book {}

public class Sample {
    public static void process(Vector<Book> books) {
        # This should not be allowed for Stack, Stack is FILO
        System.out.println(books.get(1));
    }

    public static void main(String[] args) {
        Vector<Book> books = new Vector<>();
        books.add(new Book());
        books.add(new Book());
        books.add(new Book());
        process(books);
        System.out.println("ok");

        Stack<Book> bookz = new Stack();
        bookz.add(new Book());
        bookz.add(new Book());
        bookz.add(new Book());
        process(bookz);
        System.out.println("ok");
    }
}

3
请详细说明:首先,为什么您认为这个类必须遵循“里氏替换原则”?规范并没有要求这样做。其次,您为什么认为它没有遵循这个原则? - Stefan
Stack类支持对数据的随机访问,因此不违反规则。但它可能被优化为仅支持push()和pop()。您将会注意到同样的情况适用于ArrayList(优化了随机访问)与LinkedList(优化了快速插入和删除)。 - Stefan
我检查了代码,它确实允许随机访问。我的观点是Stack类的实现违反了LSP,原因在@JohnKugelman对这个问题的回答中得到了很好的解释。 - ThinkGeek
不是的。 这违反了另一个原则(请参见SDJ的答案)。 - RealSkeptic
2
想象一个从Rectangle继承的类Square。这并不违反继承原则,因为数学上来说,Square是一个Rectangle。然而,如果Rectangle有一个setWidthsetHeight,并且你在Square中重写它们,使得它们都设置宽度和高度(以确保它是正方形),那么使用Square作为Rectangle的方法将会看到其大小意外地改变。这就是LSP违规。 - RealSkeptic
显示剩余8条评论
3个回答

4
JDK对Stack的实现是严格累加的:它仅添加了一些方法到Vector的实现中,而不会减少任何东西。因此,可以将Stack分配(替换)给类型为Vector的变量,而不限制客户端代码的操作。因此,这并不违反Liskov替换原则。
然而,其设计被认为存在缺陷,但根据不同的原则:只有在子类真正是超类的子类型的情况下,才适用于继承。来自Effective Java:

Java平台库中存在许多明显违反此原则的情况。例如,堆栈不是向量,因此堆栈不应该扩展向量。


3

是的,它就是。

栈应该只允许推入和弹出,但由于 Stack 扩展了 Vector,因此可以调用完整的 Vector 方法。可以在堆栈中的任何位置插入和删除项目,而不仅仅是在顶部。从概念上讲,应该只调用 push()pop(),但由于向后继承关系,这在语言级别上没有得到执行。

Stack代码没有违反 LSP,但其合同却违反了:"Stack 类表示对象的后进先出(LIFO)堆栈。" 它不执行自己的合同并不意味着它遵守 LSP。文档也很重要。

更好的层次结构应该有一个 Stack 接口,并具有由 Vector 支持但不提供对完整的 Vector 方法访问的具体实现。

public interface Stack<E> {
    E push(E item);
    E pop();
}

public class VectorStack<E> implements Stack<E> {
    private Vector<E> backingVector = new Vector<>();

    ...
}

Stack<E> stack = new VectorStack<>();

StackVector可以追溯到Java的第一个1.0版本。Java 1.2引入了一个更好的集合类库,它们以CollectionList接口及其各种具体实现的形式出现。

Stack等类已经被弃用,现代代码中不应使用。 Stack 的API文档建议改用Deque代替:

A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class. For example:

Deque<Integer> stack = new ArrayDeque<Integer>();

LSP(Liskov Substitution Principle)规定,任何使用Stack作为Vector的东西都应该期望它的行为像一个Vector。对于不知道Stack的类,Stack不会改变它们的行为。同样的,LinkedList实现了Deque,任何将其作为LinkedList使用的东西在插入元素到中间时都不会看到奇怪的行为。 - RealSkeptic
这是一种违规行为,因为栈应该比向量具有更少的操作,但由于破损的继承层次结构,可以在“Stack”上执行非栈操作。它不违反任何Java语言规则,因为它们没有禁用从“Vector”继承的非栈操作,但它设计得很差,违反了LSP,因为您不应该使用任何非栈方法。 - John Kugelman
这不是LSP的违规行为,而是继承思想的违规行为(栈不是向量)。 LSP仅表示如果您将其用作向量,则应该像向量一样运行。它不关心子类契约是什么。 - RealSkeptic
1
虽然如此。名称“stack”意味着元素仅从堆栈顶部推送和弹出。向量方法违反了这个暗示的契约。破碎的层次结构意味着Java无法强制执行该契约。一个人可以自愿遵守它,但没有强制力。 Stack不违反Java关于子类可替换性的规则,但这仅因为Stack不强制执行其自己的类设计。它在代码中不违反LSP,但在概念上却违反了。 - John Kugelman
我有点同意John的思路。虽然代码上没有违反LSP,但概念上确实违反了。 - ThinkGeek

0

Stack继承自Vector,这意味着它继承了所有Vector的方法。这违反了原则。为什么?

Stack抽象数据类型是一种LIFO(后进先出)类型,通常支持像poppushpeektopisEmpty等操作。

Java的堆栈支持不是“堆栈操作”的操作,这些操作是从Vector类继承而来的,例如insertElementAtremoveElementAt等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接