Abstract classes: when and when not
An abstract class is a class you cannot instantiate, which exists to be extended. It sits between a plain class and an interface, and it has a much narrower correct use than its prominence in textbooks suggests.
The short version, before the detail: use an interface unless subclasses need to share state or constructor logic. That is the whole decision rule, and the rest of this lesson is why.
The mechanism
abstract class Report {
private final String title;
private final LocalDate date;
protected Report(String title, LocalDate date) {
this.title = title;
this.date = date;
}
protected abstract String body();
protected String footer() {
return "Generated " + date;
}
}
Three things an interface cannot do are in that snippet: a private final field,
a constructor, and a protected method.
An abstract method has no body and must be implemented by a concrete subclass.
A class with any abstract method must itself be abstract.
Two errors you will meet:
error: Report is abstract; cannot be instantiated
Report r = new Report("x");
^
error: Incomplete is not abstract and does not override abstract method body() in Report
class Incomplete extends Report {
^
Both are good errors — they fire at compile time and say exactly what is missing.
The case where it genuinely wins
The template method: the parent fixes the sequence, the subclasses fill in the parts that differ.
abstract class Report {
public final String render() {
return header() + "\n" + body() + "\n" + footer();
}
private String header() {
return "%s\n%s".formatted(title, "=".repeat(title.length()));
}
protected abstract String body();
protected String footer() {
return "Generated " + date;
}
}
class DailyRun extends Report {
DailyRun(String area, int deliveries) {
super("Daily run - " + area, LocalDate.of(2026, 9, 27));
...
}
@Override
protected String body() {
return "%d deliveries in %s".formatted(deliveries, area);
}
}
class MonthlyBill extends Report {
@Override
protected String body() {
return "%s owes Rs %,d.%02d".formatted(customer, paise / 100, paise % 100);
}
@Override
protected String footer() {
return super.footer() + " - payable within 7 days";
}
}
Daily run - Wagholi
===================
42 deliveries in Wagholi
Generated 2026-09-27
Monthly bill
============
Priya owes Rs 2,141.10
Generated 2026-09-30 - payable within 7 days
Look at what each keyword is doing:
render()isfinal— subclasses cannot change the order of the sections. That is the guarantee the pattern exists to provide.header()isprivate— an implementation detail, not an extension point.body()isabstract— every report must supply one.footer()isprotectedwith a default — override it if you want to, callsuper.footer()to build on it.
Deciding which methods are final, which are abstract and which are
overridable is the design. An abstract class where everything is protected
and overridable has made no decisions and gives no guarantees.
And note where the state lives: title and date are held once, in the parent,
initialised by its constructor. That is the thing an interface cannot do, and it
is the only reason this is a class.
Can you have an abstract class with no abstract methods?
Yes, and it is occasionally right: a class that is useless on its own but has
nothing every subclass must supply. Marking it abstract stops anyone creating a
meaningless instance.
Can an abstract class have a constructor? Yes, and it runs — via super(...)
from the subclass. It just never runs on its own.
Abstract class against interface
| Question | Answer |
|---|---|
| Subclasses must share mutable or final state | Abstract class |
| Subclasses must share constructor logic or validation | Abstract class |
| You want to fix an algorithm's order and let parts vary | Abstract class, with a final template method |
| Types that are otherwise unrelated need a common capability | Interface |
| A class needs more than one of these | Interface — you only get one extends |
| You are designing a public API others will implement | Interface |
| You may need to add methods later without breaking implementers | Interface with default methods |
The costs of the abstract class, stated plainly:
- It spends the subclass's one
extends. - Every
protectedmember is a promise to every future subclass. - Deep hierarchies mean reading three files to follow one call.
- It is harder to test in isolation than a composed dependency.
Since Java 8, most of what abstract classes were used for is better done with an interface plus default methods, because shared behaviour without shared state is exactly what those provide. What is left for abstract classes is shared state, and that is genuinely a narrower case than it used to be.
The alternative worth considering first
Before writing an abstract class, ask whether the varying part could be a parameter rather than a subclass:
// Inheritance: one class per variation
abstract class Report { protected abstract String body(); }
class DailyRun extends Report { ... }
class MonthlyBill extends Report { ... }
// Composition: one class, the variation passed in
record Report(String title, LocalDate date, Supplier<String> body) {
String render() { ... }
}
new Report("Daily run - Wagholi", today, () -> "42 deliveries in Wagholi");
The second has no hierarchy, no extends spent, and is trivially testable. It is
not always better — when the variations have their own state and several methods,
subclasses read more clearly. But it should be the option you rule out, rather
than the one you never consider.
Check your work
What can an abstract class do that an interface cannot? Hold instance fields,
including mutable and final ones; declare constructors; and use protected and
package-private access.
What happens if a concrete subclass does not implement an abstract method? is not abstract and does not override abstract method body(). Either implement it or mark the subclass abstract too.
Why is render() declared final in the template method? So subclasses
cannot change the order of the sections. The fixed sequence is the guarantee the
pattern exists to provide.
Why is header() private and footer() protected? header() is an
implementation detail with no extension point; footer() is a deliberate one,
with a sensible default that subclasses may build on with super.footer().
Can an abstract class have a constructor, and when does it run? Yes — when a
subclass calls it through super(...). It never runs on its own, because the
class cannot be instantiated.
What is the one-sentence rule? Interface unless subclasses need to share state or constructor logic.
Practice 3, what breaks when render() is not final. A subclass can override
it and drop the header, or reorder the sections, and nothing warns anyone. The
parent's promise — "every report has a header, then a body, then a footer" —
becomes a suggestion. That promise is the only reason the base class exists.
Practice 4, the interface version. With default methods you can get most of
the way:
interface Report {
String title();
String body();
default String footer() { return "Generated " + LocalDate.now(); }
default String render() {
return title() + "\n" + "=".repeat(title().length())
+ "\n" + body() + "\n" + footer();
}
}
What you lose: render() cannot be final on an interface, so nothing stops a
subclass overriding it — the guarantee is gone. title must be recomputed or
re-stored by every implementation rather than held once. And there is no
constructor to validate it. What you gain: implementers keep their extends.
Which trade is right depends on whether the fixed order was the point.
Practice
-
Build the report hierarchy.
Reportwith afinal render(), an abstractbody(), and an overridablefooter(). Two subclasses. Print both. -
Cause both compile errors. Try
new Report(...)directly, and write a subclass that does not implementbody(). Read each message. -
Remove the
finalfromrender(), then write a subclass that overrides it and omits the header. Nothing complains. Write one sentence on what the base class still guarantees. -
Rewrite it as an interface with
defaultmethods. List what you lost and what you gained. Be specific — "less flexible" is not an answer. -
Replace it with composition. One
Reportclass taking the body as aSupplier<String>or just aString. Compare the three versions side by side and decide which you would want to maintain. -
Harder — a payment processor. An abstract
PaymentMethodwith afinal pay(long paise)that validates the amount, calls an abstractcharge(long paise), and records the result. Implement UPI, cash and card. Then try to add a payment method that needs to skip validation, and notice that you cannot — and that this is the template method working correctly, not failing.
Next: records, which make most of the small classes in this module unnecessary.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship