Module 6 · Inheritance, Polymorphism & Design ⏱ 19 min

Abstract Classes vs Interfaces (default methods)

By the end of this lesson you will be able to:
  • Declare an abstract class with abstract and concrete methods, and explain why it cannot be instantiated
  • Use a default method in an interface and predict whether it is inherited or overridden
  • Apply the class-wins rule to predict which method runs when candidates collide

You have a Shape concept, and you know every real shape can report its area() — but there is no such thing as the area of a generic shape. The formula only exists once you know it is a square or a circle. At the same time, you want every shape to share a describe() method that prints its area, so you are not copying that line into every subclass.

Java's answer is the abstract class: a class that is deliberately incomplete. It can declare a method with no body — abstract double area(); — and leave the implementation to each subclass, while still shipping concrete methods and fields that all subclasses reuse. You cannot instantiate an abstract class directly, because by design it is missing pieces.

flowchart TD
  A["abstract class Shape"] --> B["area — abstract, no body"]
  A --> C["describe — concrete, shared"]
  B --> S["subclass must implement area"]
  style A fill:#c2410c,color:#fff
  style B fill:#b45309,color:#fff
An abstract class mixes abstract methods (no body, subclasses must fill in) with concrete ones (shared as-is).

What an abstract class may contain

An abstract class can hold a mix of three things: abstract methods (a signature with no body, which every non-abstract subclass must implement), concrete methods (ordinary methods with bodies, shared as-is by subclasses), and fields plus a constructor (run when a subclass object is created, through super).

Marking the class abstract does two jobs at once: it permits method signatures without bodies, and it forbids new Shape() — you may only build a concrete subclass such as new Square(4). The shared concrete method is where abstract classes earn their keep. describe() can call area() even though Shape never defines area's body, because dispatch finds the subclass's version at runtime. One shared method, many correct areas.

Main.java — an abstract Shape with an abstract area() and a concrete describe() that calls it. Real, compiling Java.
public class Main {
    abstract static class Shape {
        abstract double area();

        String describe() {
            return "Shape with area " + area();
        }
    }

    static class Square extends Shape {
        private double side;

        Square(double side) {
            this.side = side;
        }

        @Override
        double area() {
            return side * side;
        }
    }

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

Default methods: a body inside an interface

Since Java 8, an interface can give a method a body too, by marking it default. A class that implements the interface inherits that body unless it overrides it. This lets you add a method to an interface without breaking the thousands of classes that already implement it — they simply pick up the default.

The mental model is optional, overridable behaviour. A class that is happy with the default does nothing; a class that needs something different overrides it. An interface still cannot hold instance state (no instance fields) and cannot be instantiated on its own — those remain the exclusive privileges of a class. Reach for a default method when several unrelated classes would otherwise copy the same trivial behaviour into each one.

flowchart LR
  I["interface Greeter"] --> D["default greet — has a body"]
  D --> U["implementing class inherits it unless it overrides"]
  style I fill:#3776ab,color:#fff
A default method gives an interface method a body; an implementing class may inherit it as-is or override it.
Main.java — a default method inherited by one class and overridden by another. Real, compiling Java.
public class Main {
    interface Greeter {
        default String greet() {
            return "Hello from interface default";
        }
    }

    static class Polite implements Greeter {
        @Override
        public String greet() {
            return "Hello from class";
        }
    }

    static class Lazy implements Greeter {
    }

    public static void main(String[] args) {
        Greeter[] gs = { new Polite(), new Lazy() };
        for (Greeter g : gs) {
            System.out.println(g.greet());
        }
    }
}

Choosing between them

Reach for an abstract class when your types are genuinely the same kind of thing — they share identity, state, and meaningful code. A Square really is a Shape, and both benefit from shared fields and a constructor. Reach for an interface when unrelated types merely share a capability: a Reporter and a Logger may both be Closeable without being related at all.

The hard rule that often decides it: a class can extends only one class but implements many interfaces. If you spend your one inheritance slot on shared code, you have nowhere left to hang the capabilities your class also needs. Interfaces compose freely; abstract classes do not. When in doubt, prefer the interface and let a helper or abstract class supply shared code alongside it.

flowchart TD
  Q["Child extends Base, implements Extra"] --> W{"which greet wins?"}
  W -->|"class wins"| B["Base.greet — the superclass method"]
  W -->|"no superclass method"| D["Extra default"]
  style B fill:#c2410c,color:#fff
The class-wins rule: a concrete superclass method beats a conflicting interface default, with no error raised.
Main.java — a superclass method and an interface default collide; the class always wins. Real, compiling Java.
public class Main {
    static class Base {
        public String greet() {
            return "Hello from Base";
        }
    }

    interface Extra {
        default String greet() {
            return "Hello from Extra default";
        }
    }

    static class Child extends Base implements Extra {
    }

    public static void main(String[] args) {
        Child c = new Child();
        System.out.println(c.greet());
    }
}
Exercise

What does this program print? An abstract class with a concrete method that calls the abstract one. Predict both lines.

public class Main {
    abstract static class Vehicle {
        abstract int wheels();

        String describe() {
            return "has " + wheels() + " wheels";
        }
    }

    static class Car extends Vehicle {
        @Override
        int wheels() { return 4; }
    }

    static class Bike extends Vehicle {
        @Override
        int wheels() { return 2; }
    }

    public static void main(String[] args) {
        Vehicle[] vs = { new Car(), new Bike() };
        for (Vehicle v : vs) {
            System.out.println(v.describe());
        }
    }
}
Exercise

Why can you NOT write new Shape() when Shape is an abstract class?

Exercise

Default method check. One class overrides tag(), the other inherits the default. Predict both lines.

public class Main {
    interface Logger {
        default String tag() {
            return "LOG";
        }
    }

    static class PlainLogger implements Logger {
    }

    static class AuditLogger implements Logger {
        @Override
        public String tag() {
            return "AUDIT";
        }
    }

    public static void main(String[] args) {
        Logger[] ls = { new AuditLogger(), new PlainLogger() };
        for (Logger l : ls) {
            System.out.println(l.tag());
        }
    }
}
Exercise

Class-wins trap. Robot extends a class with status() AND implements an interface whose default is also status(). What prints?

public class Main {
    static class Machine {
        public String status() {
            return "machine: idle";
        }
    }

    interface Reportable {
        default String status() {
            return "report: idle";
        }
    }

    static class Robot extends Machine implements Reportable {
    }

    public static void main(String[] args) {
        Robot r = new Robot();
        System.out.println(r.status());
    }
}
Exercise

A class implements two interfaces, and BOTH declare a default method ping() with a body. The class does not override ping(). What happens?

Recap

  • An abstract class can mix abstract methods (no body) with concrete ones, fields, and a constructor — and it cannot be instantiated directly.
  • A default method gives an interface method a body; implementers inherit it unless they override it.
  • When a superclass method and an interface default collide, the class always wins — no error is raised.
  • When two interface defaults collide, the class must override to disambiguate, or the build fails.
  • Prefer interfaces for shared capabilities across unrelated types; reserve abstract classes for genuinely related types that share state and code.

Next you will see why leaning on inheritance for everything leads to brittle designs, and how composition — holding and delegating to other objects — often produces code that is far easier to change.

Checkpoint quiz

What does marking a class abstract let you do that a normal class forbids?

A class extends Base (concrete greet()) and implements Iface (default greet()), overriding nothing. Which greet() runs?

Go deeper — technical resources