Skip to content
elephantoo

Abstraction & abstract classes

Lesson 16 of 43 13 min read

Abstract methods and classes, template methods and when abstraction helps.


Abstraction means showing what something does while hiding how it does it. When you call list.sort() or drive a car, you use a simple interface and ignore the machinery underneath. In class design, abstraction means capturing the common idea ("every shape has an area") without pretending to know details that only specific subclasses can provide.

In the polymorphism lesson our base Shape had double area() { return 0; }. That is a lie: there is no sensible area for a generic shape, and nothing stops someone writing new Shape() or forgetting to override area(). Abstract classes fix both problems.

Abstract methods and abstract classes#

An abstract method has a signature but no body. It says "every subclass must provide this". A class with at least one abstract method must itself be declared abstract, and an abstract class cannot be instantiated.

AbstractShapes.java
public class AbstractShapes {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle("Wheel", 1.5), new Rectangle("Door", 0.9, 2.1) };
        for (Shape s : shapes) {
            System.out.println(s.describe());
        }
        // Shape s = new Shape("blob");   // error: Shape is abstract; cannot be instantiated
    }
}

abstract class Shape {
    private final String name;                 // abstract classes can have state

    protected Shape(String name) {             // ...and constructors
        this.name = name;
    }

    abstract double area();                    // no body: subclasses MUST implement
    abstract double perimeter();

    String describe() {                        // ...and concrete methods
        return String.format("%s: area %.2f, perimeter %.2f", name, area(), perimeter());
    }
}

class Circle extends Shape {
    private final double r;

    Circle(String name, double r) {
        super(name);
        this.r = r;
    }

    @Override double area() { return Math.PI * r * r; }
    @Override double perimeter() { return 2 * Math.PI * r; }
}

class Rectangle extends Shape {
    private final double w, h;

    Rectangle(String name, double w, double h) {
        super(name);
        this.w = w;
        this.h = h;
    }

    @Override double area() { return w * h; }
    @Override double perimeter() { return 2 * (w + h); }
}
Output
Wheel: area 7.07, perimeter 9.42
Door: area 1.89, perimeter 6.00

What the compiler now guarantees:

  • Nobody can create a meaningless plain Shape.
  • Every concrete subclass must implement area() and perimeter(), or it will not compile:
Java
class Triangle extends Shape {
    Triangle() { super("Triangle"); }
    @Override double area() { return 0.5; }
}
// error: Triangle is not abstract and does not override abstract method perimeter() in Shape

Even though you can't write new Shape(...), the abstract class still has a constructor. It runs as part of building every subclass object, through super(name). Making it protected documents that only subclasses should call it.

The rules in one place#

  • abstract methods have no body (just a ;) and cannot be private, static or final (all of those prevent overriding).
  • A class with any abstract method must be abstract. A class can also be abstract with no abstract methods, purely to prevent instantiation.
  • An abstract class can have fields, constructors, static members, and concrete methods of any visibility.
  • A subclass either implements all inherited abstract methods or is declared abstract itself.
  • abstract and final can never be combined on a class: one demands subclasses, the other forbids them.
  • You can use the abstract type for variables, parameters, arrays and return types: Shape s = new Circle(...).

A partially abstract hierarchy#

Abstract classes can stack. A middle layer can implement some methods and leave the rest for its children:

Staff.java
public class Staff {
    public static void main(String[] args) {
        Employee[] team = {
            new SalariedEmployee("Asha", 90_000),
            new HourlyEmployee("Ben", 400, 120),
            new Intern("Chen", 400, 80)
        };
        for (Employee e : team) {
            System.out.printf("%-6s %-8s Rs %,10.2f%n", e.getName(), e.role(), e.monthlyPay());
        }
    }
}

abstract class Employee {
    private final String name;

    Employee(String name) { this.name = name; }

    String getName() { return name; }

    abstract double monthlyPay();
    abstract String role();
}

class SalariedEmployee extends Employee {
    private final double annualSalary;

    SalariedEmployee(String name, double monthly) {
        super(name);
        this.annualSalary = monthly * 12;
    }

    @Override double monthlyPay() { return annualSalary / 12; }
    @Override String role() { return "Salaried"; }
}

class HourlyEmployee extends Employee {
    private final double rate;
    private final int hours;

    HourlyEmployee(String name, double rate, int hours) {
        super(name);
        this.rate = rate;
        this.hours = hours;
    }

    @Override double monthlyPay() { return rate * hours; }
    @Override String role() { return "Hourly"; }
}

class Intern extends HourlyEmployee {
    Intern(String name, double rate, int hours) { super(name, rate, hours); }

    @Override double monthlyPay() { return super.monthlyPay() * 0.5; }   // stipend is half
    @Override String role() { return "Intern"; }
}
Output
Asha   Salaried Rs  90,000.00
Ben    Hourly   Rs  48,000.00
Chen   Intern   Rs  16,000.00

The template method pattern#

A very common use of abstract classes is the template method: the parent defines the steps of an algorithm in a final method and lets subclasses fill in individual steps. The overall flow is fixed in one place; only the variable parts change.

Reports.java
public class Reports {
    public static void main(String[] args) {
        String[][] rows = { {"Asha", "92"}, {"Ben", "78"} };
        new CsvReport().generate(rows);
        System.out.println();
        new HtmlReport().generate(rows);
    }
}

abstract class Report {
    // The template: the order of steps can't be changed by subclasses
    final void generate(String[][] rows) {
        header();
        for (String[] row : rows) {
            line(row);
        }
        footer();
    }

    abstract void header();
    abstract void line(String[] row);

    void footer() { }   // a "hook": optional to override, does nothing by default
}

class CsvReport extends Report {
    @Override void header() { System.out.println("name,score"); }
    @Override void line(String[] row) { System.out.println(String.join(",", row)); }
}

class HtmlReport extends Report {
    @Override void header() { System.out.println("<table>"); }
    @Override void line(String[] row) {
        System.out.println("  <tr><td>" + row[0] + "</td><td>" + row[1] + "</td></tr>");
    }
    @Override void footer() { System.out.println("</table>"); }
}
Output
name,score
Asha,92
Ben,78

<table>
  <tr><td>Asha</td><td>92</td></tr>
  <tr><td>Ben</td><td>78</td></tr>
</table>

You will meet this pattern all over the JDK and frameworks: java.io.InputStream is abstract, with read() abstract and many concrete methods built on top of it; AbstractList gives you a full List if you implement just get() and size().

Anonymous subclasses#

For a one-off implementation you can subclass an abstract class inline, without naming the class:

Anonymous.java
public class Anonymous {
    public static void main(String[] args) {
        Greeter polite = new Greeter() {
            @Override String greeting(String name) { return "Good morning, " + name + "."; }
        };
        polite.greet("Meera");
    }
}

abstract class Greeter {
    abstract String greeting(String name);

    void greet(String name) {
        System.out.println(greeting(name));
    }
}
Output
Good morning, Meera.

new Greeter() { ... } doesn't create a Greeter; it creates an object of a nameless subclass. For interfaces with a single method, a lambda (covered later) is usually neater.

When does abstraction help?#

Use an abstract class when:

  • Several related classes share state and code, and the shared base makes no sense on its own (Shape, Employee, Report).
  • You want to fix the skeleton of an algorithm and let subclasses vary the steps (template method).
  • You want to provide a partial implementation of a bigger contract to save subclasses work.

If you only need a contract (a set of method signatures) with no shared state, or a class needs to fit several roles at once, an interface is usually the better choice. That is the next lesson, and it ends with a side-by-side comparison.

Don't add abstraction speculatively. One concrete class is easier to read than an abstract base with one subclass. Extract an abstract class when you have (or clearly will have) two or more real variants.

Common mistakes#

  • Trying to instantiate an abstract class with new.
  • Giving an abstract method a body, or forgetting the abstract keyword on a method without one (missing method body, or declare abstract).
  • Making an abstract method private, static or final.
  • Forgetting to implement one abstract method in a concrete subclass.
  • Calling overridable abstract methods from the abstract class's constructor: the subclass fields aren't initialised yet when the parent constructor runs.

What's next#

Abstract classes still tie you to single inheritance. Interfaces let a class sign up for many contracts at once, and since Java 8 they can even carry default method implementations.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What happens if you write new Shape() where Shape is declared abstract?

  2. 2.A concrete (non-abstract) class extends an abstract class but does not implement one of its abstract methods. What happens?

  3. 3.Which statement about abstract classes is TRUE?

Finished reading?

Mark this lesson complete to track your progress.