【问题标题】:Do Collections.unmodifiableXXX methods violate LSP? [closed]Collections.unmodifiableXXX 方法是否违反 LSP? [关闭]
【发布时间】:2014-02-26 19:06:44
【问题描述】:

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 还是我完全搞错了?

【问题讨论】:

标签: java oop collections liskov-substitution-principle


【解决方案1】:

LSP 说每个子类都必须遵守与超类相同的契约。 Collections.unmodifiableXXX() 是否属于这种情况,因此取决于该合同的阅读方式。

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

add()的总合约是什么?根据API documentation 是:

确保此集合包含指定的元素(可选 手术)。如果此集合因以下原因而更改,则返回 true 称呼。 (如果此集合不允许重复,则返回 false 并且 已经包含指定的元素。)

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

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

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

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

顺便说一句,问得好。

【讨论】:

  • 此外,文档定义了在拒绝添加时应在特定情况下抛出的异常,包括 UnsupportedOperationException
  • 这也是为什么你永远不应该修改从你不知道的 API 获得的集合的原因。如果它没有说明,集合是可变的,你应该总是创建一个新的。
  • @cypressious:这只是很小的一部分原因。一个更大的原因是,即使知道集合允许突变,也不知道是否给了一个集合的副本或不应该更改的集合视图。
【解决方案2】:

是的,我相信你说的没错。本质上,要实现 LSP,您必须能够对子类型执行任何您可以对超类型执行的操作。这也是 LSP 出现椭圆/圆问题的原因。如果 Ellipse 有 setEccentricity 方法,并且 Circle 是 Ellipse 的子类,并且对象应该是可变的,那么 Circle 就无法实现 setEccentricity 方法。因此,你可以用椭圆做一些你不能用圆形做的事情,因此违反了 LSP。† 同样,你可以用普通的 List 做一些你不能用一个包裹的东西做的事情Collections.unmodifiableList,所以这是违反 LSP 的。

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

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

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

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

这肯定会说很多。

(类似的论点可以表明null 是每个类型的成员的想法也违反了里氏替换原则。)

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

【讨论】:

  • 然而,List 的操作方法指定了它们的行为方式有多种。合同仅规定必须采用其中之一,可修改和不可修改列表都是这种情况。严格来说,假设 List 的 add 方法不会抛出 UnsupportedOperationException 的每个人都违反了替换,而不是实现 List 的类。
  • 我认为您在技术上是正确的,但是有理论,然后有实践中的合同。如果有两种类型,ImmutableList 和 List,并且 add 方法只存在于第二种,我们一开始就不会遇到这种情况。
  • 换句话说,将一个方法放入您的 API 中,然后声明有时它会做某事,有时它会抛出,这只会招来麻烦。这是一个等待发生的错误,它破坏了 LSP 应该保护您免受伤害的东西。
  • 当然还有其他设计可能性,其中可变和不可变列表之间存在类型区别,但我认为这不是必需的。重要的是断言在调用者和被调用者之间进行通信 - 类型这样做。但是即使没有类型区分,方法也可以在其文档中指定返回值是“不可变列表”,即使这对编译器没有意义,程序员也可以告诉他们不能修改或替换它它需要一个可修改的列表。
  • 严格来说,可变列表也是如此,因此人们应该指定他们返回的列表等确实是可修改的,但我会说这是按照最少意外原则预期的,所以它是记录不可变列表确实更重要。
【解决方案3】:

我不认为这是违规行为,因为合约(即List 接口)说变异操作是可选的。

【讨论】:

  • 我不太清楚“可选”对于 LSP 的含义。客户端代码绝不能使用它,因为它可能没有它。
  • @djechlin 是的,我就是这么想的。不受支持的操作不应该真正成为不支持的接口的一部分。
  • @NarendraPathai:如果接口Wozzler 的许多实现也实现方法Wuzzle,并且Wozzler 的许多消费者希望使用方法Wuzzle(如果可用),否则会做其他事情,那么恕我直言,让Wizzler 包含Wuzzle 以及确定给定实例是否支持它的方法通常比让Wuzzle 只出现在Wuzzler 界面中而不出现在@987654330 中要好得多@。如果支持按索引写入的固定大小的集合和支持add但不按索引写入的可变大小的集合共享一个公共接口
  • ...然后一个“WrappedObservableCollection”类型将能够同时使用两者(允许客户端使用基础集合支持的任何功能)。在类型系统中分离接口能力,而不是通过能力报告方法,意味着集合可能具有的每个能力组合都需要不同的包装类。
【解决方案4】:

我认为你没有在这里混合。
来自LSP

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

LSP 指的是子类

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

【讨论】:

  • 为此目的,接口基本上可以被认为与超类相同。 (事实上​​,由于缺乏多重继承,它们在很大程度上只是一个半途而废的解决方法——在某些具有 MI 的语言中,比如 C++,“接口”只是具有纯虚拟的类成员。)接口很少只提供函数名称列表;它通常还定义了函数的作用,任何符合接口的东西最好做这些事情。
  • @cHao:我不同意。接口和超类是不相关的概念。LSP只是关于子类型化。机制类似,在IntefaceA的方法中有一个参数并传递InterfaceImplInterfaceImpl2,但我们仍然没有像 LSP 所说的那样进行子类型化。引用oracle:If your class claims to implement an interface, all methods defined by that interface must appear in its source code before the class will successfully compile.docs.oracle.com/javase/tutorial/java/concepts/interface.html
  • Oracle 专注于语言本身。 Java 不关心 SOLID,并且无法将其强制执行到任何有用的程度。但LSP 仍然适用。更重要的是,您实际上 依赖 应用它。 证明:假设我创建了一个 List&lt;T&gt;,其中每个函数都会抛出 RuntimeException。这是编译器允许的,并且当且接口本身具有预期但在实现中不存在的已知属性时才违反LSP。所以我通过了我的“列表”,你尝试使用它,然后kaboom!现在,谁应该为由此造成的崩溃负责?如果你责备我,那你就是依赖 LSP。
  • 我同意@cHao,LSP applies to the contract,不管是如何指定的。
猜你喜欢
  • 2013-06-15
  • 2013-04-28
  • 2017-03-17
  • 2011-10-12
  • 2016-03-28
  • 1970-01-01
  • 1970-01-01
  • 2014-08-16
相关资源
最近更新 更多