Interfaces and default methods
You get one extends. That is the constraint that makes interfaces necessary,
and it turns out to be a better design than the alternative — languages with
multiple inheritance spend a lot of effort on questions Java simply does not
have.
An interface says what a type can do, without saying anything about what it is.
The shape
interface Billable {
long amountPaise();
String description();
}
No bodies, no fields, no constructor. Methods on an interface are implicitly
public and abstract, so the keywords are usually left off.
class DeliveryCharge implements Billable {
private final long paise;
DeliveryCharge(long paise) {
this.paise = paise;
}
@Override
public long amountPaise() {
return paise;
}
@Override
public String description() {
return "Delivery";
}
}
public on the implementing methods is required, because an override may widen
access but never narrow it, and the interface's methods are already public.
Forgetting it produces attempting to assign weaker access privileges, which is
a confusing message for a common slip.
What it buys you
List<Billable> items = List.of(
new TiffinPlan(26),
new DeliveryCharge(3_500),
new LatePaymentFee());
long total = 0;
for (Billable b : items) {
System.out.printf("%-18s Rs %s%n", b.description(), rupees(b.amountPaise()));
total += b.amountPaise();
}
26 tiffins Rs 2,141.10
Delivery Rs 35.00
Late fee Rs 50.00
Total Rs 2,226.10
Three unrelated classes — one wraps a tiffin count, one a fixed amount, one nothing at all — and the billing code treats them identically. They share no parent. They do not have to.
A class can implement as many interfaces as it likes, which is the practical difference:
class TiffinPlan implements Billable, Printable { ... }
A TiffinPlan is billable and printable, and neither has consumed its one
extends.
default methods
Since Java 8, an interface method may have a body:
interface Billable {
long amountPaise();
String description();
default String summary() {
return description() + ": Rs " + rupeesOf(amountPaise());
}
}
Late fee: Rs 50.00
Every implementer gets summary() free and may override it.
Understand why this exists, because it shapes when you should use it. Java 8
added stream() and forEach to Collection. Without default methods, every
implementation of Collection in the world — including ones inside companies
that Oracle has never heard of — would have failed to compile overnight. Default
methods let an interface grow without breaking implementers.
So the honest guidance: default is for evolving an existing interface, and
for genuinely universal convenience methods built from the abstract ones. It is
not a way to share state, because interfaces have no fields, and using it to
smuggle in a base class produces something harder to follow than either.
Two relatives, both worth knowing:
interface Billable {
// Utility related to the type, no instance required
static String rupeesOf(long paise) {
return "%,d.%02d".formatted(paise / 100, paise % 100);
}
// Java 9+: shared by default methods, hidden from implementers
private static String pad(String s) {
return "%-18s".formatted(s);
}
}
static methods on an interface are not inherited — you call
Billable.rupeesOf(...), never somePlan.rupeesOf(...).
The diamond, and how Java resolves it
Two interfaces, the same default method:
interface Hindi { default String greet() { return "Namaste"; } }
interface Marathi { default String greet() { return "Namaskar"; } }
class Both implements Hindi, Marathi { }
error: types Hindi and Marathi are incompatible;
class Both implements Hindi, Marathi {
^
class Both inherits unrelated defaults for greet() from types Hindi and Marathi
Java refuses to choose. This is the multiple-inheritance problem, and the resolution is to make you say what you mean:
class Both implements Hindi, Marathi {
@Override
public String greet() {
return Hindi.super.greet() + " / " + Marathi.super.greet();
}
}
Namaste / Namaskar
Hindi.super.greet() is the syntax for "the default from that specific
interface". You will use it roughly once a career, and when you do, the error
message above is what sent you looking.
Constants in interfaces: don't
interface Config {
int MAX_TIFFINS = 62; // implicitly public static final
}
Legal. Every field in an interface is public static final whether you say so or
not. And it is a well-known anti-pattern, because a class that implements
Config purely to get unqualified access to the constants has published that
implementation detail in its type.
Put constants in a final class with a private constructor, or in an enum,
and refer to them by name. The enums lesson makes the stronger version of this
argument.
Interface or abstract class?
| Interface | Abstract class | |
|---|---|---|
| How many per class | Many | One |
| Fields | Constants only | Any, including mutable state |
| Constructors | None | Yes |
| Method bodies | default, static, private |
Any |
| Access levels | public (plus private helpers) |
Any |
| Expresses | "can do" | "is a" |
Default to an interface. Reach for an abstract class when subclasses must share state or constructor logic — which is the next lesson.
What interfaces are not for
- Marker interfaces with no methods, used only so
instanceofworks. Annotations do that job better, and sealed types do it best. - One interface per class, by reflex.
CustomerServicewith exactly one implementation calledCustomerServiceImplis a habit, not a design. Add the interface when there is a second implementation or a test double that needs one. - Holding state. They cannot, and trying to work around it is a sign the design wants a class.
A preview
An interface with exactly one abstract method is a functional interface, and Java lets you write one as a lambda:
interface Discount {
long applyTo(long paise);
}
Discount studentDiscount = paise -> paise * 90 / 100;
That is module 6's subject, and it is the single most common use of interfaces in
modern Java. Worth seeing now so that Comparator, Runnable and Predicate
look familiar when they arrive.
Check your work
Why must implementing methods be public? Interface methods are implicitly
public, and an override may widen access but never narrow it. Omitting it gives
attempting to assign weaker access privileges.
What can an interface hold that a class cannot, and what can it not hold? It
can be implemented many times over by one class. It cannot hold instance fields
or constructors — only public static final constants.
Why were default methods added? So an existing interface could gain methods
without breaking every implementation. Collection.stream() is the example that
forced it.
What happens when two interfaces supply the same default method? The
compiler refuses: inherits unrelated defaults. Override it and pick, using
Interface.super.method() if you want one of them.
Why is a constants interface a bad idea? Implementing it to get unqualified
access puts an implementation detail into the class's public type. Use a final
class or an enum.
When would you choose an abstract class over an interface? When subclasses must share mutable state or constructor logic. Otherwise prefer the interface.
Practice 2, the Billable bill. The total loop does not change when a fourth
kind of charge is added — that is the whole test. If adding Adjustment required
editing the loop, the interface was not doing its job and you probably had a
switch on a type somewhere.
Practice 4, the diamond. Without an override: class Both inherits unrelated
defaults for greet() from types Hindi and Marathi, at the class declaration, not
at the call site. With Hindi.super.greet() + " / " + Marathi.super.greet() it
prints Namaste / Namaskar. Note that the fix lives in the implementing class:
the interfaces cannot resolve it between themselves.
Practice 5, the interface worth deleting. A CustomerService interface with
one CustomerServiceImpl and no test double adds a file and a layer of
indirection and removes nothing. Delete it; add it back the day there is a second
implementation. The IDE's "extract interface" refactoring takes seconds when that
day comes.
Practice
-
Write
Billable.amountPaise()anddescription(). Implement it three times: a tiffin plan, a flat delivery charge and a late fee. -
Print a bill. Put them in a
List<Billable>, print each line and a total. Then add a fourth kind of charge — a negativeAdjustmentfor a missed delivery — and confirm the printing code needs no change at all. -
Add a
default. GiveBillableasummary()built from the two abstract methods. Override it in exactly one implementation and confirm both versions are used. -
Cause the diamond. Two interfaces with the same default method, one class implementing both. Read the compile error, then fix it with
Interface.super.method(). -
Delete an interface. Find or write a
CustomerServicewith a singleCustomerServiceImpl. Delete the interface, rename the class, and see what is lost. Write one sentence on when you would add it back. -
Harder — sort by different rules. Write
Comparator<Subscriber>implementations as separate classes: by name, by tiffin count descending, by pincode then name. Sort the same list three ways withlist.sort(comparator). Then look at how little each class contains and guess why module 6 replaces all three with one line each.
Next: abstract classes — the middle ground, and the narrow case where they beat an interface.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship