Default and Static Interface Methods
Add behaviour to interfaces with default, static and private methods, evolve an interface without breaking implementers, and resolve conflicts.
interface 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
- A method from a class wins over any default (class wins).
- Otherwise the default from the more specific interface wins: if
B extends A,B's version is used. - 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,hashCodeortoString; the compiler rejects it, because the class's version fromObjectwould 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
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.
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 classComparator'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.
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 2000Private 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.
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