JAVA / ABSTRACT CLASSES AND INTERFACES
Abstract classes and deferred implementation
Write an abstract base class that carries shared state and constructors while deferring specific decisions, and know when a subclass must stay abstract.
What you will learn
- Mark a class abstract to share fields and constructors that no one can instantiate
- Initialise an abstract class's fields from a subclass with super(...)
- Declare a partly finished subclass abstract instead of stubbing missing methods
- Program against the abstract type so new subclasses drop in without call-site edits
Understanding Abstract classes and deferred implementation
The abstract modifier on a class is a refusal rather than a feature. Writing abstract class Ledger tells the compiler that new Ledger(...) is an error no matter how complete the class looks, because the author has declared the concept unfinished. A method marked abstract has a signature and no body, so there is literally no code to dispatch to, and the compiler will not create an object whose method table has holes; it blocks instantiation once at the class level instead of failing later at each call.
Everything else about the class survives: fields, initialisers, constructors, concrete methods and static members. The mental model that keeps this straight is one object with layers, because new Checking(10000) allocates a single object and super("checking", 10000) runs Ledger's constructor to initialise the fields Checking inherited. That is why an abstract class needs a constructor even though nobody can call new on it, and why protected is the honest access level for it: its job is to initialise the part of the subclass instance the abstract class owns, and only a subclass can reach it.
Completeness is judged per class, not per hierarchy. A class is concrete only if every abstract method it inherits has a body somewhere in its own chain, so a subclass that fills in two of three must be declared abstract itself, which is a perfectly normal middle layer that shares more than the root but still defers one decision. Notice the asymmetry: a single abstract method forces its class to be abstract, but an abstract class is free to declare none at all, because the class-level modifier is purely about instantiability.
abstract class Ledger {
private final String name;
private long cents;
protected Ledger(String name, long openingCents) {
this.name = name;
this.cents = openingCents;
System.out.println("Ledger constructor ran for " + name);
}
// deferred: every ledger charges differently
protected abstract long feeCents();
public void closeMonth() {
cents -= feeCents();
System.out.println(name + " after fee: " + cents);
}
public long balanceCents() {
return cents;
}
}
class Checking extends Ledger {
Checking(long opening) {
super("checking", opening);
}
@Override
protected long feeCents() {
return 500;
}
}
class Savings extends Ledger {
private final long minimum;
Savings(long opening, long minimum) {
super("savings", opening);
this.minimum = minimum;
}
@Override
protected long feeCents() {
return balanceCents() >= minimum ? 0 : 200;
}
}
public class Main {
public static void main(String[] args) {
Ledger[] books = { new Checking(10000), new Savings(3000, 5000) };
for (Ledger book : books) {
book.closeMonth();
}
// Ledger plain = new Ledger("plain", 0); // Ledger is abstract; cannot be instantiated
System.out.println("checking balance now " + books[0].balanceCents());
}
}
An abstract class is a real type with real fields and constructors that names the decisions it refuses to make, and the compiler forbids new on it until a subclass supplies every one of them.
Worked examples
Abstract with zero abstract methods
Shows that the class-level modifier alone blocks new, and that new Shape() { ... } builds a subclass instead.
abstract class Shape {
// no abstract methods at all
public double area() {
return 0.0;
}
public String describe() {
return "area " + area();
}
}
class UnitSquare extends Shape {
@Override
public double area() {
return 1.0;
}
}
public class Main {
public static void main(String[] args) {
Shape square = new UnitSquare();
System.out.println(square.describe());
Shape wedge = new Shape() { // anonymous subclass, not a Shape object
@Override
public double area() {
return 2.5;
}
};
System.out.println(wedge.describe());
System.out.println("wedge's class extends "
+ wedge.getClass().getSuperclass().getSimpleName());
}
}
Example explained
Line 1Shape has a body for every method, yet a bare new Shape() would still be rejected: abstract on the class removes direct instantiation on its own.
Line 2new Shape() { ... } compiles because the braces declare an anonymous subclass, and it is that subclass which gets instantiated.
Line 3getSuperclass().getSimpleName() prints Shape, confirming the abstract class sits one level above the object that actually exists.
Line 4describe() lives in the abstract class but calls area() on the runtime class, so the anonymous override's 2.5 is what gets printed.
A middle class that stays abstract
Shows a subclass that supplies only one of two inherited abstract methods and must therefore be declared abstract itself.
import java.lang.reflect.Modifier;
abstract class Importer {
abstract String read();
abstract void write(String data);
}
abstract class CsvImporter extends Importer {
@Override
String read() {
return "id,name";
}
// write(String) is still bodyless, so this class cannot be concrete
}
class ConsoleCsvImporter extends CsvImporter {
@Override
void write(String data) {
System.out.println("out: " + data);
}
}
public class Main {
public static void main(String[] args) {
ConsoleCsvImporter importer = new ConsoleCsvImporter();
importer.write(importer.read());
System.out.println("CsvImporter abstract? "
+ Modifier.isAbstract(CsvImporter.class.getModifiers()));
System.out.println("ConsoleCsvImporter abstract? "
+ Modifier.isAbstract(ConsoleCsvImporter.class.getModifiers()));
}
}
Example explained
Line 1Importer declares two bodyless methods, so the class itself has no choice about being abstract.
Line 2CsvImporter overrides read() only; write(String) is inherited without a body, so declaring the class abstract is the only way the file compiles.
Line 3ConsoleCsvImporter adds the last missing body, which is exactly what makes new legal for it and not for its parent.
Line 4Modifier.isAbstract shows the flag surviving into the runtime class, one class at a time, rather than being a property of the whole hierarchy.
Important notes
abstract cannot be combined with final, private or static on a method, because each of those prevents the subclass override that an abstract method exists to require.
An abstract class is usable as a variable, parameter, array and return type; you simply never encounter an object whose runtime class is the abstract one.
Common mistakes
Reading "Ledger is abstract; cannot be instantiated" as an access problem and making the constructor public: the class still cannot be instantiated, and the only thing that ever reaches that constructor is super(...) from a subclass.
Silencing the "must be declared abstract or implement feeCents()" error with a stub like return 0 instead of declaring the half-finished subclass abstract, which turns a compile error into a ledger that quietly charges no fee.
Calling an abstract method from the abstract class's constructor: it dispatches to the subclass override while the subclass fields are still 0 or null, so Savings would read minimum as 0 and pick the wrong fee.
Try it yourself
Change, predict, then run
Add protected abstract int statementDay(); to the Ledger example and compile to read the two errors it produces. Fix Checking by overriding it, but fix Savings by declaring Savings abstract and adding a concrete PremiumSavings that returns 1.
Open the Java workspaceCheck your understanding
Ledger is abstract and has a protected constructor. Since new Ledger(...) is rejected by the compiler, when does that constructor body actually run?
- Every time a concrete subclass is instantiated, before the subclass constructor body finishes, because super(...) invokes it
- Never; constructors in abstract classes are discarded and serve only as documentation of the intended fields
- Once per program, when the Ledger class is loaded and initialised by the JVM
- Only when a subclass or reflection deliberately bypasses the abstract check on the class
Show answer
A Checking object contains Ledger's fields, so Ledger's constructor must run to initialise them; super("checking", opening) is the call that does it, which is why the constructor is needed even though new Ledger(...) is illegal. Option 3 confuses instance construction with class initialisation: static initialisers run once at class load, but a constructor runs once per object.