Default and Static Interface Methods

Add behaviour to interfaces with default, static and private methods, evolve an interface without breaking implementers, and resolve conflicts.

Syntaxinterface Name { default R helper() { return core(); } static Name create() { ... } private R shared() { ... } // Java 9+ }

Since Java 8 an interface can carry code. A default method has a body that every implementing class inherits unless it overrides it. A static method belongs to the interface itself, like a static method on a class. Java 9 added private methods so several defaults can share a helper without exposing it.

interface Repository<T> {
    Optional<T> findById(long id);                 // abstract

    default T findByIdOrThrow(long id) {           // inherited
        return findById(id).orElseThrow(...);
    }

    static <T> Repository<T> empty() {             // Repository.empty()
        return id -> Optional.empty();
    }
}

Why defaults exist

Adding an abstract method to a published interface breaks every class that implements it. A default method avoids that: old implementations keep compiling and get the new behaviour. That is how Collection.stream(), List.sort, Iterable.forEach and Map.getOrDefault were added without breaking every collection library. In your own code, the same trick lets you add findByIdOrThrow to a repository interface that ten classes already implement.

Static methods

Static interface methods are factories and helpers tied to the type: List.of, Map.entry, Comparator.comparing. They are not inherited. You call them as Comparator.comparing(...); ArrayList.of(...) does not compile even though ArrayList implements List.

Conflict rules

  1. A method from a class wins over any default (class wins).
  2. Otherwise the default from the more specific interface wins: if B extends A, B's version is used.
  3. If two unrelated interfaces supply the same default, the class must override it. Inside the override, A.super.method() calls one interface's version.

Limits and mistakes

  • Interfaces have no instance fields, so a default can only build on the interface's other methods. If it needs state, the design wants an abstract class or composition.
  • A default cannot override equals, hashCode or toString; the compiler rejects it, because the class's version from Object would always win.
  • Treating defaults as a way to get multiple inheritance of behaviour. Small helpers built on the abstract methods are the sweet spot; business logic belongs in services.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Add a findByIdOrThrow default to a repository interface that many classes already implement.
  • Offer a static factory such as PaymentGateway.sandbox() next to the interface it creates.
  • Build sort orders for an API's ?sort=customer,-total parameter by chaining Comparator defaults.

More examples

Resolving two conflicting defaults

Without the override, ProductService would not compile. X.super.method() is the only way to reach a specific interface's default from the implementing class.

Example · java
interface Auditable {
    default String label() { return "audited"; }
}

interface Cacheable {
    default String label() { return "cached"; }
}

// Both interfaces supply label(), so the class must decide.
class ProductService implements Auditable, Cacheable {
    @Override
    public String label() {
        return Auditable.super.label() + "+" + Cacheable.super.label();
    }
}

class Base {
    public String label() { return "from class"; }
}

// Class wins: Base.label() beats the interface default.
class ReportService extends Base implements Auditable { }

void main() {
    IO.println(new ProductService().label());
    IO.println(new ReportService().label());
}

// Output:
// audited+cached
// from class

Comparator's static and default methods

Static methods create comparators and default methods combine them, which is why the chain reads left to right. Note that reversed() applies only to the total comparator here, not to the customer order.

Example · java
record Order(String customer, long totalCents) { }

void main() {
    List<Order> orders = new ArrayList<>(List.of(
        new Order("grace", 2_000),
        new Order("ada", 5_000),
        new Order("grace", 7_500),
        new Order("ada", 1_000)));

    // comparing is static; thenComparing and reversed are defaults.
    Comparator<Order> byCustomerThenTotalDesc =
        Comparator.comparing(Order::customer)
                  .thenComparing(Comparator.comparingLong(Order::totalCents).reversed());

    orders.sort(byCustomerThenTotalDesc);   // List.sort is a default method too
    orders.forEach(o -> IO.println(o.customer() + " " + o.totalCents()));
}

// Output:
// ada 5000
// ada 1000
// grace 7500
// grace 2000

Private methods shared by defaults

The format logic lives in one place and is not part of the interface's public contract. Because write is the only abstract method, a method reference is a complete implementation.

Example · java
interface AuditLog {
    void write(String line);

    default void info(String event, String user) {
        write(format("INFO", event, user));
    }

    default void warn(String event, String user) {
        write(format("WARN", event, user));
    }

    // Java 9+: a helper for the defaults that implementers cannot see.
    private String format(String level, String event, String user) {
        return level + " event=" + event + " user=" + user;
    }
}

void main() {
    List<String> lines = new ArrayList<>();
    AuditLog log = lines::add;   // only write() needs implementing

    log.info("login", "ada");
    log.warn("password_reset", "grace");
    lines.forEach(IO::println);
}

// Output:
// INFO event=login user=ada
// WARN event=password_reset user=grace

Discussion

  • Be the first to comment on this lesson.