[英]Inheritance, composition and default methods
通常承认,通过继承扩展接口的实现不是最佳实践,并且该组合(例如,从头开始再次实现接口)更加可维护。
这是有效的,因为接口契约迫使用户实现所有所需的功能。 但是在java 8中,默认方法提供了一些可以“手动”覆盖的默认行为。 请考虑以下示例:我想设计一个用户数据库,该数据库必须具有List的功能。 出于效率目的,我选择通过ArrayList来支持它。
public class UserDatabase extends ArrayList<User>{}
这通常不被认为是一种很好的做法,如果实际上希望列表的全部功能并遵循通常的“合成而非继承”的座右铭,那么人们更愿意:
public class UserDatabase implements List<User>{
//implementation here, using an ArrayList type field, or decorator pattern, etc.
}
但是,如果不注意,则不需要覆盖某些方法,例如spliterator(),因为它们是List接口的默认方法。 问题是,List的spliterator()方法执行得比ArrayList的spliterator()方法差得多,后者已针对ArrayList的特定结构进行了优化。
这迫使开发者
所以问题是:在这种情况下,人们应该更喜欢构成而不是继承吗?
在开始考虑性能之前,我们总是应该考虑正确性,即在您的问题中我们应该考虑使用继承而不是委托所暗示的内容。 这个EclipseLink / JPA问题已经说明了这一点 。 由于继承,如果尚未填充延迟填充列表,则排序(同样适用于流操作)不起作用。
因此,我们必须在专业化,覆盖新的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
Iterator
,或使用size()
和get(int)
委托给支持List
。 因此,没有预先default
方法API可以表现出比Java 8 API的default
方法更好的性能的default
,因为过去没有ArrayList
特定的优化。
也就是说,您可以通过以不同方式使用组合来改进您的API设计:不要让UserDatabase
实现List<User>
。 只需通过访问方法提供List
。 然后,其他代码不会尝试流式传输UserDatabase
实例,而是覆盖访问器方法返回的列表。 返回的列表可以是只读包装器 ,它提供JRE本身提供的最佳性能,并在可行的default
注意覆盖default
方法。
我真的不明白这里的大问题。 即使没有扩展, 也可以使用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,通过继承(或接口)链不会带来未来的问题。
我无法针对每种情况提供建议,但对于这种特殊情况,我建议不要实施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();
}
}
请注意,在这种情况下,您将来可以完全自由地更改实施。 例如,将ArrayList
替换为TreeSet
或ConcurrentLinkedDeque
或其他。
选择很简单,根据您的要求。
注 - 以下只是一个用例。 来说明差异。
如果你想要一个不会保留重复的列表,并且要做一大堆与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
。 但它会根据情景而改变。
通常承认通过继承扩展接口的实现不是最佳实践,并且该组合(例如,从头开始再次实现接口)更易于维护。
我认为这根本不准确。 当然,在很多情况下,组合比继承更受欢迎,但是在很多情况下,继承比组合更受欢迎!
特别重要的是要意识到实现类的继承结构不需要像API的继承结构那样。
有没有人真的相信,例如,当编写像Java一样的图形库时,每个实现类应该重新实现paintComponent()方法? 实际上,设计的一个主要原则是,当为新类编写绘制方法时,可以调用super.paint()并确保绘制层次结构中的所有元素,以及处理涉及与本机接口进一步连接的复杂性。在树上。
普遍接受的是,扩展不在您控制范围内且不是为了支持继承而设计的类是危险的,并且在实现发生变化时可能会引发烦人的错误。 (如果您保留更改实施的权利,请将您的课程标记为最终课程!)。 我怀疑Oracle会在ArrayList实现中引入重大变化! 如果你尊重它的文件,你应该没事....
这就是设计的优雅。 如果他们认为ArrayList存在问题,他们将编写一个新的实现类,类似于当天替换Vector时,并且不需要引入重大更改。
===============
在你目前的例子中,有效的问题是:为什么这个类存在?
如果您正在编写一个扩展list接口的类,那么它实现了哪些其他方法? 如果它没有实现新方法,那么使用ArrayList有什么问题?
当你知道答案时,你会知道该怎么做。 如果答案“我想要一个基本上是列表的对象,但有一些额外的便利方法来操作该列表”,那么我应该使用组合。
如果答案是“我想从根本上改变列表的功能”,那么你应该使用继承,或者从头开始实现。 一个示例可能是通过重写ArrayList的add方法来实现不可修改的列表以引发异常。 如果您对这种方法感到不舒服,您可以考虑通过扩展AbstractList来实现,它实际上是继承的,以最大限度地减少重新实现的工作量。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.