Abstract Classes
Abstract classes sit between interfaces and concrete classes: they can’t be instantiated, can declare abstract methods that subclasses must implement, and can also provide shared concrete behaviour. They’re the right tool for template-method patterns and family-of-types with common state.
abstract class, template, sealed, modern
EXAMPLE
// 1) Abstract class — can't be instantiated
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
// Concrete method shared by all subclasses
public String describe() {
return String.format("%s area=%.2f perimeter=%.2f",
getClass().getSimpleName(), area(), perimeter());
}
}
public class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }
@Override public double perimeter() { return 2 * Math.PI * radius; }
}
public class Rectangle extends Shape {
private final double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override public double area() { return width * height; }
@Override public double perimeter() { return 2 * (width + height); }
}
// 2) Template method pattern — algorithm with hooks
public abstract class JobBase {
private final String name;
protected JobBase(String name) { this.name = name; }
public final void run() {
long start = System.nanoTime();
System.out.println("starting " + name);
try {
execute(); // subclass implements
System.out.println("completed in " + (System.nanoTime() - start) / 1_000_000 + " ms");
} catch (Exception e) {
System.err.println(name + " failed: " + e.getMessage());
throw e;
}
}
protected abstract void execute();
}
public class CleanupJob extends JobBase {
public CleanupJob() { super("cleanup"); }
@Override protected void execute() { /* … */ }
}
// run() is final — subclasses can't change the algorithm; only execute() is customisable.
// 3) Abstract class with state + constructor
public abstract class Animal {
private final String name;
private final int birthYear;
protected Animal(String name, int birthYear) {
this.name = name;
this.birthYear = birthYear;
}
public String name() { return name; }
public int age() { return java.time.Year.now().getValue() - birthYear; }
public abstract String sound();
}
public class Dog extends Animal {
public Dog(String name, int birthYear) { super(name, birthYear); }
@Override public String sound() { return "woof"; }
}
// 4) Abstract class vs interface — choose wisely
// abstract class WINS when:
// • Subclasses share concrete state or behaviour
// • You need protected members
// • A constructor enforces invariants for all subclasses
// • Single-inheritance hierarchy makes sense (Java is single-inheritance for classes)
//
// interface WINS when:
// • Pure contract; many unrelated types implement it
// • Need multiple inheritance of behaviour (Java is multi-interface)
// • Default methods are enough for shared logic
// 5) Combining abstract + interface
public interface Drawable {
void draw(java.awt.Graphics g);
}
public abstract class Widget implements Drawable {
protected int x, y, width, height;
public Widget(int x, int y, int w, int h) {
this.x = x; this.y = y; this.width = w; this.height = h;
}
public boolean contains(int px, int py) {
return px >= x && px <= x + width && py >= y && py <= y + height;
}
}
public class Button extends Widget {
private final String label;
public Button(int x, int y, int w, int h, String label) { super(x, y, w, h); this.label = label; }
@Override public void draw(java.awt.Graphics g) { g.drawRect(x, y, width, height); g.drawString(label, x + 4, y + 16); }
}
// 6) Sealed hierarchies (Java 17+) — closed family
public sealed abstract class Event
permits OrderPlaced, OrderShipped, OrderCancelled {
private final java.time.Instant at = java.time.Instant.now();
public java.time.Instant at() { return at; }
}
public final class OrderPlaced extends Event {
private final String orderId;
public OrderPlaced(String id) { this.orderId = id; }
public String orderId() { return orderId; }
}
public final class OrderShipped extends Event {
private final String orderId;
public OrderShipped(String id) { this.orderId = id; }
public String orderId() { return orderId; }
}
public final class OrderCancelled extends Event {
private final String orderId;
private final String reason;
public OrderCancelled(String id, String reason) { this.orderId = id; this.reason = reason; }
public String orderId() { return orderId; }
public String reason() { return reason; }
}
// Pattern matching switch (Java 21+) — exhaustive on the sealed hierarchy
static String describe(Event e) {
return switch (e) {
case OrderPlaced p -> "placed " + p.orderId();
case OrderShipped s -> "shipped " + s.orderId();
case OrderCancelled c -> "cancelled " + c.orderId() + " (" + c.reason() + ")";
};
}
// 7) Records vs abstract classes
// Records are FINAL — they can't be abstract.
// For data carriers, use records. For families needing custom behaviour, use an abstract base + records:
public sealed interface Shape2 permits Circle2, Rect2 {}
public record Circle2(double r) implements Shape2 {}
public record Rect2(double w, double h) implements Shape2 {}
// 8) Avoid these mistakes
// • Adding abstract methods later — breaks every existing subclass
// Mitigation: provide a default implementation (mark non-abstract with a sensible default)
// • Long constructor parameter lists — use a builder or composition
// • Putting business logic in the abstract class that varies by subclass — that belongs in the subclass
// • Overriding non-abstract methods to throw UnsupportedOperationException — LSP violation
// • Calling virtual methods from the constructor — runs subclass code on a half-built object
// 9) Common bugs
// • Forgot 'extends' on a subclass — Java treats it as inheriting Object only
// • Abstract method declared but no @Override on subclass — typo creates a new method
// • Mismatched signatures — different exception clauses; subclass method must throw subset
// • protected vs private fields — protected leaks to subclasses; default to private + getters
// • Cannot create instance with new BaseClass() — abstract; create concrete subclass
// • Cannot add a permits clause that includes unrelated class — must be in same module
// • Forgotten constructor super(...) call — Java auto-calls no-arg; if base has none, error
Why it matters
Reach for an abstract class when subclasses share concrete state, constructors enforcing invariants, or a template-method algorithm. Pair with sealed permits + records for closed, exhaustive hierarchies. If you only need a pure contract or multi-inheritance of behaviour, interfaces (with default methods) are usually the cleaner choice.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
}
Try it Yourself »
Discussion
Loading…