There are two inescapable drawbacks to using inheritance in JAVA:
- Breaking encapsulation forces developers to understand the implementation details of superclasses, subclasses and superclass coupling.
- Superclass updates may cause errors.
Inheritance breaks encapsulation
Here is a detailed example of this (from Effective Java, article 16)
public class MyHashSet<E> extends HashSet<E> {
private int addCount = 0;
public int getAddCount(a) {
return addCount;
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c); }}Copy the code
Here we have a custom HashSet that overrides two methods, and the only difference from the superclass is that we have added a counter to count how many elements have been added.
Write a test to test whether the new feature works:
public class MyHashSetTest {
private MyHashSet<Integer> myHashSet = new MyHashSet<Integer>();
@Test
public void test(a) {
myHashSet.addAll(Arrays.asList(1.2.3)); System.out.println(myHashSet.getAddCount()); }}Copy the code
When you run it, you see that after adding three elements, the counter outputs a value of 6.
If you go into the addAll() method in the superclass, you’ll see why it went wrong: it calls the Add () method internally. So in this test, when the subclass’s addAll() method is entered, the counter is added by 3, and then addAll() of the superclass is called, which in turn calls add() of the subclass three times, at which point the counter is added by another three.
Root of the problem
Abstracting the situation, you can see that the error is due to the existence of overwritable methods of the superclass
(that is, an overridden method in a superclass calls another overridden method), which may cause an error if the subclass overrides some of those methods.
For example, in the case above, the Father class has overridden methods A and B, and A calls B. Subclass Son overrides method B, so if the subclass calls the inherited method A, then method A calls son.b () instead of father.b (). If the correctness of the program depends on some operation in father.b () that son.b () overrides, then errors are likely to occur.
The point is that subclasses can be written in a way that looks fine on the surface but can go wrong, forcing the developer to understand the implementation details of the superclass and breaking object-oriented encapsulation, which requires that implementation details be hidden. More dangerously, errors are not always easily detected, and if a developer overwrites a superclass without knowing the implementation details, it can be a minefield.
An error may occur when the superclass is updated
This is easier to understand, with the following possibilities:
- The superclass changes the signature of an existing method. Causes a compilation error.
- New superclass methods:
- Having the same signature but different return type as a subclass’s existing method results in a compilation error.
- The same as the subclass’s existing method signature causes the subclass to inadvertently overwrite, returning to the first case.
- Does not conflict with subclasses, but may affect the correctness of the program. For example, if a superclass adds a method that inserts elements into a collection without checking them, the correctness of the program is threatened.
Design inheritable classes
When designing classes that can be inherited, note:
- For overwritable methods that are self-contained, the call details should be documented precisely.
- Expose as few protected members as possible, or you will expose too many implementation details.
- The constructor should not call any overwritable methods.
Elaborate on the first three points. It’s actually the same thing
Assume the following code:
public class Father {
public Father(a) {
someMethod();
}
public void someMethod(a) {}}Copy the code
public class Son extends Father {
private Date date;
public Son(a) {
this.date = new Date();
}
@Override
public void someMethod(a) {
System.out.println("Time = "+ date.getTime()); }}Copy the code
The above code throws a NullPointerException when the test is run:
public class SonTest {
private Son son = new Son();
@Test
public void test(a) { son.someMethod(); }}Copy the code
SomeMethod () is overridden, so son.somemethod () will be called in the superclass constructor before the subclass constructor is initialized. A null-pointer exception is thrown when date.getTime() is run.
Therefore, if you have a dependency on an overwritable method in the superclass’s constructor, you may get an error during inheritance.
conclusion
Use inheritance judiciously. Composition takes precedence over inheritance.
Overriding overriding methods that have their own use in a superclass with inheritance can be an error, and updating the superclass can introduce errors even if it is not overridden.
If both inheritance and composition are possible, then composition is preferred, and the above disadvantages of inheritance can be avoided by composition.
If inheritance is to be used, superclasses should be carefully designed and well documented.