Inheritance and Interfaces
Extends vs implements, default methods, sealed classes and abstract classes
π Object-Oriented Programming 5 topics
π Table of Contents 9
Inheritance and Composition
Inheritance and composition are the two fundamental mechanisms for building relationships between classes in Java. Choosing correctly between them is crucial for maintainable, flexible software architecture.
1. Definition
What is it?
- Inheritance: An "is-a" relationship where a class (
subclass) inherits all public and protected members of another class (superclass) using theextendskeyword. - Composition: A "has-a" relationship where a class contains an instance of another class as a field and delegates work to it.
Why do both exist?
- Inheritance enables code reuse and polymorphism.
- Composition provides loose coupling and greater flexibility.
- Together they allow developers to choose the right abstraction level for each problem.
Where does it fit?
These are two pillars of the OOP paradigm β most design patterns (Strategy, Decorator, Template Method, etc.) are built on top of them.
2. Core Concepts
2.1 The `extends` keyword
In Java, a class can extend exactly one other class (single inheritance):
public class Animal {
protected String name;
public void eat() {
System.out.println(name + " is eating");
}
}
public class Dog extends Animal {
public void bark() {
System.out.println(name + " says Woof!");
}
}
2.2 The `super` keyword
super serves two main purposes:
| Usage | Syntax | Description |
|---|---|---|
| Constructor call | super(args) |
Invokes the superclass constructor β must be the first statement |
| Method call | super.method() |
Invokes the superclass method (for overridden methods) |
| Field access | super.field |
Accesses the superclass field (when hidden by the subclass) |
public class Dog extends Animal {
public Dog(String name) {
super(); // calls Animal() constructor
this.name = name;
}
@Override
public void eat() {
super.eat(); // calls Animal.eat()
System.out.println("...and wants more!");
}
}
2.3 Method overriding and `@Override`
Rules of overriding:
| Rule | Description |
|---|---|
| Signature | Exactly matching method name and parameters |
| Return type | Same or covariant (subclass) return type |
| Access modifier | Same or wider (e.g., protected β public) |
| Exception | Cannot throw a broader checked exception |
final method |
Cannot be overridden |
static method |
Not overriding β this is called hiding |
β οΈ The
@Overrideannotation is not mandatory, but strongly recommended β it produces a compile-time error if the method doesn't actually override anything.
2.4 Constructor chaining
Constructor invocation order always proceeds top-down through the inheritance hierarchy:
public class Animal {
public Animal() { System.out.println("Animal()"); }
}
public class Dog extends Animal {
public Dog() { System.out.println("Dog()"); } // β implicit super()
}
public class Puppy extends Dog {
public Puppy() { System.out.println("Puppy()"); } // β implicit super()
}
// Output of new Puppy():
// Animal()
// Dog()
// Puppy()
2.5 `final` classes and methods
| Modifier | Effect |
|---|---|
final class |
The class cannot be extended (e.g., String, Integer) |
final method |
The method cannot be overridden in subclasses |
2.6 Composition (has-a) and delegation
Composition is not a language-level keyword β it is a design pattern where a class holds an instance of another class:
public class Engine {
public void start() { System.out.println("Engine started"); }
}
public class Car {
private final Engine engine; // has-a relationship
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start(); // delegation
}
}
2.7 Diamond problem β why no multiple inheritance?
Java does not support multiple class inheritance:
Animal
/ \
Flyer Swimmer
\ /
FlyingFish β NOT POSSIBLE via extends Flyer, Swimmer
Solution: interfaces (implements) and Java 8+ default methods.
public interface Flyer { default void move() { System.out.println("Fly"); } }
public interface Swimmer { default void move() { System.out.println("Swim"); } }
public class FlyingFish implements Flyer, Swimmer {
@Override
public void move() {
Flyer.super.move(); // explicit disambiguation
}
}
2.8 Liskov Substitution Principle (LSP)
"If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program."
Simply put: a subclass must work correctly everywhere the superclass is used.
3. Practical Usage
"Favor composition over inheritance" (GoF)
This is one of the most important pieces of advice from the Gang of Four Design Patterns book.
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship type | is-a | has-a |
| Coupling | Tight | Loose |
| Flexibility | Decided at compile-time | Swappable at runtime |
| Reuse direction | Vertical (hierarchy) | Horizontal (delegation) |
| Encapsulation | Weakens it (subclass knows parent internals) | Strengthens it |
When is inheritance appropriate?
- True is-a relationship β
Dogtruly is anAnimal - Frameworks β extending
HttpServlet,AbstractController - Template Method pattern β abstract class defines the skeleton
- Sealed hierarchies (Java 17+) β closed domain models
When is composition the better choice?
- Behavior needs to change at runtime (Strategy pattern)
- Combining multiple distinct behaviors
- The "is-a" relationship isn't natural
- The parent class is not under your control
4. Code Examples
4.1 Basic β Classic inheritance with override
public abstract class Shape {
public abstract double area();
@Override
public String toString() {
return getClass().getSimpleName() + " [area=" + area() + "]";
}
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; }
}
public class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() { return width * height; }
}
4.2 Advanced β Composition replacing inheritance
Problem: InstrumentedHashSet extends HashSet, but addAll() internally calls add() β double counting (Effective Java Item 18).
// β BAD β fragile base class problem
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
@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); // β this calls this.add()! β double counting
}
}
// β
GOOD β composition + delegation (forwarding wrapper)
public class InstrumentedSet<E> implements Set<E> {
private final Set<E> delegate; // composition
private int addCount = 0;
public InstrumentedSet(Set<E> delegate) {
this.delegate = delegate;
}
@Override
public boolean add(E e) {
addCount++;
return delegate.add(e); // delegation
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return delegate.addAll(c); // no double counting!
}
// ... remaining Set methods delegated ...
@Override public int size() { return delegate.size(); }
@Override public boolean isEmpty() { return delegate.isEmpty(); }
// etc.
}
4.3 Decorator pattern teaser β composition in practice
public interface Logger {
void log(String message);
}
public class ConsoleLogger implements Logger {
@Override
public void log(String message) { System.out.println(message); }
}
public class TimestampLogger implements Logger {
private final Logger delegate; // composition
public TimestampLogger(Logger delegate) {
this.delegate = delegate;
}
@Override
public void log(String message) {
delegate.log("[" + Instant.now() + "] " + message); // decoration
}
}
// Usage β combinable at runtime:
Logger logger = new TimestampLogger(new ConsoleLogger());
logger.log("Hello!"); // β [2026-04-04T10:00:00Z] Hello!
5. Trade-offs
Inheritance pros and cons
| β Pros | β Cons |
|---|---|
| Natural hierarchy expression | Tight coupling between super- and subclass |
| Polymorphism available natively | Fragile base class problem |
| Less boilerplate code | Single inheritance chain only |
| Simple framework extension | Hierarchy depth can become unmanageable |
Composition pros and cons
| β Pros | β Cons |
|---|---|
| Loose coupling β components are independent | More code (delegating methods) |
| Runtime swappability | No native polymorphism (interface needed) |
| Multiple behaviors combinable | Sometimes more complex structure |
| Better testability (mockable) |
6. Common Mistakes
β 1. Breaking LSP
// The Rectangle and Square problem
public class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
public class Square extends Rectangle {
@Override public void setWidth(int w) { this.width = this.height = w; } // β LSP violation!
@Override public void setHeight(int h) { this.width = this.height = h; }
}
// Client code breaks:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(3);
assert r.area() == 15; // FAIL! area() == 9
β 2. Excessively deep inheritance hierarchies
Example of an overgrown hierarchy: Object β BaseEntity β AuditableEntity β SoftDeletableEntity β User β AdminUser
π΄ A hierarchy 5+ levels deep becomes unmaintainable. Max 2-3 levels is recommended.
β 3. Calling overridable methods from constructors
public class Parent {
public Parent() {
init(); // β DANGEROUS! Child's init() is called, but child fields aren't initialized yet
}
protected void init() { /* ... */ }
}
public class Child extends Parent {
private final String value = "hello";
@Override
protected void init() {
System.out.println(value.length()); // NullPointerException! value is still null
}
}
β 4. Omitting the `@Override` annotation
// Typo without @Override β not an override, it's a new method!
public class MyList extends ArrayList<String> {
public boolean Add(String s) { ... } // β capital 'A' β not an override!
}
7. Senior-level Insights
7.1 Effective Java Item 18
"Favor composition over inheritance" β Josh Bloch
Inheritance breaks encapsulation because the subclass depends on the superclass's implementation details (not just its API). If the superclass's internal behavior changes, the subclass can break.
7.2 Template Method vs Strategy pattern
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Inheritance β abstract method | Composition β interface reference |
| When decided? | Compile-time | Runtime |
| Flexibility | One behavior variation | Multiple, swappable behaviors |
| Example | AbstractList.get() |
Passing Comparator to sort() |
7.3 Sealed hierarchies for domain modeling (Java 17+)
sealed classes and interfaces restrict the set of permitted subclasses:
public sealed interface PaymentMethod
permits CreditCard, BankTransfer, DigitalWallet {
}
public record CreditCard(String number, String expiry) implements PaymentMethod {}
public record BankTransfer(String iban) implements PaymentMethod {}
public record DigitalWallet(String provider) implements PaymentMethod {}
Benefits:
- The compiler performs exhaustive checks in
switchexpressions - Safe domain model β nobody can add new implementations
- Combined with pattern matching, produces very expressive code
String describe(PaymentMethod pm) {
return switch (pm) {
case CreditCard cc -> "Card ending in " + cc.number().substring(12);
case BankTransfer bt -> "IBAN: " + bt.iban();
case DigitalWallet dw -> "Wallet: " + dw.provider();
};
}
8. Glossary
| Term | Definition |
|---|---|
| Inheritance | A class inherits members of another class (extends) |
| Composition | A class holds an instance of another class as a field (has-a) |
| Delegation | Forwarding a task to the contained object |
| Method overriding | Replacing the superclass's method implementation in a subclass |
| Constructor chaining | The chain of constructor invocations through the inheritance hierarchy |
| Fragile base class | When internal changes in the superclass break the subclass |
| LSP | Liskov Substitution Principle β subclass must be substitutable for the superclass |
| Diamond problem | Ambiguity caused by multiple inheritance |
| Sealed class | A class with a restricted set of permitted subclasses (Java 17+) |
| Forwarding | Delegation through a wrapper class |
9. Cheatsheet
| Aspect | Inheritance (is-a) |
Composition (has-a) |
|---|---|---|
| Example | class Dog extends Animal |
class Car { private Engine engine; } |
| Benefit | Natural hierarchy | Loose coupling |
| Benefit | Polymorphism | Runtime swappability |
| Trade-off | Tight coupling | More boilerplate |
| Trade-off | Single inheritance | No inherited base behavior by default |
- Always add
@Overridefor compile-time safety. super()must be the first statement in a constructor.finalclasses and methods cannot be extended or overridden.- Keep hierarchies to 2-3 levels whenever possible.
- LSP means the subclass must remain substitutable for the superclass.
- Default choice: prefer composition unless inheritance is clearly natural.