8.1 Inheritance and Subtype Contracts
Prerequisites: Classes, Constructors, Encapsulation, and Access Modifiers
Estimated time: 30 minutes
You are ready to continue when: You can decide whether a subtype can replace its supertype and explain when to use inheritance or composition.
You copied similar code three times. After fixing EmailChannel, you forgot to update SmsChannel. Duplicated code scales quickly; coordinated repairs somehow missed that meeting.
Inheritance allows for shared implementation, but that's not sufficient reason to use inheritance. The more important question is: when a caller expects a supertype, can a subtype adhere to the same behavioral contract?
1. extends Establishes a Subtype Relationship
First, put shared state and operations in a base class. These senders only print demonstration messages; they do not contact an email or SMS service:
import java.util.Objects;
class MessageSender {
private final String message;
MessageSender(String message) {
this.message = Objects.requireNonNull(message, "message");
if (message.isBlank()) {
throw new IllegalArgumentException("blank message");
}
}
protected final String message() {
return message;
}
protected final void logMessage() {
System.out.println("Message:" + message);
}
public void send() {
logMessage();
}
}EmailSender extends it:
class EmailSender extends MessageSender {
private final String recipient;
EmailSender(String message, String recipient) {
super(message);
if (recipient == null || recipient.isBlank()) {
throw new IllegalArgumentException("blank recipient");
}
this.recipient = recipient;
}
public String recipient() { return recipient; }
@Override
public void send() {
logMessage();
System.out.println("Demo email recipient: " + recipient);
}
}A subclass object contains fields declared by its parent as well as its own fields. Private members are not inherited by the subclass and cannot be accessed directly from its source code. The base class therefore exposes controlled queries and operations through protected final methods. Directly exposing protected mutable fields would cause all subclasses to depend on the internal representation, reducing encapsulation.
When an operation must record its send time, do not substitute the object's creation time. A production implementation can read an injected Clock inside send and pass that timestamp to the logging component. Tests can then use a fixed clock, and business code stays independent of the machine's time-zone formatting.
2. Constructors Are Not Inherited, but the Chain Must Complete
EmailSender does not inherit MessageSender(String) constructors. When creating a subclass, the initialization of the parent class must be completed first:
- Set default values for all fields of the new object.
- Execute the parent class constructor chain.
- Execute the current class field initializers and initialization blocks.
- Execute the rest of the subclass constructor after the parent constructor call.
In Java 17, the super(...) must be the first statement in the constructor. If the constructor has neither super(...) nor a this(...) call delegating to another constructor in the same class, the compiler attempts to insert super(); compilation fails if the parent class does not have an accessible parameterless constructor.
Do not call methods that can be overridden by subclasses during the parent class constructor. Dynamic dispatch may lead into the subclass implementation, while the subclass fields have not yet been fully initialized.
3. "is-a" Is Just the Starting Point
A common saying is that inheritance represents "is-a" and composition represents "has-a." But just because a concept works in natural language doesn't mean it can be safely substituted.
To determine whether EmailSender extends MessageSender is reasonable, you must check the contract of the parent type:
- Are all valid messages still valid for the subclass?
- Could the subclass strengthen the preconditions, causing messages that were previously valid to fail?
- Could the subclass weaken the promises, such as silently discarding messages while reporting success?
- Does the subclass maintain all the invariants that the parent type publicly promises?
This is the practical version of the substitution principle: any code that only relies on the contract of MessageSender should still work correctly after receiving EmailSender.
If you simply want to reuse a utility method and the object does not meet the parent type contract, use composition or delegation instead.
4. Composition Means Owning and Using a Collaborator
final class Dog {
private final java.util.List<Toy> toys = new java.util.ArrayList<>();
void addToy(Toy toy) {
toys.add(java.util.Objects.requireNonNull(toy));
}
}
final class Toy {}Declaring Dog extends ArrayList<Toy> would claim that a dog can replace a toy list. Callers could then use clear and sort, regardless of whether those operations belong in the dog's contract. Composition exposes only the operations Dog permits and allows its internal collection to change later.
Preferring composition still leaves room for inheritance. Framework extension points, stable template methods, and actual class hierarchies can still be used with inheritance, provided the parent class is designed for extension and clearly documents which methods can be overridden.
5. final Closing Unneeded Extension Points
final class MessageId {
// A value type cannot have subclasses
}
class Sender {
public final void validate() {
// Subclasses cannot replace this invariant check
}
}final class cannot be inherited, and final instance methods cannot be overridden. Do not assume that every public/protected method should be part of a permanent extension protocol; closing unnecessary extension points can reduce future compatibility burdens.
Professional Quick Reference
| Decision | Check Question |
|---|---|
| Use inheritance | Can the subclass fully adhere to the parent type's contract |
| Use composition | Is it just about having, calling, or reusing another object |
| Protected members | Is it a stable and well-documented extension point |
| Constructor chain | Does the parent class have a matching and accessible constructor |
| Final | Is it indeed necessary to allow external overriding or extension |
Before Continuing
Write MessageSender's commitment to valid input, success, and failure before checking whether EmailSender strengthens the preconditions or weakens the result. If you only need to reuse a formatter, try composition and compare the public API. Also compile a case where the parent has no accessible no-argument constructor: the subclass constructor chain must eventually make an explicit call to a matching super(...).
→ Next: Polymorphism and Dynamic Dispatch
Standard Entry: JLS §8.1.4: Direct Superclass and Subtype of a Class.