Collections.unmodifiableXXX 方法是否违反了 LSP?[已关闭]

里氏替代原理SOLID的原理之一。我现在已经多次阅读了这个原则,并试图理解它。

这是我从中得到的,

这个原则与类层次结构之间的强行为契约有关。子类型应该能够在不违反约定的情况下替换为超类型。

我也读过其他一些文章,思考这个问题我有点迷茫。方法是否不违反 LSP?Collections.unmodifiableXXX()

上面链接的文章摘录:

换句话说,当通过其基类接口使用对象时,用户只知道基类的前置条件和后置条件。因此,派生对象不得期望此类用户遵守比基类所需的前提条件更强的前提条件。

我为什么这么认为?

以前

class SomeClass{
      public List<Integer> list(){
           return new ArrayList<Integer>(); //this is dumb but works
      }
}

class SomeClass{
     public List<Integer> list(){
           return Collections.unmodifiableList(new ArrayList<Integer>()); //change in implementation
     }
}

我无法改变将来返回不可修改列表的暗示。编译将起作用,但如果客户端以某种方式尝试更改返回的,那么它将在运行时失败。SomeClassList

这就是Guava为集合创建单独的不可变XXX接口的原因吗?

这不是直接违反了LSP还是我完全搞错了?


答案 1

LSP 表示,每个子类都必须遵守与超类相同的契约。因此,情况是否如此,取决于本合同的读法。Collections.unmodifiableXXX()

返回的对象如果尝试对其调用任何修改方法,则会引发异常。例如,如果调用 ,则将抛出一个。Collections.unmodifiableXXX()add()UnsupportedOperationException

什么是总合同?根据 API 文档,它是:add()

确保此集合包含指定的元素(可选操作)。如果此集合由于调用而发生更改,则返回 true。(如果此集合不允许重复并且已包含指定的元素,则返回 false。

如果这是完整的合同,那么不可修改的变体确实不能在所有可以使用集合的地方使用。但是,该规范仍在继续,并且还说:

如果集合出于除已包含该元素以外的任何原因拒绝添加特定元素,则必须引发异常(而不是返回 false)。这将保留在此调用返回后集合始终包含指定元素的不变量。

这显式允许实现具有不向集合添加参数但会导致异常的代码。当然,这包括收款客户有义务考虑这种(法律)可能性。add

因此,行为子类型(或LSP)仍然得到满足。但这表明,如果一个人计划在子类中具有不同的行为,那么也必须在toplevel类的规范中预见到。

顺便说一句,好问题。


答案 2

是的,我相信你是对的。从本质上讲,要实现 LSP,您必须能够对子类型执行任何操作,而子类型可以对超类型执行任何操作。这也是 LSP 出现椭圆/圆问题的原因。如果椭圆有一个方法,而圆是椭圆的子类,并且对象应该是可变的,那么Circle就无法实现该方法。因此,你可以对椭圆执行一些你不能对圆执行的操作,因此违反了 LSP。†同样,你可以对常规执行一些操作,而对包装者执行的椭圆不能执行,因此这是 LSP 违规。setEccentricitysetEccentricityListCollections.unmodifiableList

问题在于,我们想要的东西(一个不可变的、不可修改的、只读的列表)没有被类型系统捕获。在 C# 中,你可以使用它来捕获一个序列的想法,你可以迭代和读取,但不能写入。但是在Java中只有,它通常用于可变列表,但我们有时希望将其用于不可变列表。IEnumerableList

现在,有些人可能会说Circle可以实现并简单地抛出一个异常,同样,当您尝试修改它时,不可修改的列表(或来自Guava的不可变列表)会引发异常。但这并不意味着从LSP的角度来看,它是一个列表。首先,它至少违反了最小意外原则。如果调用方在尝试将项添加到列表时遇到意外异常,则非常令人惊讶。如果调用代码需要采取措施来区分它可以修改的列表和一个不能修改的列表(或者它可以设置其偏心率的形状,以及一个不能设置的形状),那么一个实际上不能替代另一个。setEccentricity

如果 Java 类型系统具有一个只允许迭代的序列或集合的类型,以及另一个允许修改的类型,那就更好了。也许Iterable可以用于此,但我怀疑它缺少一些真正想要的功能(如)。不幸的是,我认为这是当前Java集合API的局限性。size()

有几个人已经注意到,的文档允许实现从该方法中引发异常。我想这确实意味着一个不能修改的列表在合同方面遵守了法律条文,但我认为在争论LSP没有被违反之前,应该检查一个人的代码,看看有多少地方保护对List(,,,)的突变方法的调用。也许它不是,但这意味着调用它作为参数接收的列表的所有代码都已损坏。CollectionaddaddaddaddAllremoveclearList.add

这肯定会说很多。

(类似的论点可以表明,作为每种类型的成员的想法也违反了Liskov替换原则。null

†我知道还有其他方法可以解决椭圆/圆问题,例如使它们不可变,或删除setEccentricity方法。我在这里谈论的只是最常见的情况,作为类比。