Skip to main content
Java intermediate Lesson 20 of 58

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

Frequently Asked Questions

Can an abstract class implement an interface?
Yes. An abstract class can implement an interface without providing implementations for all the interface methods. The first concrete subclass is then responsible for implementing the remaining methods.
What is the template method pattern?
It is a behavioural design pattern where an abstract class defines the skeleton of an algorithm in a final method, and defers specific steps to abstract methods implemented by subclasses. The overall structure stays fixed; only the variable parts change.
Can an abstract class have a main method?
Yes. An abstract class can have a main method, static methods, and static fields. The abstract restriction only applies to instantiating the class directly — you can still run static code inside it.