Two cases of wrong inheritance usage

Inheritance and correlated polymorphism are among the most important concepts in OOP languages like Java, Kotlin, C#, and others. In this article, I will explain two typical cases of wrong inheritance usage that anyone should not imitate.

Introduction

Inheritance lets you define a type that derives from another type. In general, inheritance creates a generalization/specialization relationship where the supertype is considered a generalized case, and the subtype is considered a specialized case.

Java has various types of inheritance, as highlighted in the following points:

  • A class can extend only one other class
  • A class can implement one or more interfaces
  • An interface can extend one or more other interfaces

Without going into complicated hierarchies, consider the following simple scenarios:

public class Person {  /*...*/  }
public class Student extends Person {  /*...*/  }
public class Employee extends Person {  /*...*/  }
public class Fruit {  /*...*/  }
public class Orange extends Fruit {  /*...*/  }
public class Apple extends Fruit {  /*...*/  }
public class Animal {  /*...*/  }
public class Cat extends Animal {  /*...*/  }
public class Dog extends Animal {  /*...*/  }

All the above classes are reasonably valid and appropriate at a conceptual level. Each subclass is effectively a “special case” of the superclass. Students and employees are special cases of a person. Oranges and apples are special cases of fruit. And so on.

Whenever you want to use inheritance, you must first ask yourself: is the subtype a “special case” of the supertype? If the answer is yes, then inheritance can be appropriate. If the answer is no, you should avoid it.

There is, however, another question to consider. You should also ask yourself: is the subtype always substitutable for the supertype? Remember that if B extends A, a method with a type A parameter can also receive an object of type B. Thus, the method expects that the object behaves at least as type A.

In other words, you must not define a subtype that imposes some restrictions with respect to the supertype. Otherwise, the subtype is not perfectly replaceable for the supertype.

Wrong inheritance usage 1) 2D and 3D points

Suppose that, for your application, you need to use a class that represents a 2D point. Note that the JavaSE framework already provides some point types, but you may want to define a new class to have more control or implement some specific behavior.

The following is a simple class for a 2D point.

public class Point2D {
    private final double x;
    private final double y;

    public Point2D(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    // other methods toString, equals, etc.
}

The class is immutable, just only for simplicity/brevity. It could also be mutable, but the issue of this case would be the same. So far, the Point2D class, by itself, is correct and appropriate.

Now, suppose you also need to define a class representing a 3D point. Someone may be tempted to create a Point3D class by extending the Point2D class in the following way:

public class Point3D extends Point2D {
    private final double z;

    public Point3D(double x, double y, double z) {
        super(x, y);
        this.z = z;
    }

    public double getZ() {
        return z;
    }

    // other methods toString, equals, etc.
}

Observations

Is it correct? At the technical level, yes, and it can work. However, try to reason about the following issues. Let’s imagine putting a distanceTo method in the Point2D class.

    public double distanceTo(Point2D otherPoint) {
        double dx = x - otherPoint.x;
        double dy = y - otherPoint.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

Since Point3D extends Point2D, you can legally write the following code:

Point2D p1 = new Point2D(5, 8);
Point3D p2 = new Point3D(4, 3, 9);

double d = p1.distanceTo(p2);

Does it have sense? No, unfortunately, it doesn’t have any meaning. The distanceTo method can undoubtedly use the x/y values from Point3D (the variable p2), but the z value is left unused, and there is no sensible way to use it.

And what about a collection of Point2D? If you have a List<Point2D>, you can put both Point2D and Point3D objects into this list. Again, does it have sense? No, not much! Suppose you want to order the points by the distance from the origin (0, 0). What will happen with a mix of Point2D and Point3D objects? You will end up with some strange and unuseful results.

The real problem, in this case, is that a 3D point is not a “special case” of a 2D point. A 3D point differs entirely from a 2D one because it has specific concepts and formulas. For this reason, you should not use inheritance in this case.

Bottom line: never derive a 3D point class from a 2D point class. Generally speaking, never subclass a type just to reuse a handful of instance variables and methods. Before doing this, think about the generalization/specialization relationship (“Is the subtype a specialized case?”).

Wrong inheritance usage 2) Rectangles and squares

For the second case, we can stay in the geometry field. Suppose you need to use classes representing basic figures like rectangles, squares, circles, etc. The following is a simple, mutable, Rectangle class:

public class Rectangle {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    // other methods toString, equals, etc.
}

Note that, in theory, a Rectangle class may derive from an abstract Figure class. It would be appropriate and also helpful, said in general, but this aspect is unimportant now.

Many math textbooks say that a square is a particular case of a rectangle. So, you may be tempted to define a Square class by extending the Rectangle class. Before doing it, try to reason: how do you redefine the setWidth and setHeight methods? Indeed, the width and height cannot be independent in a square.

At the technical level, there is a simple thing you can do. You can set the height when the width changes and vice-versa. But attention, you must do it carefully! Look at the following code:

public class Square extends Rectangle {
    public Square(double side) {
        super(side, side);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

Did you notice the trick? We must call the explicit “super” version of setWidth and setHeight. Otherwise, we would end up in an infinite call loop.

Observations

The Square class may seem appropriate and can work (similarly to what I said for the Point3D class). However, suppose you write the following simple multiplyBy utility method:

    public static void multiplyBy(Rectangle rect, double widthFactor, double heightFactor) {
        double newWidth = rect.getWidth() * widthFactor;
        double newHeight = rect.getHeight() * heightFactor;
        rect.setWidth(newWidth);
        rect.setHeight(newHeight);
    }

Suppose also to use the multiplyBy method in the following ways:

Rectangle rec = new Rectangle(10, 6);
multiplyBy(rec, 4, 2);

Square sqr = new Square(5);
multiplyBy(sqr, 4, 2);

The multiplyBy method works correctly with the Rectangle object, making it large 40 x 12. However, it doesn’t work well with the Square object, because the square is only adjusted to 10 x 10. In practice, only the heightFactor is used since it is the last applied.

The real problem, in this case, is that the Square class imposes an important restriction on width and height since these two properties cannot be independent. In other words, a Square object cannot behave at least as a Rectangle, so a Square is not perfectly substitutable where a Rectangle is expected.

Therefore, is the Square that extends Rectangle always wrong? No, not exactly. It can be valid and appropriate if the classes are immutable. Let’s rewrite them in the following way:

public class Rectangle {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getWidth() {
        return width;
    }

    public double getHeight() {
        return height;
    }

    // other methods toString, equals, etc.
}
public class Square extends Rectangle {
    public Square(double side) {
        super(side, side);
    }
}

The latter Square class imposes no restrictions since there are no setWidth and setHeight methods to deal with. If you create a new Square object, its width and height remain at the same value for the entire object’s life. In this case, it’s correct to say that a square is a particular case of a rectangle since you can only read its width and height properties.

Bottom line: never create a subclass that restricts some behavior (making the subclass not replaceable for the superclass), especially if the two classes are mutable.

Conclusions

In this article, I discussed two noteworthy cases of wrong inheritance usage. The two cases described are the ones that always come to my mind when I think about good/bad applications of inheritance. However, I’m sure many other harmful and inappropriate uses can exist.

Similar Posts