RizTech Academy logo
RizTech Academy
Object-Oriented JavaLesson 4 of 830 min

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 instanceof works. Annotations do that job better, and sealed types do it best.
  • One interface per class, by reflex. CustomerService with exactly one implementation called CustomerServiceImpl is 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

  1. Write Billable. amountPaise() and description(). Implement it three times: a tiffin plan, a flat delivery charge and a late fee.

  2. Print a bill. Put them in a List<Billable>, print each line and a total. Then add a fourth kind of charge — a negative Adjustment for a missed delivery — and confirm the printing code needs no change at all.

  3. Add a default. Give Billable a summary() built from the two abstract methods. Override it in exactly one implementation and confirm both versions are used.

  4. 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().

  5. Delete an interface. Find or write a CustomerService with a single CustomerServiceImpl. Delete the interface, rename the class, and see what is lost. Write one sentence on when you would add it back.

  6. 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 with list.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