Skip to main content
Java · lesson 20 of 66
Open the Java page →
Java intermediate Lesson 20 of 66

Also part of: Low-Level Design

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.

By SkillByExample · Updated

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 (no state), providing shared fields, shared concrete methods, and abstract methods that subclasses must fill in. Every example below is a single file run with the JDK 21 source launcher (java File.java), so you can paste it and run it with no build step.

You cannot new an abstract class

Marking a class abstract is a compile-time rule: the compiler refuses to create an instance, even if the class has no abstract methods at all.

public class NoNew {
    public static void main(String[] args) {
        Shape s = new Shape();
        System.out.println(s);
    }
}

abstract class Shape {
    abstract double area();
}
$ java NoNew.java
NoNew.java:3: error: Shape is abstract; cannot be instantiated
        Shape s = new Shape();
                  ^
1 error
error: compilation failed

The error appears before any code runs. The fix is to instantiate a concrete subclass and keep the variable typed as the abstract class:

public class Shapes {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1.0), new Square(2.0) };
        for (Shape s : shapes) {
            System.out.printf("%s area = %.2f%n", s.getClass().getSimpleName(), s.area());
        }
    }
}

abstract class Shape {
    abstract double area();
}

class Circle extends Shape {
    private final double r;
    Circle(double r) { this.r = r; }
    double area() { return Math.PI * r * r; }
}

class Square extends Shape {
    private final double side;
    Square(double side) { this.side = side; }
    double area() { return side * side; }
}
$ java Shapes.java
Circle area = 3.14
Square area = 4.00

Shape s can point at any subclass, and s.area() dispatches to the subclass’s version at runtime. The abstract class guarantees every element has an area(); it never has to know how each one computes it.

Template method pattern

An abstract class can hold a final method that fixes the order of steps and leaves the steps themselves abstract. That is the template method pattern: one skeleton, many variations.

import java.util.List;

public class Reports {
    public static void main(String[] args) {
        List<String> rows = List.of("Item A: $10", "Item B: $25");
        System.out.println(new HtmlReport("Q3 Sales", rows).generate());
        System.out.println(new MarkdownReport("Q3 Sales", rows).generate());
    }
}

abstract class Report {
    protected final String title;
    protected final List<String> rows;
    Report(String title, List<String> rows) { this.title = title; this.rows = rows; }

    protected abstract String header();
    protected abstract String row(String r);

    public final String generate() {           // the fixed skeleton
        StringBuilder sb = new StringBuilder(header()).append('\n');
        for (String r : rows) sb.append(row(r)).append('\n');
        return sb.append("-- end of ").append(title).toString();
    }
}

class HtmlReport extends Report {
    HtmlReport(String t, List<String> r) { super(t, r); }
    protected String header()       { return "<h1>" + title + "</h1>"; }
    protected String row(String r)  { return "<p>" + r + "</p>"; }
}

class MarkdownReport extends Report {
    MarkdownReport(String t, List<String> r) { super(t, r); }
    protected String header()       { return "# " + title; }
    protected String row(String r)  { return "- " + r; }
}
$ java Reports.java
<h1>Q3 Sales</h1>
<p>Item A: $10</p>
<p>Item B: $25</p>
-- end of Q3 Sales
# Q3 Sales
- Item A: $10
- Item B: $25
-- end of Q3 Sales

Both reports run the exact same generate(), so both end with the same -- end of footer line; only header() and row() differ. Because generate() is final, no subclass can reorder the steps or drop the footer — try overriding it and the compiler rejects the subclass.

Constructors and shared state

An abstract class can have fields and a constructor. Subclasses call it with super(...), and it runs before the subclass constructor body.

public class Animals {
    public static void main(String[] args) {
        Dog rex = new Dog("Rex");
        rex.feed();
        rex.fetch();
        rex.fetch();
        System.out.println(rex.status());
    }
}

abstract class Animal {
    private final String name;
    private int energy = 100;

    Animal(String name) {
        this.name = name;
        System.out.println("Animal constructor: " + name);
    }

    abstract String food();

    void feed()          { energy = Math.min(100, energy + 20); System.out.println(name + " eats " + food()); }
    void spend(int n)    { energy = Math.max(0, energy - n); }
    String name()        { return name; }
    String status()      { return name + " energy=" + energy; }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
        System.out.println("Dog constructor");
    }
    String food() { return "kibble"; }
    void fetch()  { spend(30); System.out.println(name() + " fetches"); }
}
$ java Animals.java
Animal constructor: Rex
Dog constructor
Rex eats kibble
Rex fetches
Rex fetches
Rex energy=40

Animal constructor prints first: super(name) must be the first statement in Dog’s constructor, so the shared state is ready before the subclass touches it. feed() was capped at 100 by Math.min, so the energy went 100 → 100 → 70 → 40, not 120 → 90 → 60.

Abstract method called from the constructor

The order above has a trap: if the abstract class’s constructor calls an abstract method, the subclass’s override runs before the subclass’s own fields are initialised.

public class CtorTrap {
    public static void main(String[] args) {
        new Greeter("Hello");
    }
}

abstract class Base {
    Base() {
        System.out.println("Base sees: " + describe());
    }
    abstract String describe();
}

class Greeter extends Base {
    private final String greeting;
    Greeter(String greeting) {
        super();
        this.greeting = greeting;
        System.out.println("Greeter sees: " + describe());
    }
    String describe() { return "greeting=" + greeting; }
}
$ java CtorTrap.java
Base sees: greeting=null
Greeter sees: greeting=Hello

The first line prints null even though greeting is final: Base() runs while Greeter’s fields still hold their default values. Keep constructors of abstract classes free of calls to overridable methods; do that work in a separate method called after construction.

Implementing an interface partially

An abstract class can implement an interface and leave some of its methods abstract. The abstract class supplies the plumbing every subclass shares; the first concrete subclass supplies the rest.

import java.util.List;

public class Pipeline {
    public static void main(String[] args) {
        DataProcessor p = new UpperCaseProcessor();
        p.run("input.txt");
    }
}

interface DataProcessor {
    List<String> read(String source);
    List<String> process(List<String> data);
    void write(List<String> data);
    default void run(String source) { write(process(read(source))); }
}

abstract class BaseProcessor implements DataProcessor {
    public List<String> read(String source) {
        System.out.println("Reading from: " + source);
        return List.of("line1", "line2", "line3");  // simulated I/O
    }
    public void write(List<String> data) { data.forEach(System.out::println); }
    // process() is still abstract here
}

class UpperCaseProcessor extends BaseProcessor {
    public List<String> process(List<String> data) {
        return data.stream().map(String::toUpperCase).toList();
    }
}
$ java Pipeline.java
Reading from: input.txt
LINE1
LINE2
LINE3

BaseProcessor never declares process(), yet it compiles because it is abstract: the obligation passes down to UpperCaseProcessor. run() comes from the interface’s default method, read() and write() from the abstract class, and only process() from the concrete class.

When to choose an abstract class vs an interface

Choose an abstract class when:

  • subclasses share fields or constructor logic
  • you want a template method that fixes an algorithm’s order
  • the relationship is clearly “is-a” (every HtmlReport is a Report)

Choose an interface when:

  • unrelated classes share a capability (Comparable, Iterable)
  • a class needs several supertypes — a class can extend only one class but implement many interfaces
  • you want callers to depend on a contract with no state

Practice

  1. Add a TextReport subclass to Reports.java that renders the header as the title underlined with = and each row indented by two spaces.
Solution
import java.util.List;

public class TextReports {
    public static void main(String[] args) {
        System.out.println(new TextReport("Q3 Sales", List.of("Item A: $10", "Item B: $25")).generate());
    }
}

abstract class Report {
    protected final String title;
    protected final List<String> rows;
    Report(String title, List<String> rows) { this.title = title; this.rows = rows; }
    protected abstract String header();
    protected abstract String row(String r);
    public final String generate() {
        StringBuilder sb = new StringBuilder(header()).append('\n');
        for (String r : rows) sb.append(row(r)).append('\n');
        return sb.append("-- end of ").append(title).toString();
    }
}

class TextReport extends Report {
    TextReport(String t, List<String> r) { super(t, r); }
    protected String header()      { return title + "\n" + "=".repeat(title.length()); }
    protected String row(String r) { return "  " + r; }
}
$ java TextReports.java
Q3 Sales
========
  Item A: $10
  Item B: $25
-- end of Q3 Sales

generate() was not touched: the third subclass slots into the same skeleton, footer included.

  1. In Animals.java, add a Bird subclass whose fly() costs 50 energy. Feed it once, fly twice, then fly a third time and print the status. What stops energy going negative?
Solution
public class Birds {
    public static void main(String[] args) {
        Bird b = new Bird("Tweety");
        b.feed();
        b.fly();
        b.fly();
        b.fly();
        System.out.println(b.status());
    }
}

abstract class Animal {
    private final String name;
    private int energy = 100;
    Animal(String name) { this.name = name; }
    abstract String food();
    void feed()       { energy = Math.min(100, energy + 20); System.out.println(name + " eats " + food()); }
    void spend(int n) { energy = Math.max(0, energy - n); }
    String name()     { return name; }
    String status()   { return name + " energy=" + energy; }
}

class Bird extends Animal {
    Bird(String name) { super(name); }
    String food() { return "seed"; }
    void fly()    { spend(50); System.out.println(name() + " flies"); }
}
$ java Birds.java
Tweety eats seed
Tweety flies
Tweety flies
Tweety flies
Tweety energy=0

spend() clamps with Math.max(0, ...), and because energy is private to Animal, Bird cannot bypass the clamp — it has to go through spend().

  1. Try to override the final generate() method in a subclass of Report. What does the compiler say?
Solution
public class FinalOverride {
    public static void main(String[] args) {
        System.out.println(new ShortReport().generate());
    }
}

abstract class Report {
    protected abstract String header();
    public final String generate() { return header() + "\n-- end"; }
}

class ShortReport extends Report {
    protected String header() { return "Short"; }
    public String generate()  { return header(); }   // try to drop the footer
}
$ java FinalOverride.java
FinalOverride.java:14: error: generate() in ShortReport cannot override generate() in Report
    public String generate()  { return header(); }   // try to drop the footer
                  ^
  overridden method is final
1 error
error: compilation failed

final turns “every report ends with a footer” from a convention into a rule the compiler enforces.

  1. Fix CtorTrap.java so Base still prints a description, but only after the subclass is fully constructed.
Solution
public class CtorFixed {
    public static void main(String[] args) {
        Base g = new Greeter("Hello");
        g.announce();
    }
}

abstract class Base {
    abstract String describe();
    void announce() { System.out.println("Base sees: " + describe()); }
}

class Greeter extends Base {
    private final String greeting;
    Greeter(String greeting) { this.greeting = greeting; }
    String describe() { return "greeting=" + greeting; }
}
$ java CtorFixed.java
Base sees: greeting=Hello

Moving the call out of the constructor into announce() means it runs after Greeter’s constructor has assigned greeting.

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.
Can an abstract class have a constructor in Java?
Yes. An abstract class can declare constructors, and every subclass constructor calls one of them through super(...), explicitly or implicitly. The constructor initialises the shared fields; it simply cannot be invoked with new on the abstract class itself.