Chapter 6 of 18
06 — Interfaces
Polymorphism behind a Shape contract, tested through the interface.
Kata: take the shapes from structs and make them polymorphic behind a Shape interface,
then format them into a table.
New ideas: interface types, @Override, behaviour revealed by tests.
Why interfaces matter
An interface is a named contract: “any object that implements this promises these methods.”
One interface, many implementations — Shape is the canonical example.
public interface Shape {
String name();
double area();
}
Step 1 — test the polymorphism from the start
Notice: declare the variable as the interface, not the concrete record. The test then proves
you can use a Rectangle where a Shape is asked for:
@Test
void rectangleArea() {
Shape rectangle = new Rectangle(3.0, 4.0);
assertEquals(12.0, rectangle.area(), 0.0001);
}
RED — Shape doesn’t exist. Write the interface, then make the records implement it:
public record Rectangle(double width, double height) implements Shape {
@Override
public String name() { return "Rectangle"; }
@Override
public double area() { return width * height; }
}
GREEN. Shape rectangle = new Rectangle(...) — a variable with interface type holding a
concrete value. That’s the moment the record implements a contract.
Step 2 — a function that doesn’t care what it gets
The goal: a function that formats any shape without knowing what area() does underneath.
That’s your AreaTable.render:
public static String render(List<Shape> shapes) {
StringBuilder out = new StringBuilder();
for (Shape shape : shapes) {
out.append(String.format("%-12s %.1f%n", shape.name(), shape.area()));
}
return out.toString();
}
shape.name() and shape.area() — the runtime type decides the answer. Feed it a mix:
@Test
void rendersMixedShapesIntoAColumn() {
List<Shape> shapes = List.of(new Rectangle(3.0, 4.0), new Circle(2.0));
String table = AreaTable.render(shapes);
String expected = String.format("%-12s %.1f%n", "Rectangle", 12.0)
+ String.format("%-12s %.1f%n", "Circle", 4 * Math.PI);
assertEquals(expected, table);
}
GREEN — one List<Shape>, two different area() implementations, one render does both.
Java-specific notes
String.format("%-12s %.1f%n", ...)is the printf-style formatter: left-align 12 wide, one decimal, platform line separator. Ifexpecteduses\nyou may trip on Windows — build it with the same%nasrenderdoes (as above).var(Java 10+) —var rectangle = new Rectangle(...)is legal; but hereShape rectangleis not the same, it’s the point: program to the interface.@Overrideis a compile-time check, not a comment. Forgetting the exact signature fails the build — that’s what makes refactoring interfaces safe.
The interview lesson
“I’d define an interface, inject it, and have a real impl and a fake impl for tests” — the exact
sentence interviewers want to hear. Shape is that design in miniature: the test passes a
concrete Rectangle through an interface-typed seam, and AreaTable never learns about records.
Run it
gradle test --tests "interfaces.*"
Key takeaway: polymorphism is a runtime decision (shape.area() dispatches by actual type)
and an interface is the seam that makes your code testable. You just wrote dependency-free
polymorphism — the seed of dependency injection (chapter 11).
Next: 07 — maps — hash maps and word counting.