Java

Interfaces, abstract classes, and polymorphism

What each one can do that the other cannot, and why calling through an interface is the default.

After this lesson you can

  • Choose between an interface and an abstract class for a given design
  • Write a default method and say when one is appropriate
  • Explain what polymorphism buys you at a call site

An interface declares what a type can do; it cannot hold instance state. An abstract class can hold state and share real implementation, but a class can only extend one of them — Java has single inheritance for classes, unlimited for interfaces.

interface Shape {
    double area();
    default String describe() {          // a real, shared implementation
        return "area=" + area();
    }
}

abstract class Animal {
    protected final String name;          // real state
    Animal(String name) { this.name = name; }
    abstract String sound();               // no implementation yet
    String describe() { return name + " says " + sound(); }
}

A default method on an interface gives it a body that implementers inherit for free and may override — the mechanism that let the collections framework add forEach and stream() to every existing List without breaking a single class that already implemented it.

Try it

One loop, two different area formulasjava-21
import java.util.*; public class Solution {    interface Shape {        double area();    }     record Circle(double radius) implements Shape {        public double area() { return Math.round(Math.PI * radius * radius * 100) / 100.0; }    }     record Square(double side) implements Shape {        public double area() { return side * side; }    }     public static String describe() {        List<Shape> shapes = List.of(new Circle(2), new Square(3));        double total = 0;        for (Shape s : shapes) total += s.area();        return "total=" + total;    }}
What to look for

Polymorphism: one call site, many behaviours

List<Shape> shapes = List.of(new Circle(2), new Square(3));
double total = 0;
for (Shape s : shapes) {
    total += s.area();   // which area() runs is decided at runtime
}

The loop does not know or care whether each element is a Circle or a Square. s.area() dispatches to whichever class's implementation the object actually is, decided at runtime by the object's real type, not by the variable's declared type Shape. This is what makes adding a Triangle later free at every existing call site: nothing that already calls .area() on a Shape needs to change.

Programming to the interface

List<String> names = new ArrayList<>();   // not: ArrayList<String> names

Declaring the variable as the interface type, even though the concrete class is known, is the convention for a reason: every place that reads names only ever depends on what List promises, so swapping ArrayList for LinkedList later touches one line instead of every call site that happened to know the concrete type.

Try it yourself

2 visible tests · 2 hidden tests

Vehicle declares speedKph() and a default method description() that reads "moves at " + speedKph() + " kph". Two records implement it: Car, which uses the default description as-is, and Bicycle, which overrides description() to return "pedals at " + speedKph() + " kph" instead. Implement describeAll(carSpeeds, bikeSpeeds): build one Car per value in carSpeeds and one Bicycle per value in bikeSpeeds, in that order, and return the list of every vehicle's description().

  • describeAll([120], [18])
  • describeAll([100,130], [])
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up