What you'll learn

  • extends, super and @Override
  • Abstract classes vs interfaces
  • Default methods in interfaces
  • Polymorphism and pattern matching with instanceof

Real systems have families of related things: circles, rectangles and squares are all shapes; NetSuite, Salesforce and HubSpot connectors are all connectors. Java gives you three tools to model that: inheritance, interfaces and polymorphism.

Inheritance: extends

class Rectangle extends Shape {
    protected final double width, height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }

    @Override
    double area() { return width * height; }
}

class Square extends Rectangle {
    Square(double side) { super(side, side); }   // call the parent's constructor
}

A Square is a Rectangle: it inherits its fields and methods. super(...) calls the parent’s constructor; super.method() calls the parent’s version of a method you’ve overridden.

@Override tells the compiler “I mean to replace a parent method” — if you misspell the name, you get an error instead of a silent new method.

Abstract classes

abstract class Shape implements Describable {
    abstract double area();                // every shape must implement this

    public String describe() {             // shared behaviour
        return getClass().getSimpleName() + " with area " + area();
    }
}

You can’t create a plain Shape — only its concrete subclasses. Abstract classes are good for sharing code and forcing subclasses to fill in the gaps.

Interfaces: a contract

interface Describable {
    String describe();

    default String label() {               // default method (Java 8+)
        return "[" + describe() + "]";
    }
}

An interface says what a type can do, not how. A class can extend only one class but implement many interfaces — so interfaces are the main way Java models “can do” relationships (Comparable, Runnable, AutoCloseable…). Default methods let interfaces carry shared behaviour too.

Abstract classInterface
Fieldsanyconstants only
Constructorsyesno
A class can useonemany
Use it fora family sharing code and statea capability or contract

Interfaces with a single abstract method are functional interfaces — the foundation of lambda expressions.

Polymorphism: one call, many behaviours

List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3), new Square(2));
for (Shape s : shapes) {
    System.out.println(s.label());   // each shape runs its own area()/describe()
}

The loop only knows it has Shapes, yet each object runs its own version of area(). Add a Triangle tomorrow and this loop works unchanged — that’s the real payoff.

Checking the actual type: pattern matching

if (s instanceof Square sq) {        // Java 16+: test and cast in one step
    System.out.println(sq.width);
}

Needing many instanceof checks is often a hint that the behaviour belongs in a method on the classes themselves.

The full example

// Shapes.java — inheritance, abstract classes, interfaces and polymorphism.
import java.util.List;

public class Shapes {
    public static void main(String[] args) {
        List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3), new Square(2));

        double total = 0;
        for (Shape s : shapes) {          // one loop, many shapes: polymorphism
            System.out.println(s.label());
            total += s.area();
        }
        System.out.printf("Total area: %.2f%n", total);

        Shape s = shapes.get(2);
        if (s instanceof Square sq) {     // pattern matching for instanceof (Java 16+)
            System.out.println("Found a square: " + sq.width);
        }
    }
}

interface Describable {
    String describe();

    // Default methods give interfaces shared behaviour (Java 8+)
    default String label() {
        return "[" + describe() + "]";
    }
}

abstract class Shape implements Describable {
    abstract double area();

    @Override
    public String describe() {
        return getClass().getSimpleName() + " with area " + String.format("%.2f", area());
    }
}

class Circle extends Shape {
    private final double radius;
    Circle(double radius) { this.radius = radius; }

    @Override
    double area() { return Math.PI * radius * radius; }
}

class Rectangle extends Shape {
    protected final double width, height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }

    @Override
    double area() { return width * height; }
}

class Square extends Rectangle {
    Square(double side) { super(side, side); }   // call the parent constructor

    @Override
    public String describe() {
        return "Square " + width + "x" + width + " -> " + super.describe();
    }
}

Try it yourself

Add a Triangle class with a base and height (area = base × height ÷ 2) and add one to the list. Nothing else should need to change.

Show a solution
class Triangle extends Shape {
    private final double base, height;
    Triangle(double base, double height) { this.base = base; this.height = height; }

    @Override
    double area() { return base * height / 2; }
}
// List.of(new Circle(1), new Rectangle(2, 3), new Square(2), new Triangle(3, 4))

Key takeaways

  • extends reuses a parent class; super reaches the parent; @Override guards against typos.
  • Abstract classes share code; interfaces define contracts — and a class can implement many.
  • Polymorphism lets one piece of code work with every subtype.
  • Prefer methods on the types over long chains of instanceof.

Example files