Java Single Inheritance Explained
Understand how Java's single inheritance rule works for classes: extending one superclass, chaining super constructors, overriding methods, and designing clear class hierarchies.
To work with Java class inheritance, you need to understand its core syntax, runtime behavior, and practical implementation patterns. Java enforces single inheritance for classes: a class can directly extend only one superclass. This design choice avoids the ambiguity of multiple inheritance of implementation. A class can implement multiple interfaces, but class-level inheritance stays single. Understanding this rule is fundamental to designing object hierarchies that are predictable and maintainable.
The Core Rule: One Superclass Only
The extends keyword establishes an inheritance relationship between two classes. A subclass inherits accessible fields and methods from its superclass, and it can add new members or override existing ones. The single inheritance rule means this chain is strictly linear: each class has exactly one direct parent.
class Animal { void eat() { System.out.println("Animal eats"); } } class Dog extends Animal { void bark() { System.out.println("Dog barks"); } }
Here, Dog inherits eat() from Animal. If you try to declare a class that extends two classes, the compiler rejects it:
// This does not compile class FlyingDog extends Animal, Bird { }
The reasoning behind this restriction is the diamond problem, where ambiguity arises if two parent classes define the same method. Java sidesteps this by permitting only single inheritance for classes, while interfaces, which are contracts, can be implemented freely.
The super Keyword: Accessing the Parent
A subclass can interact with its superclass using super. There are two primary uses: calling a superclass constructor and invoking a superclass method that has been overridden.
Constructor Chaining
When you create a subclass instance, a superclass constructor is always invoked. If a constructor does not call super(...), the compiler inserts a call to the no-argument superclass constructor. This ensures the superclass is fully initialized first. If the superclass has only parameterized constructors, the subclass must call one explicitly via super(...).
class Vehicle { private String engine; Vehicle(String engine) { this.engine = engine; } } class Car extends Vehicle { Car(String engine) { super(engine); // mandatory because Vehicle has no no-arg constructor } }
Failing to call super(engine) here results in a compile-time error because Vehicle has no default constructor. This rule forces the subclass to participate in proper initialization of inherited state.
Method Invocation
Within an overriding method, super.methodName() lets you call the parent's version. This is useful for extending behavior rather than completely replacing it.
class BasePrinter { void printMessage() { System.out.println("Base message"); } } class DecoratedPrinter extends BasePrinter { @Override void printMessage() { System.out.println("Before base"); super.printMessage(); System.out.println("After base"); } }
This pattern lets a subclass add pre- and post-processing while reusing the parent's logic, which avoids duplication.
Overriding Methods and Method Dispatch
Single inheritance creates a linear chain, so method resolution is straightforward: the most specific override wins; if the subclass does not override the method, the superclass version is used. Polymorphism leverages this: a reference of the superclass type can point to a subclass object, and the correct overridden method is dispatched at runtime.
class Shape { void draw() { System.out.println("Drawing shape"); } } class Circle extends Shape { @Override void draw() { System.out.println("Drawing circle"); } } public class Main { public static void main(String[] args) { Shape s = new Circle(); s.draw(); // Outputs: Drawing circle } }
Because the inheritance chain is single, there is no ambiguity about which override applies. The runtime can follow one path up the class hierarchy to find the most specific implementation.
Constructors and Initialization Order
When a subclass object is created, the superclass constructor runs first, from the topmost superclass down to the subclass. This ensures that inherited fields are initialized before any subclass-specific logic runs.
class Parent { Parent() { System.out.println("Parent constructor"); } } class Child extends Parent { Child() { System.out.println("Child constructor"); } } public class Demo { public static void main(String[] args) { new Child(); // Outputs: Parent constructor, then Child constructor } }
A related correctness concern: because the superclass constructor runs before the subclass constructor body, avoid calling overridable methods from a constructor. The subclass's overridden version may execute before the subclass's fields are initialized, which can lead to subtle bugs.
Designing with Single Inheritance in Mind
Single inheritance encourages deep but narrow hierarchies. When designing a class hierarchy, favor composition over inheritance when the relationship is not a clear "is-a" relationship. Inheritance should be used only when a subclass truly is a more specific version of the superclass, and it must preserve the superclass's contract.
For example, if you want to reuse logging functionality across different classes, a Logger class used as a field is often more flexible than making every class inherit from a LoggingBase class. Composition avoids the coupling that inheritance introduces and allows flexibility without being constrained by single inheritance.
Common Mistakes and How to Avoid Them
One common mistake is attempting to change the return type of an overridden method to an unrelated type. Java allows covariant return types, meaning the overriding method can return a subtype of the original return type, but it cannot return an unrelated type. This restriction keeps the subclass compatible with the superclass contract.
Another frequent error is hiding static methods. If a subclass declares a static method with the same signature as a static method in the superclass, that is not overriding but hiding. The version that runs depends on the reference type, not the object type, which can be surprising. It is generally better to call static methods via the class name to avoid ambiguity.
Single Inheritance vs. Interfaces
The single inheritance rule applies to classes, but a class can implement any number of interfaces. This distinction is significant: a superclass can provide state and concrete methods, while an interface defines a contract. Since Java 8, interfaces can have default and static methods, but they still do not carry instance state. Use inheritance when you want code reuse and shared implementation; use interfaces when you need to define capabilities that unrelated classes can implement.
| Aspect | Single Inheritance | Multiple Interfaces |
|---|---|---|
| Implementation reuse | Yes | Default methods only (Java 8+) |
| State inheritance | Fields inherited | No instance fields (constants allowed) |
| Diamond problem | Not possible for classes | Resolved with specificity rules |
| Flexibility | Limited to one parent | Many contracts |
In practice, combining a single superclass with multiple interfaces is a common and effective pattern. For instance, a class can extend an abstract base that provides common fields and methods, while implementing several interfaces that define its external capabilities.
The Role of Access Modifiers
Access modifiers (private, public, protected) control which members are inherited and visible. Private members are not inherited into the subclass's accessible interface, but they still exist in the object and can be accessed indirectly through public or protected methods. Protected members are inherited and accessible in subclasses, but not from unrelated classes in a different package. When designing a class intended as a superclass, plan the visibility of members carefully: fields should usually be private and exposed through protected or public accessors, to maintain encapsulation and avoid breaking subclasses when the internal representation changes.