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
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; }}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 testsVehicle 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], [])
Sign up to check the hidden tests and save your progress. Sign up