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 inserts super(), which only compiles when the parent has a no-argument constructor.
  • Private members stay private. SmartMeter cannot read city; it can read id, which is protected (visible to subclasses and the same package, see access modifiers).
  • One parent only. A class extends at most one class. Without extends, it extends java.lang.Object, which is where toString, equals and hashCode come 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.

Class hierarchy: Object at the top; the abstract class Tariff extends Object and declares price; FixedTariff extends Tariff and implements price; GreenTariff extends FixedTariff and overrides price. A call t.price(350) on a GreenTariff object starts at GreenTariff. Object Tariff {abstract} FixedTariff GreenTariff price(int) declared, no body price(int) implemented price(int) overridden, calls super t.price(350) on a GreenTariff object runs GreenTariff.price
The hollow arrowheads mean "extends". For a 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.