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 Pricing {
long unitCents(int quantity);
default long totalCents(int quantity) { // a real, shared implementation
return unitCents(quantity) * quantity;
}
}
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 Pricing { long unitCents(int quantity); default long totalCents(int quantity) { return unitCents(quantity) * quantity; } } record Flat(long cents) implements Pricing { public long unitCents(int quantity) { return cents; } } record Bulk(long cents, int threshold, long discounted) implements Pricing { public long unitCents(int quantity) { return quantity >= threshold ? discounted : cents; } } public static List<String> describe() { List<Pricing> plans = List.of(new Flat(250), new Bulk(250, 10, 200)); List<String> out = new ArrayList<>(); for (Pricing p : plans) { out.add(p.getClass().getSimpleName() + " x12=" + p.totalCents(12) + " x3=" + p.totalCents(3)); } return out; }}Polymorphism: one call site, many behaviours
List<Pricing> plans = List.of(new Flat(250), new Bulk(250, 10, 200));
for (Pricing p : plans) {
long total = p.totalCents(12); // which unitCents() runs is decided at runtime
}
The loop does not know or care whether each element is a Flat or a
Bulk plan. totalCents calls unitCents, and that call 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
Pricing. This is what makes adding a Seasonal plan later free at
every existing call site: nothing that already calls totalCents on a
Pricing 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.
When two interfaces bring the same default
A class may implement two interfaces that both supply a default method with the same signature. The compiler refuses to pick one: the class must override the method, and inside it can call either parent's version explicitly with .
public class Solution { interface Walker { default String move() { return "walks"; } } interface Swimmer { default String move() { return "swims"; } } static class Duck implements Walker, Swimmer { // Without this override the class does not compile. @Override public String move() { return Walker.super.move() + " and " + Swimmer.super.move(); } } public static String describe() { return "duck " + new Duck().move(); }}Common mistakes
- Calling an overridable method from a constructor. The superclass
constructor runs before the rest of the subclass's constructor, so the
overriding method sees the subclass's fields still at
null/0. - Expecting overloads to dispatch at runtime. Which overload runs is chosen at compile time from the declared types of the arguments; only overriding is decided by the object's runtime class.
- Reaching for an abstract class just to share one helper method. A
defaultmethod (or a static helper) does that without spending the class's single superclass slot.
public class Solution { static abstract class Widget { final String label; Widget() { label = "widget:" + name(); // calls the subclass override } abstract String name(); } static class Button extends Widget { private final String text; Button() { super(); // Widget() runs here, before the next line text = "OK"; } @Override String name() { return text; } } public static String describe() { Button b = new Button(); return b.label + " / name() now=" + b.name(); }}What interviewers ask
- Now that interfaces have default methods, why use an abstract class at all? Interfaces still cannot hold instance fields or constructors; an abstract class can carry state and enforce construction, at the cost of single inheritance.
- What is the difference between overloading and overriding? Overloads are different signatures resolved by the compiler; an override replaces a superclass or interface method and is selected at runtime.
- A class implements two interfaces with the same default method. What
happens? It does not compile until the class overrides the method;
it may delegate with
A.super.m().
Exercise · Pro
A shared default, overridden by exactly one implementer
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade