继承、组合和默认方法

Inheritance, composition and default methods

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

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

public class UserDatabase extends ArrayList<User>{}

这通常不会被认为是很好的做法,如果真的想要 List 的全部功能并遵循通常的 "composition over inheritance" 座右铭:

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 或
  2. 实现的 spliterator() 方法
  3. 使用默认方法会损失大量性能。

所以问题是:在这种情况下,是否仍然 "as true" 人们应该更喜欢组合而不是继承?

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

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

   // implementation ...

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

你的两点并没有改变这一点。如果你知道 "ArrayList has its own, more efficient implementation of spliterator()",那么你可以将它委托给你的支持实例,如果你不知道,那么默认方法会处理它。

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

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

我建议创建一个 return 是 Stream:

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

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

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

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

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

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

因此,在 Java 8 下,您并没有 失去 表现,您只是 没有获得 更多,当您不会覆盖 default 方法。我想,由于其他改进,性能仍将优于 Java 7(没有 default 方法)。

Stream API 不容易比较,因为 API 在 Java 8 之前不存在。但是,很明显,类似的操作,例如如果您手动实施缩减,除了通过必须防止 remove() 尝试的委派列表的 Iterator 别无选择,因此包装 ArrayList Iterator,或使用 size()get(int) 委托给支持 List。因此,default 之前的方法 API 不会表现出比 Java 8 API 的 default 方法更好的性能,因为没有ArrayList——具体优化过去反正。

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

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

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

如果你想要一个不会保留重复的列表,并且会做一大堆与 ArrayList 截然不同的事情,那么扩展 ArrayList 是没有用的,因为你是从头开始写一切。

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

例如:具有 4GB 内存的计算机 parent 和 Child 具有 8 GB 内存。如果 parent 有 4 GB 而新的 Child 有 4 GB 来制作 8 GB,那就太糟糕了。而不是具有 8 GB RAM 实现的 child。

在这种情况下,我建议 composition。但是会根据场景变化。

It is usually admitted that extending implementations of an interface through inheritance is not best practice, and that composition (e.g. implementing the interface again from scratch) is more maintainable.

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

认识到您的实现的继承结构 classes 不需要看起来与您的 API 的继承结构有任何相似之处,这一点尤为重要。

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

普遍接受的是,扩展 class 不在您的控制范围内且并非旨在支持继承的内容是危险的,并且在实现更改时可能会成为恼人错误的来源。 (因此,如果您保留更改实施的权利,请将您的 classes 标记为最终版本!)。我怀疑 Oracle 会在 ArrayList 实现中引入重大更改!如果您尊重其文档,您应该没问题....

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

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

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

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

当你知道答案时,你就会知道该怎么做。如果答案"I want an object which is basically a list, but has some extra convenience methods to operate on that list",那么我应该使用组合。

如果答案是 "I want to fundamentally change the functionality of a list" 那么您应该使用继承,或者从头开始实现。一个示例可能是通过覆盖 ArrayList 的 add 方法抛出异常来实现不可修改的列表。如果您对这种方法感到不舒服,您可以考虑通过扩展 AbstractList 从头开始​​实现,它的存在恰恰是为了继承以最大限度地减少重新实现的工作量。