继承,组合和默认方法

时间:2015-07-06 09:20:38

标签: java inheritance java-8 composition default-method

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

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

public class UserDatabase extends ArrayList<User>{}

这通常不被认为是一种很好的做法,如果实际上希望列表的全部功能并且遵循通常的&#34;组合而不是继承,那么人们会更喜欢这种做法。座右铭:

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. 使用默认方法失去了大量性能。
  3. 所以问题是:它是否仍然是真实的&#34;在这种情况下,谁应该更喜欢构成而不是继承呢?

5 个答案:

答案 0 :(得分:6)

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

因此,我们必须在专有化(覆盖新的default方法)在继承情况下完全破坏以及default方法无法使用最大值的可能性之间进行权衡在代表团案件中的表现。我认为,答案应该是显而易见的。

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

因此,如果您没有覆盖default方法,那么您在使用Java 8下的委派时不会失去性能,您只是没有获得更多。我认为,由于其他改进,性能仍然优于Java 7(没有default方法)。

Stream API不容易比较,因为在Java 8之前不存在API。但是,很明显类似的操作,例如如果你手动实施减少,除了通过你的代表团名单的Iterator之外别无选择,必须防范remove()次尝试,因此请ArrayList {{1} }或使用Iteratorsize()委托给支持get(int)。因此,前任List方法API无法表现出比Java 8 API的default方法更好的性能,因为过去没有default特定的优化

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

答案 1 :(得分:2)

我真的不明白这里的大问题。您仍然可以使用ArrayList支持UserDatabase,即使没有扩展它,也可以通过委派获得性能。您无需对其进行扩展即可获得性能。

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,通过继承(或接口)链不会带来未来的问题。

答案 2 :(得分:0)

我无法针对每种情况提供建议,但对于这种特殊情况,我建议不要实施ListUserDatabase.set(int, User)的目的是什么?您真的想要用全新用户替换支持数据库中的 i-th 条目吗?那么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();
    }
}

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

答案 3 :(得分:0)

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

注意 - 以下只是一个用例。说明差异。

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

在上面你应该Implement List。但是,如果您只是优化ArrayList的实现,那么您应该复制粘贴ArrayList的整个实现,并遵循优化而不是extending ArrayList。为什么,因为多层次的实施使得某人试图解决问题变得困难。

例如:一台4GB Ram作为父母的计算机,而Child正在使用8 GB ram。如果父级有4 GB而新的Child有4 GB可以制作8 GB,这是很糟糕的。而不是具有8 GB RAM实现的孩子。

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

答案 4 :(得分:0)

  

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

我认为这根本不准确。当然,有很多情况下组合比继承更受欢迎,但在很多情况下,继承比组合更受欢迎!

特别重要的是要意识到实现类的继承结构不需要看起来像API的继承结构。

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

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

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

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

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

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

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

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