Abstract Classes in Java
Master abstract classes — when to use them over interfaces, template method pattern, and how they enable shared behaviour across a class hierarchy.
An abstract class is a class that cannot be instantiated and is designed to be extended. It sits between a concrete class (everything implemented) and an interface (nothing implemented), providing a middle ground: shared state, shared concrete methods, and abstract methods that subclasses must fill in. The key benefit is that you can put real logic — field initialisation, helper methods, shared algorithms — directly in the abstract class, eliminating the duplication that would otherwise appear across every subclass.
Anatomy of an Abstract Class
Abstract classes differ from interfaces in that they can hold fields, constructors, and fully implemented methods. When a subclass calls super(...), it inherits both the state and the shared behaviour defined here. Abstract methods act as extension points — the abstract class says “I need this step done, but I don’t know how yet.”
public abstract class Report {
// State — abstract classes can have fields (interfaces cannot)
protected final String title;
protected final String author;
protected final LocalDate generatedAt;
// Constructor — called by subclasses via super()
public Report(String title, String author) {
this.title = title;
this.author = author;
this.generatedAt = LocalDate.now();
}
// Abstract methods — subclasses MUST implement these
protected abstract String buildHeader();
protected abstract String buildBody();
protected abstract String buildFooter();
// Template method — defines the fixed skeleton; calls abstract steps
// final prevents subclasses from changing the overall algorithm structure
public final String generate() {
StringBuilder sb = new StringBuilder();
sb.append(buildHeader()).append("\n");
sb.append(buildBody()).append("\n");
sb.append(buildFooter());
return sb.toString();
}
// Concrete helper available to all subclasses
protected String formatDate(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("MMM d, yyyy"));
}
}
Template Method Pattern
The generate() method above is a template method — it defines the fixed algorithm structure and delegates the variable steps to abstract methods. This pattern is valuable because it enforces a consistent process (every report always has a header, body, and footer in that order) while leaving each subclass free to customise the content of each step. You get consistency without copy-paste.
public class HtmlReport extends Report {
private final List<String> rows;
public HtmlReport(String title, String author, List<String> rows) {
super(title, author); // initialises shared fields
this.rows = rows;
}
@Override
protected String buildHeader() {
return "<html><head><title>" + title + "</title></head><body>"
+ "<h1>" + title + "</h1>"
+ "<p>Author: " + author + " | Date: " + formatDate(generatedAt) + "</p>";
}
@Override
protected String buildBody() {
StringBuilder sb = new StringBuilder("<table>");
for (String row : rows) sb.append("<tr><td>").append(row).append("</td></tr>");
return sb.append("</table>").toString();
}
@Override
protected String buildFooter() {
return "<footer>Generated by ReportEngine</footer></body></html>";
}
}
public class MarkdownReport extends Report {
private final List<String> rows;
public MarkdownReport(String title, String author, List<String> rows) {
super(title, author);
this.rows = rows;
}
@Override
protected String buildHeader() {
return "# " + title + "\n_" + author + " — " + formatDate(generatedAt) + "_\n";
}
@Override
protected String buildBody() {
StringBuilder sb = new StringBuilder();
rows.forEach(row -> sb.append("- ").append(row).append("\n"));
return sb.toString();
}
@Override
protected String buildFooter() {
return "\n---\n*Generated by ReportEngine*";
}
}
// Both use the same generate() skeleton — only the steps differ
List<String> data = List.of("Item A: $10", "Item B: $25", "Item C: $5");
Report html = new HtmlReport("Q3 Sales", "Alice", data);
Report md = new MarkdownReport("Q3 Sales", "Alice", data);
System.out.println(html.generate());
System.out.println(md.generate());
Abstract Class with Shared State and Behaviour
Abstract classes are especially powerful when subclasses share both fields and non-trivial logic. Without an abstract base class, the energy field and the rest() / feed() / getStatus() methods would have to be duplicated in every animal subclass — a maintenance burden and a source of subtle inconsistencies. The abstract class owns all of that once, and subclasses only provide the parts that truly differ.
public abstract class Animal {
private final String name;
private final String species;
private double energy; // 0.0 to 1.0
public Animal(String name, String species) {
this.name = name;
this.species = species;
this.energy = 1.0;
}
// Each subclass defines HOW it eats, sleeps, and sounds — not WHETHER it does
public abstract void eat();
public abstract void sleep();
public abstract String makeSound();
// Shared behaviour — identical for all animals, defined once
public void rest() {
energy = Math.min(1.0, energy + 0.3);
sleep(); // delegates to the subclass's implementation
System.out.printf("%s rests. Energy: %.0f%%%n", name, energy * 100);
}
public void feed() {
energy = Math.min(1.0, energy + 0.2);
eat(); // delegates to the subclass's implementation
System.out.printf("%s fed. Energy: %.0f%%%n", name, energy * 100);
}
public String getStatus() {
return String.format("%s (%s) — energy: %.0f%%", name, species, energy * 100);
}
protected String getName() { return name; }
protected double getEnergy() { return energy; }
protected void consumeEnergy(double amount) {
energy = Math.max(0, energy - amount);
}
}
public class Dog extends Animal {
private final String breed;
public Dog(String name, String breed) {
super(name, "Canis lupus familiaris");
this.breed = breed;
}
@Override public void eat() { System.out.println(getName() + " chomps kibble."); }
@Override public void sleep() { System.out.println(getName() + " curls up."); }
@Override public String makeSound() { return "Woof!"; }
// Dog-specific behaviour that has no place in the abstract class
public void fetch() {
consumeEnergy(0.2);
System.out.printf("%s fetches the ball! Energy: %.0f%%%n", getName(), getEnergy() * 100);
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name, "Felis catus");
}
@Override public void eat() { System.out.println(getName() + " delicately nibbles."); }
@Override public void sleep() { System.out.println(getName() + " finds a sunny spot."); }
@Override public String makeSound() { return "Meow."; }
}
Abstract Class Implementing an Interface Partially
An abstract class can implement an interface and leave some methods abstract, letting subclasses finish the job. This is a powerful layering technique: the abstract class handles the generic plumbing that every subclass would do identically, while still requiring subclasses to provide the parts that vary. You get the structure of an interface contract combined with the code-reuse of an abstract class.
public interface DataProcessor {
List<String> read(String source);
List<String> process(List<String> data);
void write(List<String> data, String destination);
default void run(String source, String destination) {
write(process(read(source)), destination);
}
}
// Abstract class handles reading (same for every subclass); subclasses handle processing and writing
public abstract class BaseProcessor implements DataProcessor {
@Override
public List<String> read(String source) {
System.out.println("Reading from: " + source);
return List.of("line1", "line2", "line3"); // simulated I/O
}
// process() and write() remain abstract — each subclass decides its own logic
}
public class UpperCaseFileProcessor extends BaseProcessor {
@Override
public List<String> process(List<String> data) {
return data.stream().map(String::toUpperCase).toList();
}
@Override
public void write(List<String> data, String destination) {
System.out.println("Writing " + data.size() + " lines to " + destination);
data.forEach(System.out::println);
}
}
DataProcessor p = new UpperCaseFileProcessor();
p.run("input.txt", "output.txt");
// Reading from: input.txt
// Writing 3 lines to output.txt
// LINE1
// LINE2
// LINE3
When to Choose Abstract Class vs Interface
Choose an abstract class when:
- Subclasses share fields or constructors
- You want to provide a substantial amount of common logic via concrete methods
- The relationship is clearly “is-a” (all subclasses ARE a Report, ARE an Animal)
- You want to use the Template Method pattern to enforce an algorithm skeleton
Choose an interface when:
- You are defining a capability that unrelated classes can share (Comparable, Iterable)
- You need multiple inheritance of type
- You want maximum flexibility — any class can implement without joining a hierarchy