Abstract Classes
Write a partial class that fixes the shared steps and leaves the rest to subclasses, and decide when that beats an interface.
abstract 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 class | Interface | |
|---|---|---|
| Instance fields and constructors | Yes | No (constants only) |
| How many per class | One superclass | Many |
| Usable as a lambda | No | Yes, 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
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.
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: 3A 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.
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.
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