Inheritance with extends
Make one class a specialised version of another with extends, and know exactly what a subclass gets, what it does not, and when to stop.
class Subclass extends Superclass {
Subclass(...) { super(...); }
// extra fields and methods
}class Admin extends User says an Admin is a User with something extra. The subclass gets the superclass's accessible fields and methods without repeating them, and an Admin can be passed anywhere a User is expected.
What a subclass gets
- public and protected members are inherited and usable.
- package-private members (no modifier) are usable only when both classes are in the same package.
- private fields still exist inside every subclass object, but the subclass cannot touch them by name. It goes through the superclass's methods.
- Constructors are not inherited. A subclass writes its own and calls one of the superclass's with
super(...).
Java has single inheritance for classes: one extends, at most. A class with no extends clause extends java.lang.Object, which is where toString, equals and hashCode come from. A class marked final cannot be extended at all; String is final, and so is every record.
class BaseEntity {
private final long id;
BaseEntity(long id) { this.id = id; }
long id() { return id; }
}
class Order extends BaseEntity {
Order(long id) { super(id); }
}Where you meet it in back-end code
Exception hierarchies are the most common good use: an ApiException carrying an HTTP status, with NotFoundException and ConflictException under it, lets one handler catch them all. Frameworks also ask you to extend their base classes now and then, and JDK skeletons such as AbstractList exist to be extended.
Mistakes people make
- Extending a class just to reuse a few of its methods when the subclass is not really a kind of it.
class UserList extends ArrayList<User>exposes every list method, including ones that break your rules. Hold a list in a field instead. - Deep chains (
BaseEntitytoAuditedEntitytoSoftDeletableAuditedEntity...). Every level is something a reader must open to understand one class. protectedfields. They let every subclass, in any package, change state the superclass relies on. Keep fields private and expose methods.
Use extends when the is-a relationship is real and stable. When it is only about sharing code, an interface plus composition is usually the better fit.
Example
When to use it
- Give NotFoundException and ConflictException a shared ApiException parent so one handler turns them into HTTP status codes.
- Share id and creation timestamp handling between entity classes that really are kinds of the same thing.
- Extend a JDK skeleton class such as AbstractList to get a working collection by writing only two methods.
More examples
An exception hierarchy for API errors
Each subclass passes its fixed status up to the ApiException constructor, so the handler only needs to catch the base type. This is the shape most web frameworks expect when they map exceptions to HTTP responses.
class ApiException extends RuntimeException {
private final int status;
ApiException(int status, String message) {
super(message);
this.status = status;
}
int status() { return status; }
}
class NotFoundException extends ApiException {
NotFoundException(String what) { super(404, what + " not found"); }
}
class ConflictException extends ApiException {
ConflictException(String message) { super(409, message); }
}
void handle(String path) {
try {
if (path.equals("/users/7")) throw new NotFoundException("user 7");
if (path.equals("/users")) throw new ConflictException("email already registered");
IO.println("200 OK");
} catch (ApiException e) {
// One catch covers every subclass.
IO.println(e.status() + " " + e.getMessage());
}
}
void main() {
handle("/users/7");
handle("/users");
handle("/health");
}
// Output:
// 404 user 7 not found
// 409 email already registered
// 200 OKEvery class ends at Object
User has no extends clause, so the compiler makes it extend Object. That is why every object, including your own, has toString, equals and hashCode available.
class User { }
class Admin extends User { }
void main() {
// getSuperclass() walks one level up the chain.
Class<?> type = Admin.class;
while (type != null) {
IO.println(type.getSimpleName());
type = type.getSuperclass();
}
Object anything = new Admin();
IO.println(anything instanceof User);
}
// Output:
// Admin
// User
// Object
// truePrivate state stays behind the superclass's methods
The subclass reuses Account's rules by calling deposit() rather than editing the field. If Account later adds logging or an overdraft check, SavingsAccount gets it for free. The zero check matters because deposit() rejects 0 and small balances round down to no interest.
class Account {
private long balanceCents;
void deposit(long cents) {
if (cents <= 0) throw new IllegalArgumentException("deposit must be positive");
balanceCents += cents;
}
long balanceCents() { return balanceCents; }
}
class SavingsAccount extends Account {
// Cannot write balanceCents += ... here: the field is private to Account.
void addInterest(int basisPoints) {
long interest = balanceCents() * basisPoints / 10_000;
if (interest > 0) deposit(interest); // small balances round to 0
}
}
void main() {
SavingsAccount account = new SavingsAccount();
account.deposit(100_000);
account.addInterest(250);
IO.println(account.balanceCents());
}
// Output:
// 102500
Discussion