1. Inheritance limitations in Java
Only single inheritance of classes. In Java, a class can inherit from only one other class. This is called single inheritance. For example, this is allowed:
class Animal { }
class Dog extends Animal { }
But this is not allowed:
class Animal { }
class Robot { }
// ERROR! Java does not support multiple inheritance of classes
class RoboDog extends Animal, Robot { }
If you try to declare such a class, the compiler will say: "class RoboDog cannot extend multiple classes". Why? Because multiple inheritance leads to ambiguities: if both parents have a method with the same signature, which one should be used? This is the famous “diamond problem.”
Interfaces in Java can be implemented in any number, but we haven’t studied them yet. We’ll talk about them later.
Constructors are not inherited. Even if you have a parent class with a convenient constructor, that constructor will not appear in the child class automatically. You need to explicitly call the parent’s constructor via super(...) in the child class constructor.
Private members are not inherited. All private (private) fields and methods of the parent are not accessible in the child class. They exist “inside” the object, but you cannot access them directly.
2. Problems of a fragile hierarchy
Tight coupling between classes. When you create a class hierarchy, subclasses become tightly coupled to the parent class. If you change the base class, it can affect (or even break) all of its subclasses. Imagine you have a class Animal, from which Dog, Cat, Bird, and a dozen others inherit. If you change the structure of Animal (for example, by adding a new required parameter to the constructor), you’ll have to go through all child classes and update their code. This is especially painful in large projects.
The “breaking” inheritance problem. Sometimes a subclass can unintentionally change behavior that the base class relies on. For example, the parent class calls its method inside another method, and the subclass overrides that method and changes its logic. As a result, the parent class starts to work differently than expected.
class Animal {
void makeSound() {
System.out.println("Some sound");
}
void sleep() {
System.out.println("Animal is going to sleep...");
makeSound(); // Parent calls its own method
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.sleep();
}
}
What will the program print?
Animal is going to sleep...
Woof!
The base class expected that makeSound() was its own implementation, but in fact the version from the subclass is called! This can lead to unexpected bugs if the subclass overrides the method with different logic.
3. The fragile base class problem
This is a real problem in large projects. If you change the base class (for example, by adding a field or changing a method implementation), you risk breaking the behavior of all subclasses. Sometimes this doesn’t show up immediately, and finding such a bug can take hours or even days.
An illustration: suppose you have a class Shape with a draw() method. You decide to add a new method drawShadow() to Shape that calls draw(). But one of the subclasses (Circle) overrides draw(), and now when calling drawShadow() on Circle, the behavior may be unexpected.
4. Tight coupling and refactoring difficulties
When classes are connected through inheritance, changing one class can affect an entire chain of dependencies. This makes the code less flexible and complicates refactoring and extension. Sometimes you have to rewrite entire hierarchies to add new functionality.
Real-world example
class Vehicle { /* ... */ }
class Car extends Vehicle { /* ... */ }
class Bicycle extends Vehicle { /* ... */ }
class Bus extends Vehicle { /* ... */ }
Suddenly a requirement comes in: “Let’s add an electric scooter!” But an electric scooter is both a vehicle and a gadget. What do you do? If you start stretching the hierarchy to fit every new entity, it will quickly become unmanageable.
5. The problem of code reuse without a logical relationship
Very often beginners (and not only beginners) use inheritance for code reuse even when there is no “is-a” (is-a) relationship between classes. This leads to an incorrect architecture.
An example of incorrect inheritance
class DatabaseUtils {
void connect() { /* ... */ }
void disconnect() { /* ... */ }
}
class User extends DatabaseUtils { // A user is not a database utility!
String name;
}
It’s better to use composition: make DatabaseUtils a separate class and call its methods where needed, rather than inheriting from it.
6. Alternatives to inheritance
Composition (has-a)
If an object “contains” another object, use composition. For example, a Car can have an Engine field:
class Engine { /* ... */ }
class Car {
private Engine engine;
// ...
}
Delegation
Instead of extending a class, delegate the task to another object. This preserves flexibility and reduces component coupling.
Interfaces
In Java, a class can implement any number of interfaces. This allows you to combine behavior flexibly without a rigid hierarchy. We’ll return to interfaces later.
When should you use inheritance?
Use inheritance only if there is a clear “is-a” (is-a) relationship between classes:
- A cat is an animal (Cat extends Animal)
- A circle is a shape (Circle extends Shape)
- An administrator is a user (Admin extends User)
Do not use inheritance just for code reuse—for that, use composition and delegation.
7. A few practical examples
Example: an overcomplicated hierarchy
class Animal { }
class Mammal extends Animal { }
class Cat extends Mammal { }
class PersianCat extends Cat { }
class SuperPersianCat extends PersianCat { }
If your hierarchy goes deeper than three levels, think twice: maybe it’s time to stop. Hierarchies that are too deep make code harder to understand and maintain.
Example: a flat hierarchy
class Animal { }
class Cat extends Animal { }
class Dog extends Animal { }
class Bird extends Animal { }
class Fish extends Animal { }
class Spider extends Animal { }
class Platypus extends Animal { }
class Dragon extends Animal { }
If you have dozens of subclasses, each differing by only one method, it may be better to use interfaces or composition.
8. Typical mistakes when using inheritance
Error #1: Inheritance without an “is-a” relationship.
If the subclass is not actually a kind of the parent, the architecture becomes unnatural and quickly gets out of control. For example, the User class should not inherit from DatabaseUtils, even if that seems “convenient.”
Error #2: Overriding methods while changing the contract.
If you override a method and change its logic so that it no longer meets the parent’s expectations, this will lead to unexpected errors. For example, if the base class expects the draw() method to draw a shape, but in a subclass it suddenly starts performing dangerous side effects—this is a disaster.
Error #3: Deep or overly flat hierarchies.
Hierarchies that are too deep make code hard to understand; overly flat hierarchies lead to duplication.
Error #4: Trying to bypass language limitations.
Developers attempt to implement multiple inheritance with hacks (copy-paste, “utility” superclasses), which leads to chaos.
Error #5: Blindly using inheritance for code reuse.
This often leads to unexpected couplings between classes and complicates testing and maintenance. Use composition and delegation instead.
GO TO FULL VERSION