Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

Default & Static Methods in Interfaces

Why default methods were added, the three resolution rules when a class inherits conflicting defaults, calling a specific supertype with X.super.method(), and private interface methods.

Intermediate6 min readUpdated
On this page

Default methods look like a convenience feature and were actually a compatibility mechanism. The Java 8 library designers needed to add stream() to Collection, and there was no way to do it without breaking the world.

Key Takeaways

  • Default methods exist for interface evolution: adding a method to a published interface without breaking implementers.
  • Resolution order: class wins over interface, then most specific interface, then the class must override.
  • Interface.super.method() calls a specific supertype's default explicitly.
  • Interfaces gained behaviour but still cannot hold instance state — that is why this is not full multiple inheritance.
  • Since Java 9, interfaces can have private methods to share code between defaults.

The problem they solved

what Java 8 needed to do
public interface Collection<E> extends Iterable<E> {
    // ... every method that existed in Java 7 ...
 
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
 
    default boolean removeIf(Predicate<? super E> filter) {
        boolean removed = false;
        Iterator<E> it = iterator();
        while (it.hasNext()) {
            if (filter.test(it.next())) { it.remove(); removed = true; }
        }
        return removed;
    }
 
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }
}

Before Java 8, adding any of those would have broken every class implementing Collection — in the JDK, in every framework, and in every application. Default methods made the addition source- and binary-compatible: an existing implementation that knows nothing about stream() inherits a working one.

They also enabled the functional interfaces to carry composition helpers. Predicate stays functional — one abstract method, test — while providing and, or and negate as defaults, and not and isEqual as statics.

Static methods on interfaces

factory methods where they belong
public interface Validator<T> {
    List<String> validate(T target);                     // the abstract method
 
    static <T> Validator<T> alwaysValid() {              // static: not inherited
        return t -> List.of();
    }
 
    static <T> Validator<T> requireNonNull(String field, Function<T, ?> getter) {
        return t -> getter.apply(t) == null ? List.of(field + " is required") : List.of();
    }
}
 
Validator<Order> v = Validator.requireNonNull("customer", Order::customer);

Static interface methods are not inheritedSomeImplementation.alwaysValid() does not compile. They are called on the interface itself. Their purpose is to eliminate the companion utility class: Comparator.comparing, List.of, Map.entry, Stream.of and Predicate.not all live on the interface rather than in a Collections-style helper.

The diamond, and the three rules

Conflict.java
interface Greeter {
    default String greet() { return "hello from Greeter"; }
}
 
interface Shouter {
    default String greet() { return "HELLO FROM SHOUTER"; }
}
 
// Does not compile:
//   class Both implements Greeter, Shouter { }
//   error: types Greeter and Shouter are incompatible;
//          class Both inherits unrelated defaults for greet()
 
class Both implements Greeter, Shouter {
    @Override
    public String greet() {
        // Explicitly choose, or combine. Interface.super.method() is the syntax.
        return Greeter.super.greet() + " / " + Shouter.super.greet();
    }
}

The compiler refuses to pick for you. That refusal is the design: an ambiguity that resolves silently would be a source of bugs on every future library upgrade.

The full resolution algorithm has three steps, applied in order:

Class wins, then most specific interface, then the compiler makes you decide.

Rule 1 — the class wins. A concrete method inherited from a superclass beats any interface default, even if the interface is "closer" in the declaration:

class wins
class Base {
    public String greet() { return "from Base"; }
}
 
class Derived extends Base implements Greeter { }
 
new Derived().greet();   // "from Base" — Greeter's default is not used

This rule exists for compatibility: adding a default method to an interface must never change the behaviour of an existing class that already has that method.

Rule 2 — the most specific interface wins.

specificity
interface Loud extends Greeter {
    @Override default String greet() { return "LOUD"; }
}
 
class Ok implements Greeter, Loud { }    // compiles
 
new Ok().greet();   // "LOUD" — Loud is more specific than Greeter

Rule 3 — otherwise, override. As in the first example. And note that the override may re-abstract the method:

re-abstracting
interface Strict extends Greeter {
    @Override String greet();     // no body — forces implementers to provide one
}

Private interface methods

Java 9 added private and private static methods to interfaces, closing an obvious gap: before that, two default methods sharing logic had to either duplicate it or expose a public helper that became part of the API forever.

EventSink.java
public interface EventSink {
    void write(String payload);
 
    default void info(String message)  { emit("INFO", message); }
    default void error(String message) { emit("ERROR", message); }
 
    // Private: shared by the defaults, invisible to implementers and callers.
    private void emit(String level, String message) {
        write("[" + level + "] " + Instant.now() + " " + message);
    }
 
    private static String truncate(String s, int max) {
        return s.length() <= max ? s : s.substring(0, max) + "...";
    }
}

What they are still not

Interfaces cannot declare instance fields. Every field in an interface is implicitly public static final, so there is no inherited state and therefore no diamond problem for state — which is the hard version of multiple inheritance that Java deliberately avoided.

the boundary
interface Counter {
    int count = 0;              // public static final — shared by ALL implementers
    // private int count;       // does not compile
 
    default void increment() {
        // count++;             // does not compile: cannot assign a final field
    }
}

That single restriction is the whole answer to "does Java 8 have multiple inheritance?" — behaviour yes, state no.

Interfaces also cannot have constructors, cannot declare protected or package-private members, and cannot override Object methods with a default. That last one is worth knowing: writing default String toString() in an interface is a compile error, because Object's implementation would win by rule 1 anyway and allowing the declaration would only mislead.

Abstract class or interface, revisited

With defaults in the picture, the decision reduces to one question — does it need instance state or a constructor?

NeedChoose
Mutable or immutable instance fieldsAbstract class
Constructor logic, or super(...) chainingAbstract class
A capability that unrelated types may also wantInterface
Multiple supertypesInterface
A published API you expect to extend laterInterface with defaults

The JDK uses both together deliberately: List is the interface everything is written against, and AbstractList is a skeleton that turns twelve methods into two for anyone implementing it. Naming that pairing is a good way to close the answer.

Interview shortlist

Expect: why default methods were added (interface evolution, and the Collection.stream() example); what happens with two conflicting defaults and how to resolve it; whether this is multiple inheritance; and what a static interface method is for. The strongest answer to the first one names the concrete problem — stream() on Collection — rather than the abstract capability.

Frequently Asked Questions

Why were default methods added to Java 8?
To allow the JDK to add stream(), forEach(), removeIf() and spliterator() to the Collection interfaces without breaking every implementation ever written. Before Java 8, adding a method to a published interface was a binary-incompatible change: every implementing class stopped compiling. Default methods made interface evolution possible, and lambdas were the reason the JDK needed it.
How does Java resolve conflicting default methods?
Three rules in order. First, a concrete method inherited from a superclass always wins over any interface default. Second, the most specific interface wins — if one interface extends the other, the subinterface default is used. Third, if neither applies the class must override the method explicitly, and can delegate with Interface.super.method(). The compiler refuses to guess.
Does this make Java support multiple inheritance?
Multiple inheritance of behaviour, yes; of state, no. A class can inherit method bodies from several interfaces, but interfaces still cannot declare instance fields, so there is no diamond problem for state — which is the version of the problem that made C++ multiple inheritance difficult. That distinction is exactly what interviewers are checking when they ask.

Related tutorials