Java Module 6 – Inheritance, interfaces and polymorphism
Abstract classes and template methods
Declare Java abstract classes and methods, write a final template method that subclasses fill in, and choose between an abstract class and an interface.
What you will learn
- Declare abstract classes and abstract methods
- Implement the template method pattern with a final skeleton method
- Use a JDK abstract class such as AbstractList by implementing only its abstract methods
- Choose between an abstract class and an interface
Before you start
What you’ll learn · Before you start
What you will learn
- Declare abstract classes and abstract methods
- Implement the template method pattern with a final skeleton method
- Use a JDK abstract class such as AbstractList by implementing only its abstract methods
- Choose between an abstract class and an interface
Before you start
On this page
An abstract class is a class that is unfinished on purpose. You declare it with abstract class, nobody can
create an object of it with new, and it may declare abstract methods, methods with a signature but no body,
which every concrete subclass must implement. In every other way it is an ordinary class: it can hold fields,
constructors that check them, and finished methods. That mix makes it the natural home of the template method,
a design in which one final method of the superclass fixes the steps of a job and their order, and calls abstract
methods for the steps that differ, so each subclass writes only those. An interface, the subject of the next lesson,
can also declare methods without bodies, but it cannot hold the state of an object, and that difference usually
decides between the two.
This lesson builds one template, shows seven declarations that the compiler refuses, and gets a complete List out
of the JDK’s AbstractList with two short methods.
An abstract report and two concrete ones
// Reports.java: one final render() method, and two kinds of report that supply its steps.
import java.util.List;
class Reports {
static void main() {
List<Sale> sales = List.of(
new Sale("masala dosa", 3, 27_000),
new Sale("filter coffee", 5, 15_000),
new Sale("idli", 4, 16_000));
Report text = new TextReport("Counter sales", sales);
Report csv = new CsvReport("Counter sales", sales);
IO.print(text.render());
IO.println("---");
IO.print(csv.render());
try {
new CsvReport(" ", sales);
} catch (IllegalArgumentException e) {
IO.println("---\nrefused: " + e.getMessage());
}
}
}
record Sale(String item, int quantity, long amountPaise) { }
abstract class Report {
private final String title;
private final List<Sale> sales;
protected Report(String title, List<Sale> sales) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("a report needs a title");
}
if (sales.isEmpty()) {
throw new IllegalArgumentException("a report needs at least one sale");
}
this.title = title;
this.sales = List.copyOf(sales);
}
/** The template method: the steps and their order are fixed here, once. */
final String render() {
StringBuilder out = new StringBuilder(header(title));
long total = 0;
for (Sale sale : sales) {
out.append(row(sale));
total += sale.amountPaise();
}
if (showsTotal()) {
out.append(footer(total));
}
return out.toString();
}
protected abstract String header(String title);
protected abstract String row(Sale sale);
protected abstract String footer(long totalPaise);
/** A hook: a step with a default that subclasses may override. */
protected boolean showsTotal() { return true; }
static String rupees(long paise) {
return String.format("%d.%02d", paise / 100, paise % 100);
}
}
class TextReport extends Report {
TextReport(String title, List<Sale> sales) { super(title, sales); }
@Override
protected String header(String title) { return title + "\n" + "=".repeat(title.length()) + "\n"; }
@Override
protected String row(Sale s) { return String.format("%-14s %2d %8s\n", s.item(), s.quantity(), rupees(s.amountPaise())); }
@Override
protected String footer(long totalPaise) { return String.format("%-17s %8s\n", "total", rupees(totalPaise)); }
}
class CsvReport extends Report {
CsvReport(String title, List<Sale> sales) { super(title, sales); }
@Override
protected String header(String title) { return "item,quantity,amount\n"; }
@Override
protected String row(Sale s) { return s.item() + "," + s.quantity() + "," + rupees(s.amountPaise()) + "\n"; }
@Override
protected String footer(long totalPaise) { return ""; }
@Override
protected boolean showsTotal() { return false; } // a CSV file holds the rows and nothing else
} from session import run, version
version()
run("""
java Reports.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Reports.java Counter sales ============= masala dosa 3 270.00 filter coffee 5 150.00 idli 4 160.00 total 580.00 --- item,quantity,amount masala dosa,3,270.00 filter coffee,5,150.00 idli,4,160.00 --- refused: a report needs a title
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 reports_session.py
Report is abstract, yet most of the program’s logic lives in it:
- State and its rules.
Reportkeeps the title and the sales, and its constructor refuses a blank title or an empty list. Every subclass gets those checks without repeating them:new CsvReport(" ", sales)fails inReport’s constructor, whichCsvReport’ssuper(title, sales)runs. The constructor isprotected, a common choice, because only subclasses ever call it. - The fixed part of the job.
render()writes the header, one row for each sale, and the footer with the total. It isfinal, so no subclass can change that order. - The parts that differ.
header(),row()andfooter()are abstract.TextReportandCsvReportimplement all three, andrender()reaches their versions through dynamic dispatch, as in the previous lesson.
Report text = new TextReport(...) is ordinary code. An abstract class cannot have objects of its own, but its type
works like any other: a Report variable can refer to an object of any concrete subclass.
What the compiler checks
Java (AbstractErrors.java) · AbstractErrors.java
// AbstractErrors.java: six abstract declarations that the compiler refuses.
class AbstractErrors {
static void main() {
Report r = new Report(); // an abstract class has no objects of its own
}
}
abstract class Report {
abstract String body();
}
class PlainReport extends Report { } // body() is still missing
abstract final class LockedReport { } // final forbids the subclasses abstract needs
abstract class DraftReport {
abstract String body() { return ""; } // an abstract method has no body
private abstract String footer(); // a private method cannot be overridden
static abstract String title(); // a static method cannot be overridden either
} Java (FinalTemplate.java) · FinalTemplate.java
// FinalTemplate.java: a subclass that tries to replace the template method itself.
class FinalTemplate {
static void main() { }
}
abstract class Report {
final String render() { return header() + body(); }
protected abstract String header();
protected abstract String body();
}
class QuickReport extends Report {
@Override
String render() { return body(); } // would skip the header
@Override
protected String header() { return "Quick report\n"; }
@Override
protected String body() { return "all good\n"; }
} from session import run, version
version()
run("""
!java AbstractErrors.java
!java FinalTemplate.java
""") Output
# openjdk version "25.0.4.1" LTS
$ java AbstractErrors.java
AbstractErrors.java:14: error: illegal combination of modifiers: abstract and final
abstract final class LockedReport { } // final forbids the subclasses abstract needs
^
AbstractErrors.java:18: error: illegal combination of modifiers: abstract and private
private abstract String footer(); // a private method cannot be overridden
^
AbstractErrors.java:19: error: illegal combination of modifiers: abstract and static
static abstract String title(); // a static method cannot be overridden either
^
AbstractErrors.java:4: error: Report is abstract; cannot be instantiated
Report r = new Report(); // an abstract class has no objects of its own
^
AbstractErrors.java:12: error: PlainReport is not abstract and does not override abstract method body() in Report
class PlainReport extends Report { } // body() is still missing
^
AbstractErrors.java:17: error: abstract methods cannot have a body
abstract String body() { return ""; } // an abstract method has no body
^
6 errors
error: compilation failed
[exit status 1]
$ java FinalTemplate.java
FinalTemplate.java:16: error: render() in QuickReport cannot override render() in Report
String render() { return body(); } // would skip the header
^
overridden method is final
1 error
error: compilation failed
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py
- No objects of an abstract class.
new Report()fails with “Report is abstract; cannot be instantiated”. - Every abstract method gets a body somewhere.
PlainReportinheritsbody()without implementing it, and the message says what to do: implementbody(), or declarePlainReportabstract too and leave the job to its own subclasses. - Abstract methods have no body, and they live only in abstract classes. A class with even one abstract method,
declared or inherited and not implemented, must be declared abstract. The reverse is not required: an abstract
class may have no abstract methods at all, just to prevent
new. abstractcontradicts three modifiers. It needs subclasses that override, so it cannot be combined withfinal, which forbids subclasses, nor withprivateorstatic, because neither kind of method can be overridden.- A
finaltemplate cannot be replaced.QuickReporttries to overriderender()to skip the header, and the compiler refuses: “overridden method is final”.
The template method, step by step
The report template and its two subclasses
Text description of the diagram
The diagram shows three classes. Report, at the top, is abstract. It declares the final method render(), which calls the steps of a report in a fixed order, the abstract methods header(), row() and footer(), which have no body, and the hook showsTotal(), which returns true unless a subclass overrides it.
Two classes extend Report, shown below it with arrows labelled extends:
- TextReport writes the title with an underline as its header, each sale as a row of aligned columns, and a total line as its footer.
- CsvReport writes the column names as its header, each sale as comma-separated values, and an empty footer, and it overrides showsTotal() to return false, so render() leaves the total out.
render() is the template method: it holds the algorithm, and its steps are methods that subclasses fill in.
It calls four of them, and each has one of three roles:
header(title), abstract.TextReportwrites the title, underlined;CsvReportwrites the column names.row(sale), abstract, called once for each sale.TextReportwrites aligned columns;CsvReportwrites comma-separated values.showsTotal(), a hook that returnstrue.TextReportinherits it;CsvReportoverrides it to returnfalse.footer(total), abstract.TextReportwrites the total line;CsvReportreturns an empty string, whichrender()never asks for, because the hook said no.
A hook is a step with a default implementation that subclasses may override and usually do not. CsvReport
uses one to leave the total out, because a CSV file should hold nothing but its rows, and render() never has to
know which subclass it is serving. Hooks are also the safe way to grow a template: a new hook with a default leaves
every existing subclass compiling, while a new abstract method breaks each one until it implements the method.
Three habits keep a template sound:
- Make the template method
final, so the order of the steps and the work between them, such as adding up the total, stays in one place. - Make the steps
protected: subclasses implement them, and callers use the template. The steps ofReportare meant to be called byrender(), not by the code that prints a report. - Keep the steps small and give each one a single job. A step that does several things forces every subclass to repeat the parts it did not want to change.
The same design in the JDK
The JDK uses templates in many of its abstract classes. java.util.AbstractList implements the whole List
interface on top of two abstract methods, get(int) and size():
// Multiples.java: a whole List<Integer> from AbstractList, with only get() and size() written.
import java.util.AbstractList;
import java.util.List;
class Multiples extends AbstractList<Integer> {
private final int step;
private final int count;
Multiples(int step, int count) {
if (step <= 0 || count < 0) {
throw new IllegalArgumentException("step must be positive and count must not be negative");
}
this.step = step;
this.count = count;
}
@Override
public Integer get(int index) {
if (index < 0 || index >= count) {
throw new IndexOutOfBoundsException("index " + index + ", size " + count);
}
return step * (index + 1);
}
@Override
public int size() { return count; }
static void main() {
List<Integer> sevens = new Multiples(7, 10); // no array: each value is computed by get()
IO.println(sevens);
IO.println("contains 42: " + sevens.contains(42) + ", indexOf(42): " + sevens.indexOf(42));
IO.println("subList(2, 5): " + sevens.subList(2, 5));
int sum = 0;
for (int n : sevens) {
sum += n;
}
IO.println("sum: " + sum);
IO.println("equals List.of(7, 14): " + new Multiples(7, 2).equals(List.of(7, 14)));
try {
sevens.set(0, 1);
} catch (UnsupportedOperationException e) {
IO.println("set(0, 1): UnsupportedOperationException");
}
}
} from session import run, version
version()
run("""
java Multiples.java
""") Output
# openjdk version "25.0.4.1" LTS $ java Multiples.java [7, 14, 21, 28, 35, 42, 49, 56, 63, 70] contains 42: true, indexOf(42): 5 subList(2, 5): [21, 28, 35] sum: 385 equals List.of(7, 14): true set(0, 1): UnsupportedOperationException
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 multiples_session.py
Multiples stores two numbers and computes each element in get(), yet it answers contains(), indexOf() and
subList(), works in a for-each loop, prints itself, and compares equal to List.of(7, 14). All of those come from
AbstractList and its superclass AbstractCollection, built on the two methods that Multiples implements. The last line shows
a default step: AbstractList.set() throws UnsupportedOperationException, and its documentation says to override
set() to make a list modifiable, and add() and remove() as well when its size may change.
java.io.InputStream works the same way. A subclass must provide read(), which returns the next byte, and the
method that reads into an array of bytes calls read() repeatedly unless the subclass overrides it with something
faster.
Abstract class or interface?
Both can declare methods that subclasses or implementing classes must provide. They differ in what else they can hold:
| Criterion | Abstract class | Interface |
|---|---|---|
| Instance fields | yes, with any access | none: every field is public, static and final |
| Constructors | yes, run through super(...) | none |
| How many a class can have | one, as its superclass | any number |
| Methods | abstract and finished ones, with any access except private abstract | abstract, default, static and private ones; all but the private ones are public |
| When to choose | Related classes that share state and code, such as a template | A capability that unrelated classes share, such as Comparable |
So the deciding question is usually whether the shared part needs state. Report keeps a title and a list of sales
and checks them in its constructor, which an interface cannot do. When the shared part is only a set of operations,
an interface is the better choice: a class can implement several interfaces while it extends one class, so an
interface never takes the single superclass a class may need for something else.
The two also combine. A common shape in the JDK is an interface for the type and an abstract class that implements
part of it: List is the interface, and AbstractList is the head start for writing one. The next lesson covers
interfaces, including the default methods that let them carry code.
Your turn: a template for importing data
DataImporter declares the three steps of an import, read(), problem() and save(), and the final template
method importAll(), which is still empty. CsvImporter is finished, PipeImporter is a stub, and the checks use
RecordingStore, a fake that writes each call into a log, so they can test the order of the steps.
Java (DataImporter.java) · importer/DataImporter.java
// DataImporter.java: TODO 1.
abstract class DataImporter {
/**
* The template method: reads the records, checks each one, saves the good ones and rejects the others, in input
* order. Returns how many records were saved.
*/
final int importAll(String input, Store store) {
// TODO 1: for each record that read(input) returns, problem(record) is null when the record is good: then
// call save(record, store). Otherwise call store.reject(record, problem). Count the saved records.
return 0;
}
/** Step 1: splits the input into records, without headers or blank lines. */
protected abstract String[] read(String input);
/** Step 2: why the record cannot be saved, or null when it can. */
protected abstract String problem(String record);
/** Step 3: saves a good record. */
protected abstract void save(String record, Store store);
} Java (CsvImporter.java) · importer/CsvImporter.java
// CsvImporter.java: finished. Reads lines such as "tea,1500" (an item and its amount in paise) after a header line.
import java.util.Arrays;
class CsvImporter extends DataImporter {
@Override
protected String[] read(String input) {
String[] lines = input.strip().split("\n");
return Arrays.copyOfRange(lines, 1, lines.length); // skips the header
}
@Override
protected String problem(String record) {
String[] fields = record.split(",");
if (fields.length != 2) {
return "expected 2 fields";
}
if (!fields[1].matches("\\d+")) {
return "the amount is not a whole number of paise";
}
return null;
}
@Override
protected void save(String record, Store store) {
String[] fields = record.split(",");
store.save(fields[0], Long.parseLong(fields[1]));
}
} Java (PipeImporter.java) · importer/PipeImporter.java
// PipeImporter.java: TODO 2.
class PipeImporter extends DataImporter {
// TODO 2: lines such as "tea|1500", with no header line. Skip blank lines, and give the same reasons as
// CsvImporter for a record that does not have 2 fields or whose amount is not a whole number.
@Override
protected String[] read(String input) { return new String[0]; }
@Override
protected String problem(String record) { return "not written yet"; }
@Override
protected void save(String record, Store store) { }
} Java (Store.java) · importer/Store.java
// Store.java: where imported records end up. Leave this file as it is.
class Store {
void save(String item, long amountPaise) {
IO.println("saved " + item + ": " + amountPaise + " paise");
}
void reject(String record, String reason) {
IO.println("rejected \"" + record + "\": " + reason);
}
} Java (ImporterChecks.java) · importer/ImporterChecks.java
// ImporterChecks.java: the sample checks, with a store that records calls instead of saving.
// Run them with: java importer/ImporterChecks.java
class ImporterChecks {
private static int passed;
private static int total;
static void main() {
String csv = "item,amount\ntea,1500\ncoffee,abc\nsamosa,2000\n";
check("the CSV importer reads 3 records after the header", new CsvImporter().read(csv).length == 3);
RecordingStore store = new RecordingStore();
int saved = new CsvImporter().importAll(csv, store);
check("2 of the 3 CSV records are saved", saved == 2);
check("the store sees the CSV records in input order", store.log().equals(
"save tea 1500 | reject coffee,abc: the amount is not a whole number of paise | save samosa 2000"));
store = new RecordingStore();
saved = new PipeImporter().importAll("tea|1500\n\nvada|x\npoha|2500\n", store);
check("2 of the 3 pipe records are saved", saved == 2);
check("pipe records are split at the | and blank lines skipped", store.log().equals(
"save tea 1500 | reject vada|x: the amount is not a whole number of paise | save poha 2500"));
store = new RecordingStore();
new PipeImporter().importAll("chai|10|20\n", store);
check("a pipe record with 3 fields is rejected", store.log().equals("reject chai|10|20: expected 2 fields"));
IO.println(passed + " of " + total + " checks passed");
if (passed != total) {
System.exit(1);
}
}
static void check(String what, boolean ok) {
total++;
if (ok) {
passed++;
}
IO.println((ok ? "PASS " : "FAIL ") + what);
}
}
/** A fake store: it writes each call into a log instead of saving anything. */
class RecordingStore extends Store {
private final StringBuilder log = new StringBuilder();
@Override
void save(String item, long amountPaise) { add("save " + item + " " + amountPaise); }
@Override
void reject(String record, String reason) { add("reject " + record + ": " + reason); }
private void add(String entry) {
log.append(log.isEmpty() ? "" : " | ").append(entry);
}
String log() { return log.toString(); }
} from session import run, version
# The sample checks against the starter code in importer/.
version()
run("""
!java importer/ImporterChecks.java
""") Output
# openjdk version "25.0.4.1" LTS $ java importer/ImporterChecks.java PASS the CSV importer reads 3 records after the header FAIL 2 of the 3 CSV records are saved FAIL the store sees the CSV records in input order FAIL 2 of the 3 pipe records are saved FAIL pipe records are split at the | and blank lines skipped FAIL a pipe record with 3 fields is rejected 1 of 6 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checks_session.py
- TODO 1: write
importAll(). For each record thatread()returns, callproblem(); save the record when the answer isnull, and pass it tostore.reject()with the reason otherwise. Keep the input order, and return how many records were saved. - TODO 2: write
PipeImporterfor lines such astea|1500, with no header line. Skip blank lines, and give the same two reasons asCsvImporter.
Hint: String.split takes a regular expression, and in a regular expression | means “or”, so
record.split("|") splits between every pair of characters. Write record.split("\\|").
When the program prints 6 of 6 checks passed, both importers run through the one template you wrote, and a third
format would need only its three steps.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · Can an abstract class have a constructor, and why would it have one?
Yes. An abstract class cannot be instantiated with new, but every object of one of its concrete subclasses
contains the abstract class’s part, and that part is initialised by the abstract class’s constructor, which each
subclass constructor runs through super(...). A constructor there lets the abstract class set up and check the state
it owns, so the rule is written once: in this lesson, Report refuses a blank title or an empty list of sales for
every kind of report. Such constructors are often protected, since only subclasses call them. A good answer
adds that interfaces have no constructors and no instance fields, which is one of the reasons to choose an abstract
class.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
You are writing payment providers that share retry logic and a count of failed attempts, and one provider must also extend a vendor's client class. Would you use an abstract class, an interface or both? Defend the choice.
-
Mid to senior
Your abstract DataImporter has a final importAll() template and 12 subclasses in three teams' code. You must add an audit step after every saved record, and one subclass needs to skip some records after reading them. How do you change the template without breaking the 12 subclasses?
Key takeaways
abstract classmakes a class that cannot be instantiated; its abstract methods have no body, and every concrete subclass must implement them.- An abstract class can have fields, constructors and finished methods, and its constructor runs, through
super(...), for every subclass object. abstractcannot be combined withfinal,privateorstatic.- A template method is a
finalmethod that fixes the steps of a job and calls abstract methods and hooks for the steps that differ; add new steps as hooks with defaults. AbstractListturnsget()andsize()into a complete read-onlyList;InputStreambuilds its otherreadmethods onread().- Choose an abstract class for shared state and code, an interface for a capability that unrelated classes share, and often both: an interface for the type and an abstract class as a starting point.
References
- The Java Language Specification, Java SE 25: chapter 8, Classes (8.1.1.1 abstract Classes, 8.4.3.1 abstract Methods) (Oracle)
- The Java Language Specification, Java SE 25: chapter 9, Interfaces (Oracle)
- AbstractList (Java SE 25 API) (Oracle)
- AbstractCollection (Java SE 25 API) (Oracle)
- InputStream (Java SE 25 API) (Oracle)
- String.split (Java SE 25 API) (Oracle)
Report a problem with this lesson
Kept only in this browser. Your Learn progress