通常承认,通过继承扩展接口的实现不是最佳实践,并且该组合(例如,从头开始再次实现接口)更加可维护。
这是有效的,因为接口契约会强制用户实现所有所需的功能。但是在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的特定结构进行了优化。
这迫使开发者
所以问题是:它是否仍然是真实的&#34;在这种情况下,谁应该更喜欢构成而不是继承呢?
答案 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} }或使用Iterator
和size()
委托给支持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)
我无法针对每种情况提供建议,但对于这种特殊情况,我建议不要实施List
。 UserDatabase.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
替换为TreeSet
或ConcurrentLinkedDeque
或其他。
答案 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来实现,它实际上是为了继承而最大限度地减少重新实现的工作。