简体   繁体   中英

Inheritance, composition and default methods

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 scratch) is more maintenable.

This works because the interface contract forces the user to implement all the desired functionality. However in java 8, default methods provide some default behavior which can be "manually" overriden. Consider the following example : I want to design a user database, which must have the functionalities of a List. I choose, for efficiency purposes, to back it by an ArrayList.

public class UserDatabase extends ArrayList<User>{}

This would not usually be considered great practice, and one would prefer, if actually desiring the full capabilities of a List and following the usual "composition over inheritance" motto :

public class UserDatabase implements List<User>{
  //implementation here, using an ArrayList type field, or decorator pattern, etc.
}

However, if not paying attention, some methods, such as spliterator() will not be required to be overridden, as they are default methods of the List interface. The catch is, that the spliterator() method of List performs far worse than the spliterator() method of ArrayList, which has been optimised for the particular structure of an ArrayList.

This forces the developer to

  1. be aware that ArrayList has its own, more efficient implementation of spliterator(), and manually override the spliterator() method of his own implementation of List or
  2. lose a huge deal of performance by using the default method.

So the question is : is it still "as true" that one should prefer composition over inheritance in such situations ?

Before start thinking about performance, we always should think about correctness, ie 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, eg 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.

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.

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 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM