Problems With Inheritance in Java
Having trouble with proper subclass inheritance in Java? Click here to figure out how to avoid broken code.
Join the DZone community and get the full member experience.
Join For FreeInheritance is one of the fundamental principles in an object-oriented paradigm, bringing a lot of values in software design and implementation. However, there are situations where even the correct use of inheritance breaks the implementations. This post is about looking at them closely with examples.
Fragility of Inheritance
The improper design of the parent class can leads subclasses of a superclass to use the superclass in unexpected ways. This often leads to broken code, even when the IS-A criterion is met. This architectural problem is known as the fragile base class problem in object-oriented programming systems.
The obvious reason for this problem is that the developer of a base class has no idea of the subclass design. When they modify the base class, the subclass implementation could potentially break.
For example, the following program shows how seemingly an inheriting subclass can malfunction by returning the wrong value.
class Rectangle {
private int length;
private int breadth;
public Rectangle(int length, int breadth) {
this.length = length;
this.breadth = breadth;
}
public int calculateArea() {
return length * breadth;
}
// getters and setters
}
// Square IS-A Rectangle
class Square extends Rectangle {
public Square(int side) {
super(side, side);
}
}
Now, the following shows how to the test to ensure that the inheritance works fine.
@Test
public void testSquare() {
Square square = new Square(5);
assertEquals("Area of square", 25, square.calculateArea()); // fine
// set breadth of square :(
square.setBreadth(9);
assertEquals("Area of new square", 81, square.calculateArea()); // ohh nooo
}
Obviously, if I create the instance of Square
and call a method calculateArea
, it will give the correct value. But, if I set any of dimension of the square,
since the square is a rectangle, it gives the unexpected value for the area and the second assertion fails as below:
java.lang.AssertionError: Area of new square
Expected :81
Actual :45
Is There Any Solution?
There is no straightforward solution to this problem because this is all about following the best practices while designing architecture. According to Joshua Bloch, in his book Effective Java, programmers should "design and document for inheritance or else prohibit it."
If there is a breakable superclass, it is better to prohibit inheritance by labeling a declaration of a class or method, respectively, with the keyword "final." And, if you are allowing your class to be extended, it is best to only use one way to populate fields.
Here, use either constructors
or setters
but not both.
So, if I remove the setters from the parent class as below:
class Rectangle {
private int length;
private int breadth;
public Rectangle(int length, int breadth) {
this.length = length;
this.breadth = breadth;
}
public int calculateArea() {
return length * breadth;
}
// getters
}
Then, the child classes can't misuse the setter avoiding fragility issue as:
@Test
public void testSquare() {
Square square = new Square(5);
assertEquals("Area of square", 25, square.calculateArea()); // fine
// square.setBreadth(9); //no way to set breadth :)
}
Inheritance Violates Encapsulation
Sometimes, your private data gets modified and violates encapsulation. This will happen if you are extending features from an undocumented parent class — even though the IS-A criterion is met.
For example, let us suppose A overrides all methods in B by first validating input arguments in each method (for security reasons). If a new method is added to B and A and is not updated, the new method introduces a security hole.
For example, I have created new HashSet implementation to count the numbers of elements added to it as:
class MyHashSet<T> extends HashSet<T> {
//The number of attempted element insertions since its creation --
//this value will not be modified when elements are removed
private int addCount = 0;
public MyHashSet() {}
@Override
public boolean add(T a) {
addCount++;
return super.add(a);
}
@Override
public boolean addAll(Collection<? extends T> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
Everything looks good. So, it is time to test this extension!
@Test
public void testMyHashSet() {
MyHashSet<String> mhs = new MyHashSet<>();
mhs.add("A");
mhs.add("B");
mhs.add("C");
assertEquals("Number of attempted adds so far", 3, mhs.getAddCount());
mhs.remove("B");
assertEquals("Number of attempted adds so far even after removal", 3, mhs.getAddCount());
mhs.addAll(Arrays.asList("D", "E", "F"));
assertEquals("Size of Elements in current set", 5, mhs.size());
assertEquals("New number of attempted adds so far", 6, mhs.getAddCount()); // fails
}
The test fails with a failure in the last assertion as below:
java.lang.AssertionError: New number of attempted adds so far
Expected :6
Actual :9
The cause of the problem is that in the implementation of HashSet, addAll
calls the add
method. Therefore, we are incrementing addCount
too many times in calls to addAll.
How to Fix This Issue?
The principle is the same as in an earlier fix: "Design and document for inheritance or else prohibit it." The proper documentation while designing features would reduce the chances of issues like this.
Fix specific to this issue is not to increment addCount
in addAll
operations, since the value is getting updated in add
operation, which gets called from addAll
as:
class MyHashSet<T> extends HashSet<T> {
// . . .
@Override
public boolean addAll(Collection<? extends T> c) {
return super.addAll(c);
}
// . . .
}
So, this is it! Until next time, happy learning!
As usual, all the source code presented in the above examples is available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments