Collections.unmodifiableXXX方法是否违反了LSP?

时间:2014-02-26 19:06:44

标签: java oop collections liskov-substitution-principle

Liskov Substitution principleSOLID的原则之一。我现在已经多次阅读过这个原则,并试图理解它。

这是我用它做的,

  

这一原则与强烈的行为契约有关   类的层次结构。子类型应该可以替换为   在不违反合同的情况下超级型。

我也读过其他一些articles,我对这个问题的思考有点失落。 Collections.unmodifiableXXX()方法是否违反了LSP?

以上链接文章的摘录:

  

换句话说,当通过其基类接口使用对象时,   用户只知道基数的前提条件和后置条件   类。因此,派生对象不得指望此类用户服从   比基类

所需的前提条件更强的前提条件

为什么我这么认为?

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
     }
}

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

这就是为什么Guava为集合创建了单独的ImmutableXXX接口吗?

这不是直接违反LSP或我完全弄错了吗?

4 个答案:

答案 0 :(得分:34)

LSP说每个子类必须服从与超类相同的契约。无论是否Collections.unmodifiableXXX()都是如此,因此取决于合同的读取方式。

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

add()的总合同是什么?根据{{​​3}},它是:

  

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

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

  

如果集合因任何原因拒绝添加特定元素   除了它已经包含元素,它必须抛出一个   异常(而不是返回false)。这保留了不变量   一个集合在此之后总是包含指定的元素   呼叫返回。

这明确允许实现具有不将add的参数添加到集合但导致异常的代码。当然,这包括他们将该(合法)可能性考虑在内的集合客户的义务。

因此仍然实现了行为子类型(或LSP)。 但这表明,如果计划在子类中有不同的行为,那么在顶级类的规范中也必须预见到这一点。

顺便说一下,好的问题。

答案 1 :(得分:12)

是的,我相信你说得对。从本质上讲,要实现LSP,您必须能够使用超类型执行的子类型。这也是椭圆/圆问题出现在LSP上的原因。如果Ellipse具有setEccentricity方法,并且Circle是Ellipse的子类,并且对象应该是可变的,则Circle无法实现setEccentricity方法。因此,你可以用Ellipse做一些你不能用Circle做的事情,因此违反了LSP。†同样,你可以用普通List做一些你无法做到的事情。一个由Collections.unmodifiableList包裹的,这是一个LSP违规。

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

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

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

有些人注意到Collection的文档允许实现从add方法抛出异常。我认为这确实意味着无法修改的列表在遵守add合同时遵守法律条文,但我认为应该检查一个人的代码,看看有多少地方可以保护在争论不违反LSP之前,使用try / catch块调用List(addaddAllremoveclear)的变异方法。也许它不是,但这意味着所有在它作为参数接收的List上调用List.add的代码都会被破坏。

那肯定会说很多。

(类似的论点可以表明null是每种类型成员的想法也违反了Liskov替代原则。)

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

答案 2 :(得分:5)

我不相信这是违规行为,因为合同(即List接口)表示变异操作是可选的。

答案 3 :(得分:0)

我认为你不是在这里混合东西 来自LSP

  

Liskov的行为亚型概念定义了一个概念   可变对象的可替代性;也就是说,如果S是T的子类型,   那么程序中类型T的对象可以用对象替换   键入S而不改变任何所需的属性   程序(例如,正确性)。

LSP指的是子类

列表是接口而不是超类。它指定了类提供的方法列表。但是这种关系不像父类那样耦合。 A类和B类实现相同接口的事实并不能保证这些类的行为。一个实现总是可以返回true而另一个实现抛出异常或总是返回false或者其他什么但是两者都遵循接口,因为它们实现了接口的方法,因此调用者可以在对象上调用该方法。