It is usually admitted that extending implementations of an interface through inheritance is not best practice, and that composition (eg. implementing the interface again from s
I cannot provide an advice for every situation, but for this particular case I'd suggest not to implement the List
at all. What would be the purpose of UserDatabase.set(int, User)
? Do you really want to replace the i-th entry in the backing database with the completely new user? What about add(int, User)
? It seems for me that you should either implement it as read-only list (throwing UnsupportedOperationException
on every modification request) or support only some modification methods (like add(User)
is supported, but add(int, User)
is not). But the latter case would be confusing for the users. It's better to provide your own modification API which is more suitable for your task. As for read requests, probably it would be better to return a stream of users:
I'd suggest to create a method which returns the Stream
:
public class UserDatabase {
List<User> list = new ArrayList<>();
public Stream<User> users() {
return list.stream();
}
}
Note that in this case you are completely free to change the implementation in future. For example, replace ArrayList
with TreeSet
or ConcurrentLinkedDeque
or whatever.
The selection is simple based on your requirement.
Note - The below is just a use case . to illustrate the difference.
If you want a list that is not going to keep duplicates and going to do a whole bunch of things very much different from ArrayList
then there is no use of extending ArrayList
because you are writing everything from scratch.
In the above you should Implement List
. But if you are just optimizing an implementation of ArrayList
then you should copy paste the whole implementation of ArrayList
and follow optimization instead of extending ArrayList
. Why because multiple level of implementation makes it difficult for someone tries to sort out things.
Eg: A computer with 4GB Ram as parent and Child is having 8 GB ram. It is bad if parent has 4 GB and new Child has 4 GB to make an 8 GB. Instead of a child with 8 GB RAM implementation.
I would suggest composition
in this case. But it will change based on the scenario.
Before start thinking about performance, we always should think about correctness, i.e. in your question we should consider what using inheritance instead of delegation implies. This is already illustrated by this EclipseLink/ JPA issue. Due to the inheritance, sorting (same applies to stream operation) don’t work if the lazily populated list hasn’t populated yet.
So we have to trade off between the possibility that the specializations, overriding the new default
methods, break completely in the inheritance case and the possibility that the default
methods don’t work with the maximum performance in the delegation case. I think, the answer should be obvious.
Since your question is about whether the new default
methods change the situation, it should be emphasized that you are talking about a performance degradation compared to something which did not even exist before. Let’s stay at the sort
example. If you use delegation and don’t override the default
sorting method, the default
method might have lesser performance than the optimized ArrayList.sort
method, but before Java 8 the latter did not exist and an algorithm not optimized for ArrayList
was the standard behavior.
So you are not loosing performance with the delegation under Java 8, you are simply not gaining more, when you don’t override the default
method. Due to other improvements, I suppose, that the performance will still be better than under Java 7 (without default
methods).
The Stream
API is not easily comparable as the API didn’t exist before Java 8. However, it’s clear that similar operations, e.g. if you implement a reduction by hand, had no other choice than going through the Iterator
of your delegation list which had to be guarded against remove()
attempts, hence wrap the ArrayList
Iterator
, or to use size()
and get(int)
which delegate to the backing List
. So there is no scenario where a pre- default
method API could exhibit better performance than the default
methods of the Java 8 API, as there was no ArrayList
-specific optimization in the past anyway.
That said, your API design could be improved by using composition in a different way: by not letting UserDatabase
implement List<User>
at all. Just offer the List
via an accessor method. Then, other code won’t try to stream over the UserDatabase
instance but over the list returned by the accessor method. The returned list may be a read only wrapper which provides optimal performance as it is provided by the JRE itself and takes care to override the default
methods where feasible.
I don't really understand the big issue here. You can still back your UserDatabase
with an ArrayList even if not extending it, and get the performance by delegation. You do not need to extend it to get the performance.
public class UserDatabase implements List<User>{
private ArrayList<User> list = new ArrayList<User>();
// implementation ...
// delegate
public Spliterator() spliterator() { return list.spliterator(); }
}
Your two points are not changing this. If you know "ArrayList has its own, more efficient implementation of spliterator()", then you can delegate it to your backing instance, and if you do not know, then the default method takes care of it.
I am still unsure whether it really makes any sense to implement the List interface, unless you are explicitly making a reusable Collection library. Better create your own API for such one-offs that does not come with future problems through the inheritance (or interface) chain.
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.
I don't think that this is accurate at all. For sure there are lots of situations where composition is preferred over inheritance, but there are lots of situations where inheritance is preferred over composition!
Its especially important to realise that the inheritance structure of your implementation classes need not look anything like the inheritance structure of your API.
Does anyone really believe, for example, that when writing a graphical library like Java swing every implementation class should reimplement the paintComponent() method? In fact a whole principal of the design is that when writing paint methods for new classes you can call super.paint() and that insures that all elements in the hierarchy are drawn, as well as handling the complications involving interfacing with the native interface further up the tree.
What is generally accepted is that extending classes not within your control that were not designed to support inheritance is dangerous and potentially a source of irritating bugs when the implementation changes. (So mark your classes as final if you reserve the right to change your implementation!). I doubt Oracle would introduce breaking changes into ArrayList implementation though! Provided you respect its documentation you should be fine....
Thats the elegance of the design. If they decide that there is a problem with the ArrayList, they will write a new implementation class, similar to when they replaced Vector back in the day, and there will be no need to introduce breaking changes.
===============
In your current example, the operative question is: why does this class exist at all?
If you are writing a class which extends the interface of list, which other methods does it implement? If it implements no new methods, what is wrong with using ArrayList?
When you know the answer that you will know what to do. If the answer "I want an object which is basically a list, but has some extra convenience methods to operate on that list", then I should use composition.
If the answer is "I want to fundamentally change the functionality of a list" then you should use inheritance, or implement from scratch. An example might be implementing an unmodifiable list by overriding ArrayList's add method to throw an exception. If you are uncomfortable with this approach you might consider implementing from scratch by extending AbstractList, which exists precisely to be inherited from to minimise the effort of reimplementation.