iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…