Inheritance, Interfaces and Polymorphism
Building type hierarchies in Java: extends and super, overriding with @Override, abstract classes, interfaces with default methods, and polymorphism through dynamic dispatch.
Extending a class
Inheritance
lets a new class reuse and specialize an existing one; the general
idea is on
Object-Oriented Concepts. In Java, the keyword
extends after the class
name makes the new class (the subclass) inherit all fields and
methods of an existing class (the superclass). A smart meter is a
meter, with an IP address on top.
public class Main {
public static void main(String[] args) {
SmartMeter m = new SmartMeter("E-001", "Delft", "10.0.4.17");
System.out.println(m.describe());
System.out.println(m.ipAddress());
System.out.println(m instanceof Meter);
}
}
class Meter {
protected final String id;
private final String city;
Meter(String id, String city) {
this.id = id;
this.city = city;
}
String describe() {
return id + " in " + city;
}
}
class SmartMeter extends Meter {
private final String ipAddress;
SmartMeter(String id, String city, String ipAddress) {
super(id, city);
this.ipAddress = ipAddress;
}
String ipAddress() {
return id + " at " + ipAddress;
}
}
E-001 in Delft E-001 at 10.0.4.17 true
A few rules show up in this small example:
-
Constructors are not inherited. The subclass
writes its own and passes the parent's part on with
super(...). If you leave that call out, the compiler insertssuper(), which only compiles when the parent has a no-argument constructor. -
Private members stay private.
SmartMetercannot readcity; it can readid, which isprotected(visible to subclasses and the same package, see access modifiers). -
One parent only. A class extends at most one
class. Without
extends, it extendsjava.lang.Object, which is wheretoString,equalsandhashCodecome from.
Overriding methods
A subclass overrides a method by declaring one with the same name
and parameter types. In Java every non-static, non-private,
non-final method can be overridden; there is no
virtual keyword as in C#. Inside the override,
super.describe() calls the parent's version, so you
extend its behavior instead of copying it.
public class Main {
public static void main(String[] args) {
Meter plain = new Meter("E-001", "Delft");
Meter smart = new SmartMeter("E-002", "Leiden", "10.0.4.17");
System.out.println(plain.describe());
System.out.println(smart.describe());
}
}
class Meter {
private final String id;
private final String city;
Meter(String id, String city) {
this.id = id;
this.city = city;
}
String describe() {
return id + " in " + city;
}
}
class SmartMeter extends Meter {
private final String ipAddress;
SmartMeter(String id, String city, String ipAddress) {
super(id, city);
this.ipAddress = ipAddress;
}
@Override
String describe() {
return super.describe() + " (smart, " + ipAddress + ")";
}
}
E-001 in Delft E-002 in Leiden (smart, 10.0.4.17)
The variable smart is declared as a Meter,
yet the smart meter's own describe() runs. That is
polymorphism, explained in
Dynamic dispatch below.
The @Override annotation
An annotation is metadata written
with @ in front of a declaration. It does not change
what the code does by itself; the compiler, tools or frameworks read
it. The JDK defines a few, such as @Deprecated and
@FunctionalInterface; libraries define many more, such
as JUnit's @Test (see
Testing and Code Quality).
@Override is the one
you will write most. It tells the compiler "this method is meant to
override or implement a method from a supertype", and the compiler
refuses the code when it does not. Without it, a typo silently
creates a new method, and the parent's version keeps running:
public class Main {
public static void main(String[] args) {
Meter smart = new SmartMeter("E-002", "Leiden");
System.out.println(smart.describe());
}
}
class Meter {
final String id;
final String city;
Meter(String id, String city) {
this.id = id;
this.city = city;
}
String describe() {
return id + " in " + city;
}
}
class SmartMeter extends Meter {
SmartMeter(String id, String city) {
super(id, city);
}
@Override
String descibe() {
return id + " (smart)";
}
}
Main.java:27: error: method does not override or implement a method from a supertype
An override may not be less accessible than the method it replaces, and its return type must be the same or a subtype (a covariant return type). Static methods are never overridden; see what is not polymorphic.
Abstract classes and methods
Sometimes the parent class is only a common shape: there is no such
thing as "a tariff" without knowing which kind. An
abstract class, marked abstract, can hold fields, constructors and
finished methods, but it cannot be instantiated. An
abstract method is a method
declared with abstract and without a body: the
signature only. Every concrete (non-abstract) subclass must
implement it.
import java.math.BigDecimal;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Tariff> tariffs = List.of(
new FixedTariff(new BigDecimal("0.30")),
new GreenTariff(new BigDecimal("0.30")),
new FixedTariff(new BigDecimal("0.27")));
for (Tariff t : tariffs) {
System.out.println(t.name() + ": " + t.price(350));
}
}
}
abstract class Tariff {
abstract BigDecimal price(int kwh);
String name() {
return getClass().getSimpleName();
}
}
class FixedTariff extends Tariff {
private final BigDecimal perKwh;
FixedTariff(BigDecimal perKwh) {
this.perKwh = perKwh;
}
@Override
BigDecimal price(int kwh) {
return perKwh.multiply(BigDecimal.valueOf(kwh));
}
}
class GreenTariff extends FixedTariff {
private static final BigDecimal SURCHARGE = new BigDecimal("5.00");
GreenTariff(BigDecimal perKwh) {
super(perKwh);
}
@Override
BigDecimal price(int kwh) {
return super.price(kwh).add(SURCHARGE);
}
}
FixedTariff: 105.00 GreenTariff: 110.00 FixedTariff: 94.50
name() is written once in Tariff and used
by all subclasses; getClass() returns the actual class
of the object, so it prints the subclass name. The compiler enforces
both rules of abstract classes:
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Tariff t = new Tariff();
System.out.println(t.price(350));
}
}
abstract class Tariff {
abstract BigDecimal price(int kwh);
}
Main.java:5: error: Tariff is abstract; cannot be instantiated
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
Tariff t = new DayNightTariff(new BigDecimal("0.34"), new BigDecimal("0.22"));
System.out.println(t.price(350));
}
}
abstract class Tariff {
abstract BigDecimal price(int kwh);
}
class DayNightTariff extends Tariff {
private final BigDecimal day;
private final BigDecimal night;
DayNightTariff(BigDecimal day, BigDecimal night) {
this.day = day;
this.night = night;
}
}
Main.java:14: error: DayNightTariff is not abstract and does not override abstract method price(int) in Tariff
Dynamic dispatch
In the tariff loop, the variable t has the declared
type Tariff, but t.price(350) ran three
different methods. That works through
dynamic dispatch: for an
instance method call, the JVM looks at the class of the actual
object at run time and runs the most specific override it finds,
walking up the hierarchy if the class does not override the method
itself. This is how Java implements
polymorphism: code written against Tariff works for every
subclass, including ones written later.
GreenTariff object, the call goes to the lowest
class: the declared type of the variable only decides which
methods may be called, not which version runs.
public class Main {
public static void main(String[] args) {
Meter m = new SmartMeter();
System.out.println(m.report(5));
System.out.println(m.report(5L));
System.out.println(m.report(Integer.valueOf(5)));
}
}
class Meter {
String report(int kwh) { return "Meter.report(int)"; }
String report(Object kwh) { return "Meter.report(Object)"; }
}
class SmartMeter extends Meter {
@Override
String report(int kwh) { return "SmartMeter.report(int)"; }
String report(long kwh) { return "SmartMeter.report(long)"; }
}
SmartMeter.report(int) Meter.report(Object) Meter.report(Object)
What is not polymorphic
Dynamic dispatch applies to instance methods only. Fields and static methods are looked up from the declared type at compile time. A subclass that declares a field or static method with the same name hides the parent's one instead of overriding it:
public class Main {
public static void main(String[] args) {
Meter m = new SmartMeter();
System.out.println(m.describe());
System.out.println(m.kind);
System.out.println(m.category());
System.out.println(Meter.category());
System.out.println(SmartMeter.category());
}
}
class Meter {
String kind = "meter";
String describe() {
return "a meter";
}
static String category() {
return "Meter";
}
}
class SmartMeter extends Meter {
String kind = "smart meter";
@Override
String describe() {
return "a smart meter";
}
static String category() {
return "SmartMeter";
}
}
a smart meter meter Meter Meter SmartMeter
The object is a SmartMeter in every line, yet
m.kind and m.category() use
Meter's versions. Calling a static method through a
variable compiles but misleads the reader; always call it through
the class name. And never declare a field with the same name as one
in the parent: you get two fields in one object.
Interfaces
An
interface
is a named set of methods that a class promises to provide. A class
lists its interfaces with implements and must then
implement every abstract method. Interfaces describe a
capability ("can send a notification", "can be compared")
rather than what something is, so unrelated classes can
share one.
A default method is an
interface method with a body, marked default (Java 8).
Implementing classes inherit it and may override it. Default methods
were added so that existing interfaces, such as the collection
interfaces, could gain new methods without breaking every class that
already implemented them.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Notifier> notifiers = List.of(
new EmailNotifier("mei.chen@example.com"),
new SmsNotifier("+31 6 12345678"));
for (Notifier n : notifiers) {
n.send("INC-40112 resolved");
System.out.println(" via " + n.channel());
}
}
}
interface Notifier {
void send(String message);
default String channel() {
return "unknown";
}
}
class EmailNotifier implements Notifier {
private final String address;
EmailNotifier(String address) {
this.address = address;
}
@Override
public void send(String message) {
System.out.println("mail to " + address + ": " + message);
}
@Override
public String channel() {
return "email";
}
}
class SmsNotifier implements Notifier {
private final String number;
SmsNotifier(String number) {
this.number = number;
}
@Override
public void send(String message) {
System.out.println("sms to " + number + ": " + message);
}
}
mail to mei.chen@example.com: INC-40112 resolved via email sms to +31 6 12345678: INC-40112 resolved via unknown
Methods in an interface are public unless marked
private, even without the keyword. The implementing
method must therefore be public too; forgetting it is a
frequent compile error:
public class Main {
public static void main(String[] args) {
Notifier n = new EmailNotifier();
n.send("INC-40112 resolved");
}
}
interface Notifier {
void send(String message);
}
class EmailNotifier implements Notifier {
void send(String message) {
System.out.println("mail: " + message);
}
}
Main.java:13: error: send(String) in EmailNotifier cannot implement send(String) in Notifier
Several interfaces and static methods
A class can implement several interfaces, and a variable can have an
interface type: code that only needs readings asks for a
Measurable and does not care what else the object can
do. Interfaces can also hold static methods (called
through the interface name) and constants, which are implicitly
public static final. Since Java 9, an interface can
have private helper methods for its default methods.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
SmartMeter m = new SmartMeter("E-007", 18745);
m.addAlert("tamper detected");
System.out.println(m.id() + " " + m.kwh() + " kWh, alerts: " + m.alerts());
Measurable r = m;
Alerting a = m;
System.out.println(r.kwh() + " / " + a.alerts().size());
System.out.println(Measurable.unit());
}
}
interface Measurable {
String id();
int kwh();
static String unit() {
return "kWh";
}
}
interface Alerting {
List<String> alerts();
void addAlert(String text);
}
class SmartMeter implements Measurable, Alerting {
private final String id;
private final int kwh;
private final List<String> alerts = new ArrayList<>();
SmartMeter(String id, int kwh) {
this.id = id;
this.kwh = kwh;
}
@Override public String id() { return id; }
@Override public int kwh() { return kwh; }
@Override public List<String> alerts() { return List.copyOf(alerts); }
@Override public void addAlert(String text) { alerts.add(text); }
}
E-007 18745 kWh, alerts: [tamper detected] 18745 / 1 kWh
When two interfaces bring a default method with the same signature,
the class must decide. The compiler reports
types Emailing and Texting are incompatible until the
class overrides the method; inside the override,
Emailing.super.channel() calls one specific default:
public class Main {
public static void main(String[] args) {
System.out.println(new SmartMeter().channel());
}
}
interface Emailing {
default String channel() { return "email"; }
}
interface Texting {
default String channel() { return "sms"; }
}
class SmartMeter implements Emailing, Texting {
@Override
public String channel() {
return Emailing.super.channel() + " and " + Texting.super.channel();
}
}
email and sms
Abstract class or interface?
| Feature | Abstract class | Interface |
|---|---|---|
| A class can have | One superclass | Any number |
| Instance fields (state) | Yes | No, only constants |
| Constructors | Yes | No |
| Method bodies | Yes, any access level |
default, static and
private methods
|
| Expresses | "is a kind of" with shared code | "can do" a capability |
Start with an interface: it keeps the most options open, and any
class can implement it. Add an abstract class when several
implementations really share state and code, often as a base class
that implements the interface. The JDK does exactly that:
List is an interface, AbstractList a
helper base class, and ArrayList a concrete class.
Casting and instanceof
Assigning a subclass object to a superclass variable (an
upcast) is always safe and needs no syntax. Going the other
way, a downcast such as (SmartMeter) meter, is
checked at run time and throws ClassCastException when
the object is not of that class. Check first with
instanceof; since Java 16 the pattern form tests and
casts in one step (see
Control Flow and Switch Expressions):
public class Main {
public static void main(String[] args) {
Meter[] meters = { new SmartMeter("E-002", "10.0.4.17"), new Meter("E-001") };
for (Meter m : meters) {
if (m instanceof SmartMeter s) {
System.out.println(m.id + " is smart: " + s.ipAddress);
} else {
System.out.println(m.id + " is read by hand");
}
}
try {
SmartMeter wrong = (SmartMeter) meters[1];
} catch (ClassCastException e) {
System.out.println("cast failed: " + meters[1].id + " is not a SmartMeter");
}
}
}
class Meter {
final String id;
Meter(String id) {
this.id = id;
}
}
class SmartMeter extends Meter {
final String ipAddress;
SmartMeter(String id, String ipAddress) {
super(id);
this.ipAddress = ipAddress;
}
}
E-002 is smart: 10.0.4.17 E-001 is read by hand cast failed: E-001 is not a SmartMeter
Many instanceof checks on your own types are a design
smell: usually a method on the superclass, overridden in each
subclass, says the same thing more clearly. The exception is a
closed set of variants, where a
sealed type
and a pattern-matching switch are the better tool.
final, sealed and constructors
final on a method forbids overriding it; on a class it
forbids subclassing it altogether. String and the
wrapper classes such as Integer are final, so nobody
can change how they behave. Records are implicitly final, and so are
enums (an enum whose constants have their own bodies is implicitly
sealed). Use final for methods whose behavior other
code relies on, and for classes that were not designed to be
extended.
public class Main {
public static void main(String[] args) {
System.out.println(new CheapTariff().price(100));
}
}
class Tariff {
final double price(int kwh) {
return kwh * 0.30;
}
}
class CheapTariff extends Tariff {
@Override
double price(int kwh) {
return 0;
}
}
Main.java:15: error: price(int) in CheapTariff cannot override price(int) in Tariff
Between "anyone may extend" and final sits
sealed, which allows only the subclasses you list; it
is explained on
Records, Enums and Sealed Types.
Code before super(...)
Until Java 24, super(...) had to be the very first
statement of a constructor, so a subclass could not check or clean
up an argument before passing it on without a helper method. Java 25
finalized flexible constructor bodies (JEP 513): statements
may come before super(...), as long as they do not use
the object under construction (no calls to its methods, no reading
its fields); assigning its own fields is allowed.
public class Main {
public static void main(String[] args) {
System.out.println(new SmartMeter(" e-001 ").id);
new SmartMeter(" ");
}
}
class Meter {
final String id;
Meter(String id) {
this.id = id;
}
}
class SmartMeter extends Meter {
SmartMeter(String rawId) {
String clean = rawId.strip().toUpperCase();
if (clean.isEmpty()) {
throw new IllegalArgumentException("meter id is empty");
}
super(clean);
}
}
E-001 Exception in thread "main" java.lang.IllegalArgumentException: meter id is empty
Composition over inheritance
Inheritance couples a subclass tightly to its parent: a change in the parent can break every subclass, and a class can only have one parent. The usual advice, explained on Object-Oriented Concepts, is to prefer composition: give a class a field of an interface type and delegate to it. A meter does not need to be a notifier; it needs to have one.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Meter delft = new Meter("E-001", new EmailNotifier("mei.chen@example.com"));
delft.record(18412);
delft.record(18100);
System.out.println(delft.readings());
}
}
interface Notifier {
void send(String message);
}
class EmailNotifier implements Notifier {
private final String address;
EmailNotifier(String address) {
this.address = address;
}
@Override
public void send(String message) {
System.out.println("mail to " + address + ": " + message);
}
}
class Meter {
private final String id;
private final Notifier notifier;
private final List<Integer> readings = new ArrayList<>();
Meter(String id, Notifier notifier) {
this.id = id;
this.notifier = notifier;
}
void record(int kwh) {
if (!readings.isEmpty() && kwh < readings.getLast()) {
notifier.send(id + ": reading " + kwh + " is lower than " + readings.getLast());
}
readings.add(kwh);
}
List<Integer> readings() {
return List.copyOf(readings);
}
}
mail to mei.chen@example.com: E-001: reading 18100 is lower than 18412 [18412, 18100]
Swapping email for SMS now means passing a different object, not
writing a new subclass of Meter. The same structure
makes the class easy to test: a test passes a spy
Notifier that records the messages (see
Testing and Code Quality). Frameworks such as Spring are built around this idea, called
dependency injection.