Inheritance
Sharing what is common
Suppose you write two subsystem classes and notice they are largely identical:
class Intake {
private String name;
private boolean enabled;
void enable() { enabled = true; }
void disable() { enabled = false; }
String getName() { return name; }
}
class Shooter {
private String name;
private boolean enabled;
void enable() { enabled = true; }
void disable() { enabled = false; }
String getName() { return name; }
}
Every line is duplicated. Fix a bug in one and you must remember the other.
Inheritance lets you write the shared part once and build on it:
class Subsystem {
protected String name;
protected boolean enabled;
Subsystem(String name) {
this.name = name;
}
void enable() { enabled = true; }
void disable() { enabled = false; }
String getName() { return name; }
}
class Intake extends Subsystem {
Intake() {
super("intake");
}
void collect() {
System.out.println("collecting");
}
}
Intake gets enable, disable, and getName without restating them, and adds collect of its own.
Vocabulary
| Term | Meaning |
|---|---|
| Superclass / parent | The class being extended (Subsystem) |
| Subclass / child | The class doing the extending (Intake) |
extends | Declares the relationship |
super | Refers to the parent — its constructor or its methods |
| Override | Replace an inherited method with your own version |
| Inherit | Receive a member from the parent without rewriting it |
Constructors and super
Constructors are not inherited. Each subclass declares its own, and its first job is to construct the parent portion of the object.
class Intake extends Subsystem {
private int port;
Intake(int port) {
super("intake"); // must be the first statement
this.port = port;
}
}
Rules that trip people up
super(...) must be the first statement in the constructor. Java has to finish building the parent part before the subclass touches anything.
If you omit it, Java inserts super() — a call to the parent’s no-argument constructor. If the parent has no no-argument constructor, that is a compile error, and the message (“constructor Subsystem in class Subsystem cannot be applied to given types”) is confusing until you know this rule.
A class with no extends implicitly extends Object, which is where toString, equals, and hashCode come from.
Overriding
A subclass can replace an inherited method:
class Subsystem {
void describe() {
System.out.println("subsystem: " + name);
}
}
class Intake extends Subsystem {
@Override
void describe() {
System.out.println("intake on port " + port);
}
}
Always write @Override. It is optional to the compiler but it verifies you really are overriding something — misspell the name and you have silently created a new method that nothing calls.
To extend the parent’s behaviour rather than replace it, call super:
@Override
void describe() {
super.describe(); // do the parent's version first
System.out.println(" port: " + port);
}
Which version actually runs is decided at runtime by the real object, not the declared type. That is runtime polymorphism, and it is the reason inheritance is useful rather than merely tidy.
Access levels
What a subclass can see depends on the modifier:
| Modifier | Same class | Subclass | Everywhere |
|---|---|---|---|
private | Yes | No | No |
| (no modifier) | Yes | Same package only | No |
protected | Yes | Yes | No |
public | Yes | Yes | Yes |
protected exists specifically for inheritance — visible to subclasses, hidden from unrelated code.
That said, private fields with protected or public accessor methods is usually the better design. A protected field becomes part of the contract with every subclass, and changing it later breaks all of them.
What is not inherited
Three things behave differently from what you might expect:
Constructors are not inherited, as above.
Fields are hidden, not overridden. Declaring a field with the same name in a subclass does not replace the parent’s — both exist, and which one you see depends on the declared type:
class Parent { String label = "parent"; }
class Child extends Parent { String label = "child"; }
Parent p = new Child();
System.out.println(p.label); // "parent", not "child"
This is field hiding and it is almost always a mistake. Keep fields private and use methods, which do override properly.
Static methods are hidden, not overridden, and are resolved by declared type at compile time.
Java has single inheritance
A class may extend exactly one class:
class Intake extends Subsystem, Loggable { } // does not compile
This avoids the ambiguity that arises when two parents define the same method differently. When you need a class to fill several roles, use interfaces — a class can implement any number of those.
When inheritance is the wrong tool
Inheritance is often reached for too quickly. The test is whether the subclass genuinely is a kind of the parent, in every context where the parent is used.
class Drivetrain extends Subsystem { } // a drivetrain IS a subsystem — fine
class Robot extends Drivetrain { } // a robot is not a drivetrain — wrong
The second should be composition — a Robot has a Drivetrain:
class Robot {
private final Drivetrain drivetrain; // has-a, not is-a
Robot(Drivetrain drivetrain) {
this.drivetrain = drivetrain;
}
}
Prefer composition when you are unsure
Inheritance permanently couples the subclass to the parent’s implementation. A change in the parent can break every subclass, sometimes subtly.
Composition — holding an object as a field — is looser, easier to test, and easier to change. Reach for inheritance when there is a genuine is-a relationship and real behaviour to share. Reach for composition otherwise. See Dependency Injection.
Preventing inheritance
final on a class stops anyone extending it; on a method it stops overriding:
final class Constants { } // cannot be extended
class Base {
final void criticalStep() { } // cannot be overridden
}
Useful when a class’s correctness depends on behaviour that a subclass must not change.
Common mistakes
- Omitting
@Overrideand silently creating a new method. super(...)not first in the constructor.- Expecting a parent’s no-argument constructor to exist when it defines only a parameterised one.
- Hiding a field and expecting it to override.
- Deep hierarchies. More than two or three levels becomes hard to follow; prefer composition.
- Inheriting for code reuse alone, with no is-a relationship.
- Calling an overridable method from a constructor. The subclass’s version runs before its fields are initialised, so it sees zeros and nulls.
Practice
- Write a
Subsystembase class with a name and enable/disable behaviour, then two subclasses that add their own methods. - Add a
describe()method to the parent and override it in one subclass, callingsuper.describe()inside. - Demonstrate field hiding, then fix it by making the field private with a getter.
- Give the parent only a parameterised constructor, then write a subclass constructor that omits
super(...). Read the compile error. - Take a class that extends another purely for code reuse and rewrite it using composition.
Hints
protectedfields, or private with accessors.- Put
super.describe()first, then add the extra line. - Same field name in both, assign a
Childto aParentvariable. - The error is worth seeing once so you recognise it later.
- Replace
extends Xwith a private field of typeX, and forward the calls you actually need.