11

通常承认通过继承扩展接口的实现不是最佳实践,并且组合(例如,从头开始再次实现接口)更易于维护。

这是可行的,因为接口契约强制用户实现所有所需的功能。但是在 java 8 中,默认方法提供了一些可以“手动”覆盖的默认行为。考虑以下示例:我想设计一个用户数据库,它必须具有列表的功能。为了提高效率,我选择用 ArrayList 来支持它。

public class UserDatabase extends ArrayList<User>{}

这通常不会被认为是一种很好的做法,如果实际上想要 List 的全部功能并遵循通常的“组合优于继承”的座右铭,人们会更喜欢:

public class UserDatabase implements List<User>{
  //implementation here, using an ArrayList type field, or decorator pattern, etc.
}

但是,如果不注意,某些方法,例如 spliterator() 将不需要被覆盖,因为它们是 List 接口的默认方法。问题是,List 的 spliterator() 方法的性能远不如 ArrayList 的 spliterator() 方法,后者已针对 ArrayList 的特定结构进行了优化。

这迫使开发人员

  1. 请注意,ArrayList 有自己的、更高效的 spliterator() 实现,并手动覆盖他自己的 List 实现的 spliterator() 方法或
  2. 使用默认方法会损失大量性能。

所以问题是:在这种情况下,人们应该更喜欢组合而不是继承,这仍然“真实”吗?

4

5 回答 5

6

在开始考虑性能之前,我们始终应该考虑正确性,即在您的问题中,我们应该考虑使用继承而不是委托意味着什么。这个 EclipseLink/JPA 问题已经说明了这一点。由于继承,如果尚未填充延迟填充的列表,则排序(同样适用于流操作)不起作用。

default因此,我们必须在覆盖新方法的特化在继承情况下完全中断的可能性default与在委托情况下方法无法发挥最大性能的可能性之间进行权衡。我想,答案应该是显而易见的。

由于您的问题是关于新default方法是否会改变这种情况,因此应该强调的是,与以前甚至不存在的东西相比,您所谈论的性能下降。让我们停留在sort示例中。如果您使用委托并且不覆盖default排序方法,则该default方法的性能可能低于优化ArrayList.sort方法,但在 Java 8 之前,后者不存在,并且未优化的算法ArrayList标准行为。

因此,当您不覆盖该方法时,您不会在 Java 8 下失去委托的性能,您根本不会获得更多。default我想由于其他改进,性能仍将优于 Java 7(没有default方法)。

该API 不容易比较,因为该 API 在 Java 8 之前不存在。但是,很明显,类似的操作,例如,如果您手动实现归约,除了通过您的委托列表Stream之外别无选择Iterator防止remove()尝试,因此包装ArrayList Iterator, or to use size()and get(int)which delegate to the backing List. 因此,在任何情况下,预default方法 API 都不会表现出比defaultJava 8 API 的方法更好的性能,因为过去无论如何都没有ArrayList特定的优化。

也就是说,您的 API 设计可以通过以不同的方式使用组合来改进:根本不让UserDatabase实现List<User>。只需提供List通过访问器方法。然后,其他代码不会尝试在UserDatabase实例上流式传输,而是在访问器方法返回的列表上流式传输。返回的列表可能是一个只读包装器,它提供最佳性能,因为它由 JRE 本身提供,并注意default在可行的情况下覆盖方法。

于 2015-07-06T10:30:03.427 回答
2

我真的不明白这里的大问题。UserDatabase即使不扩展它,您仍然可以使用 ArrayList 来支持它,通过委托获得性能。您无需扩展它即可获得性能。

public class UserDatabase implements List<User>{
   private ArrayList<User> list = new ArrayList<User>();

   // implementation ...

   // delegate
   public Spliterator() spliterator() { return list.spliterator(); }
}

你的两点并没有改变这一点。如果你知道“ArrayList 有它自己的、更高效的 spliterator() 实现”,那么你可以将它委托给你的支持实例,如果你不知道,那么默认方法会处理它。

我仍然不确定实现 List 接口是否真的有意义,除非你明确地制作了一个可重用的 Collection 库。通过继承(或接口)链更好地为此类一次性使用创建您自己的 API,这些 API 不会带来未来的问题。

于 2015-07-06T09:34:38.253 回答
0

我不能为每种情况提供建议,但对于这种特殊情况,我建议根本不要实施List。目的是UserDatabase.set(int, User)什么?您真的想用全新的用户替换后备数据库中的第 i 个条目吗?怎么样add(int, User)?在我看来,您应该将其实现为只读列表(UnsupportedOperationException每个修改请求上抛出)或仅支持某些修改方法(add(User)支持,但add(int, User)不支持)。但是后一种情况会让用户感到困惑。最好提供您自己的更适合您的任务的修改 API。至于读取请求,可能最好返回一个用户流:

我建议创建一个返回的方法Stream

public class UserDatabase {
    List<User> list = new ArrayList<>();

    public Stream<User> users() {
        return list.stream();
    }
}

请注意,在这种情况下,您将来可以完全自由地更改实现。例如,替换ArrayListTreeSetorConcurrentLinkedDeque或其他。

于 2015-07-06T10:24:57.953 回答
0

根据您的要求,选择很简单。

注意- 下面只是一个用例。来说明区别。

如果你想要一个不会重复的列表,并且会做一大堆非常不同的事情,ArrayList那么扩展是没有用的,ArrayList因为你是从头开始编写所有内容。

在上面你应该Implement List。但是,如果您只是优化一个实现,ArrayList那么您应该复制粘贴整个实现ArrayList并遵循优化而不是extending ArrayList. 为什么,因为多层次的实施使某人难以理清事情。

例如:一台有 4GB 内存的计算机作为父母和孩子有 8 GB 内存。如果父母有 4 GB 而新的孩子有 4 GB 来制作 8 GB,那就不好了。而不是一个具有 8 GB RAM 实现的孩子。

我建议composition在这种情况下。但它会根据场景而改变。

于 2015-07-06T10:49:36.907 回答
0

通常承认通过继承扩展接口的实现并不是最佳实践,而组合(例如从头开始重新实现接口)更易于维护。

我认为这根本不准确。当然,有很多情况下组合优先于继承,但也有很多情况下继承优先于组合!

意识到实现类的继承结构不必看起来像 API 的继承结构,这一点尤为重要。

例如,有没有人真的相信,在编写像 Java swing 这样的图形库时,每个实现类都应该重新实现 paintComponent() 方法?事实上,设计的一个整体原则是,在为新类编写绘制方法时,您可以调用 super.paint() 并确保绘制层次结构中的所有元素,并进一步处理涉及与本机接口交互的复杂性上树。

普遍接受的是,扩展不在您控制范围内且未设计为支持继承的类是危险的,并且在实现更改时可能会导致烦人的错误。(因此,如果您保留更改实现的权利,请将您的课程标记为最终课程!)。我怀疑 Oracle 会在 ArrayList 实现中引入重大更改!如果你尊重它的文件,你应该没问题....

这就是设计的优雅。如果他们确定 ArrayList 存在问题,他们将编写一个新的实现类,类似于他们在当天替换 Vector 时,并且不需要引入重大更改。

================

在您当前的示例中,操作性问题是:为什么这个类存在?

如果您正在编写一个扩展列表接口的类,它还实现了哪些其他方法?如果它没有实现新方法,那么使用 ArrayList 有什么问题?

当您知道答案时,您将知道该怎么做。如果答案“我想要一个基本上是列表的对象,但有一些额外的便利方法可以在该列表上进行操作”,那么我应该使用组合。

如果答案是“我想从根本上改变列表的功能”,那么你应该使用继承,或者从头开始实现。一个示例可能是通过重写 ArrayList 的 add 方法来引发异常来实现不可修改的列表。如果您对这种方法感到不舒服,您可以考虑通过扩展 AbstractList 从头开始​​实现,它的存在正是为了继承以最小化重新实现的工作量。

于 2015-07-06T11:16:55.160 回答