Inheritance happens to be one of the prime features of the Java language and is one of the key requirements for Object-Oriented Programming. It provides a lot of help in reducing code repetition, helps in designing the application and whatnot. However, for the best use of this feature, we must also know when we shall avoid using it.
Just like any other feature of a language, inheritance (and by inheritance, we’re talking in terms of class and not interface) can be useful as well as problematic. If you’re extending a class that was developed by yourself or your team, then probably you’ll be fine. Or if a class was designed in a way so that it needed to be extended, probably you’ll be fine too. But there are situations unlike the aforementioned.
Inheritance can violate encapsulation. A superclass may change its implementation from one release to another, which may return in an error in the subclass without even changing a line of code. The code breaks unless we end up changing the subclass with each release of the superclass.
To understand this situation better, let’s go through an example. Let’s implement a variation of the HashSet collection which keeps track of the total number of elements ever being added to the Set. That would include the current elements and the deleted or removed elements.
A pretty decent implementation that should work for our use case, right? I’m afraid not. Let’s see how it runs.
So even though the list only had 2 strings, the count this function shows in 4. What? Why? Is HashSet broken? Actually, none of these. It’s just that HashSet implements the addAll( ) function by internally calling the add( ) method. Since we’ve overridden both, the counter gets updated twice for a single list element.
There are multiple ways of “fixing” this situation. We could just avoid overriding the addAll( ) method, and hence use the addAll( ) from HashSet that doesn’t increment the counter and hence we would eventually get the correct count. But what happens if the authors of HashSet decided to change the original implementation of addAll( ) and instead of using the add( ) method which we very conveniently overloaded, use some other logic. It breaks our “fix” without even changing a line in our codebase.
We could even override the original method in a way that doesn’t use the addAll( ) method at all. Instead, put in a logic of our own that works our use case. Not only would that be time-consuming and difficult, it some times may not be even possible since it may require access to the private data members of the superclass.
Depending on the logic of the superclass wouldn’t just be an issue of the convention, but an overall bad decision. Suppose in a custom implementation of a HashSet we only allowed even elements. So in our implementation, we would just override all the methods capable of adding an element and put in a predicate check for odd and even. And ta-da! It works. And one fine day, the authors of HashSet went a little too generous by introducing another way of adding an element. Due to inheritance, the custom implementation of our Set also exposes a method to add an element without checking for its odd or even status.
So we go berzerk and decide that we’re done overriding a superclass’ method. We would just extend the HashSet class and simply provide our own methods to do our needful business. No overriding at all! I won’t deny that this is a very safe route to take. But some bad luck strikes and the authors of HashSet accidentally create another method in another release that matches the whole signature of your custom method( which isn’t a very unlikely situation ) barring the return type. Boom! The subclass can’t compile anymore. If you change the return type to match the newly added method’s return type, you get into the whirlpool of the aforementioned problems. Possibly, our implementation and the Super Class’ implementation of the function may be about two totally different things.
So in short, extending a class that you don’t control or which wasn’t intended to be extended may get your code in a situation worse than when your code doesn’t run. It gives you a fragile code that may break without any changes made to it. In part 2 of this blog, we’re going to discuss about Composition, and how that can save us the trouble. Stay tuned.