Abstract Classes

Write a partial class that fixes the shared steps and leaves the rest to subclasses, and decide when that beats an interface.

Syntaxabstract class Base { abstract Result step(Input in); final Result run(Input in) { return step(in); } } class Impl extends Base { @Override Result step(Input in) { ... } }

An abstract class is a class you cannot instantiate. It exists to be extended. It can declare abstract methods, which have no body, alongside ordinary fields, constructors and finished methods. A concrete subclass must implement every abstract method, or be declared abstract itself.

abstract class ImportJob {
    final String run(String input) {        // fixed steps
        List<String> rows = parse(input);
        return "imported " + save(rows);
    }
    protected abstract List<String> parse(String input);
    protected abstract int save(List<String> rows);
}

The template method pattern

The example above is the main reason to write an abstract class. The superclass owns the order of the steps and the parts that never change (timing, logging, error handling, transactions). Subclasses fill in only the parts that differ. Marking the outer method final stops a subclass from skipping a step by accident. Optional steps can be ordinary protected methods with an empty body, known as hooks.

Abstract class or interface?

Abstract classInterface
Instance fields and constructorsYesNo (constants only)
How many per classOne superclassMany
Usable as a lambdaNoYes, with one abstract method

Define the type as an interface, because callers and tests should depend on that. Add an abstract class next to it only when several implementations share real state or a fixed algorithm. The JDK does exactly this: List is the interface, AbstractList is a skeleton that implements most of it once you supply get and size.

Back-end uses

Base request handlers that wrap every call in the same error handling, importers that read, validate and save in a fixed order, and report exporters that share headers and footers are all good fits. Jakarta's HttpServlet is an abstract class you extend and override doGet or doPost on.

Mistakes people make

  • Making a class abstract only to stop people creating it. For a utility class, a private constructor says that more clearly.
  • A base class that keeps growing helpers every subclass inherits whether it needs them or not.
  • Calling abstract methods from the abstract class's constructor, which runs subclass code before the subclass is initialised.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Give every HTTP handler the same exception-to-status mapping and request metrics through one final handle method.
  • Run CSV, JSON and XML imports through the same parse, validate and save sequence with only parsing differing.
  • Expose a database result page as a read-only List by extending AbstractList instead of copying rows.

More examples

A base handler with shared error handling

Every handler gets the same mapping from exceptions to status codes and the same thread-safe request counter without repeating them. The 500 branch also keeps internal messages such as 'database down' out of the response.

Example · java
abstract class BaseHandler {
    private final AtomicInteger requests = new AtomicInteger();   // shared by request threads

    final String handle(String path) {
        requests.incrementAndGet();
        try {
            return "200 " + doHandle(path);
        } catch (IllegalArgumentException e) {
            return "400 " + e.getMessage();
        } catch (RuntimeException e) {
            // Log e here; never send its message to the client.
            return "500 internal error";
        }
    }

    int requests() { return requests.get(); }

    protected abstract String doHandle(String path);
}

class UserHandler extends BaseHandler {
    @Override
    protected String doHandle(String path) {
        String id = path.substring("/users/".length());
        if (!id.chars().allMatch(Character::isDigit)) {
            throw new IllegalArgumentException("id must be numeric");
        }
        if (id.equals("0")) throw new IllegalStateException("database down");
        return "{\"id\":" + id + "}";
    }
}

void main() {
    UserHandler handler = new UserHandler();
    IO.println(handler.handle("/users/42"));
    IO.println(handler.handle("/users/abc"));
    IO.println(handler.handle("/users/0"));
    IO.println("requests: " + handler.requests());
}

// Output:
// 200 {"id":42}
// 400 id must be numeric
// 500 internal error
// requests: 3

A full List from AbstractList

Two methods give you a read-only List that works with streams, for-each and every API that takes a List. The skeleton classes in java.util are the textbook case of an abstract class sitting next to an interface.

Example · java
class Range extends AbstractList<Integer> {
    private final int start, end;   // end is exclusive

    Range(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer get(int index) {
        Objects.checkIndex(index, size());
        return start + index;
    }

    @Override
    public int size() { return end - start; }
}

void main() {
    List<Integer> pages = new Range(1, 6);
    // contains, indexOf, subList, toString and iteration all come from AbstractList.
    IO.println(pages);
    IO.println(pages.contains(4) + " " + pages.indexOf(4));
    IO.println(pages.subList(1, 3));
}

// Output:
// [1, 2, 3, 4, 5]
// true 3
// [2, 3]

Optional hook methods

Abstract methods are steps every subclass must supply; hooks are steps a subclass may supply. Keeping the hooks empty, rather than abstract, saves every simple subclass from writing no-op methods.

Example · java
abstract class PaymentProcessor {
    final String charge(long cents) {
        beforeCharge(cents);
        String ref = doCharge(cents);
        afterCharge(ref);
        return ref;
    }

    // Hooks: empty by default, overridden only when needed.
    protected void beforeCharge(long cents) { }
    protected void afterCharge(String ref) { }

    protected abstract String doCharge(long cents);
}

class CardProcessor extends PaymentProcessor {
    @Override
    protected String doCharge(long cents) { return "card-" + cents; }
}

class AuditedCardProcessor extends CardProcessor {
    @Override
    protected void beforeCharge(long cents) { IO.println("audit: charging " + cents); }

    @Override
    protected void afterCharge(String ref) { IO.println("audit: done " + ref); }
}

void main() {
    IO.println(new CardProcessor().charge(1200));
    IO.println(new AuditedCardProcessor().charge(800));
}

// Output:
// card-1200
// audit: charging 800
// audit: done card-800
// card-800

Discussion

  • Be the first to comment on this lesson.