Default Methods
Default methods, introduced in Java 8, are methods declared in an interface with a body, marked with the default keyword, providing a concrete implementation that implementing classes inherit automatically without being forced to override it. Before Java 8, interfaces could declare only abstract methods, meaning adding any new method to an interface would break every existing implementation — a problem that became critical when the Streams API needed to add methods like forEach(), stream(), and removeIf() to the existing Collection-family interfaces without breaking the entire ecosystem of third-party collection implementations. Default methods solved this by allowing interface evolution: new methods can be added to an interface with a default implementation, and existing implementing classes continue to compile and work without modification, inheriting the default behavior until they choose to override it. This entry covers the design motivation and interface evolution problem default methods solve, the complete syntax and inheritance rules, the diamond problem and Java's resolution rules for conflicting default methods from multiple interfaces, the explicit disambiguation syntax using InterfaceName.super.method(), the interaction between default methods and abstract classes, and the static method companion feature introduced alongside default methods.
Motivation — The Interface Evolution Problem
// ── The interface evolution problem — pre-Java 8 ─────────────────────
// Original interface (hypothetical pre-Java 8 Collection):
interface OldCollection<E> {
boolean add(E e);
boolean remove(Object o);
int size();
// ... other abstract methods
}
// Thousands of classes implement this:
class MyCustomList<E> implements OldCollection<E> {
public boolean add(E e) { /* ... */ return true; }
public boolean remove(Object o) { /* ... */ return true; }
public int size() { /* ... */ return 0; }
}
// If we add a NEW abstract method to OldCollection:
// interface OldCollection<E> {
// ...
// void forEach(Consumer<? super E> action); // NEW — breaks MyCustomList!
// }
// MyCustomList no longer compiles — missing implementation of forEach()
// This would break EVERY existing implementor across the entire ecosystem
// ── The Java 8 solution — default methods ──────────────────────────────
interface ModernCollection<E> {
boolean add(E e);
boolean remove(Object o);
int size();
Iterator<E> iterator(); // assume this already existed
// NEW method with default implementation — does NOT break existing implementors:
default void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : (Iterable<E>) (() -> iterator())) { // simplified illustration
action.accept(e);
}
}
}
// MyCustomList still compiles WITHOUT modification:
class MyModernList<E> implements ModernCollection<E> {
public boolean add(E e) { return true; }
public boolean remove(Object o) { return true; }
public int size() { return 0; }
public Iterator<E> iterator() { return Collections.<E>emptyList().iterator(); }
// forEach() is INHERITED from the interface — no changes needed!
}
MyModernList<String> list = new MyModernList<>();
list.forEach(System.out::println); // works via inherited default implementation
// ── Real-world example: Java 8's actual Iterable.forEach() ────────────
// This is (approximately) what was added to java.lang.Iterable:
//
// public interface Iterable<T> {
// Iterator<T> iterator();
//
// default void forEach(Consumer<? super T> action) {
// Objects.requireNonNull(action);
// for (T t : this) {
// action.accept(t);
// }
// }
// }
//
// This single addition gave forEach() to EVERY Collection implementation
// in existence — ArrayList, HashSet, TreeMap.values(), custom classes,
// third-party libraries — without recompiling or modifying any of them.
// ── Overriding the default for efficiency ──────────────────────────────
class EfficientList<E> extends AbstractList<E> {
private E[] data;
private int size;
@Override public E get(int index) { return data[index]; }
@Override public int size() { return size; }
// Override default forEach() with a more efficient array-based loop,
// avoiding the overhead of creating an Iterator object:
@Override
public void forEach(Consumer<? super E> action) {
for (int i = 0; i < size; i++) {
action.accept(data[i]); // direct array access — no Iterator overhead
}
}
}
// This is exactly what ArrayList.forEach() does in the actual JDK sourceThe Diamond Problem and Conflict Resolution Rules
// ── Rule 1: classes win over interfaces ───────────────────────────────
interface Greeter {
default String greet() { return "Hello from interface"; }
}
class BaseGreeter {
public String greet() { return "Hello from class"; }
}
class ConcreteGreeter extends BaseGreeter implements Greeter {
// No override needed — class implementation ALWAYS wins
}
System.out.println(new ConcreteGreeter().greet()); // "Hello from class"
// Even though Greeter has a default method, BaseGreeter's concrete method wins
// ── Rule 2: more specific interface wins ───────────────────────────────
interface Animal {
default String sound() { return "Some generic sound"; }
}
interface Dog extends Animal {
@Override
default String sound() { return "Woof"; } // more specific — overrides Animal's
}
class Labrador implements Dog {
// Inherits Dog's sound() — Dog is more specific than Animal
}
System.out.println(new Labrador().sound()); // "Woof" — Dog's version wins
// Even if Labrador also somehow "implements" Animal directly, Dog still wins:
class GoldenRetriever implements Animal, Dog {
// Dog extends Animal, so Dog's default is more specific — no conflict
}
System.out.println(new GoldenRetriever().sound()); // "Woof"
// ── Rule 3: unrelated interfaces — must explicitly resolve ─────────────
interface Flyer {
default String move() { return "Flying"; }
}
interface Swimmer {
default String move() { return "Swimming"; }
}
// COMPILE ERROR without explicit override:
// class Duck implements Flyer, Swimmer { }
// error: class Duck inherits unrelated defaults for move() from types Flyer and Swimmer
// CORRECT — must explicitly override and resolve:
class Duck implements Flyer, Swimmer {
@Override
public String move() {
// Explicit choice — call one, both, or write new logic:
return Flyer.super.move() + " and " + Swimmer.super.move();
}
}
System.out.println(new Duck().move()); // "Flying and Swimming"
// Alternative resolution — just pick one:
class Penguin implements Flyer, Swimmer {
@Override
public String move() {
return Swimmer.super.move(); // explicitly choose Swimmer's version
}
}
System.out.println(new Penguin().move()); // "Swimming"
// Alternative resolution — write completely new logic, ignoring both:
class Fish implements Flyer, Swimmer {
@Override
public String move() {
return "Gliding through water"; // neither parent's implementation used
}
}
// ── InterfaceName.super — only valid inside an overriding method ──────
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // explicitly calls A's version
B.super.hello(); // explicitly calls B's version
System.out.println("C"); // and adds its own behavior
}
}
new C().hello();
// Output:
// A
// B
// C
// ── No conflict for abstract methods with same signature ───────────────
interface Named {
String getName(); // abstract — no default
}
interface Identified {
String getName(); // abstract — same signature, no default
}
// No conflict — both are satisfied by ONE implementation:
class Person implements Named, Identified {
@Override
public String getName() { return "Alice"; } // satisfies BOTH interfaces
}
System.out.println(new Person().getName()); // "Alice" — no ambiguity, no errorStatic Interface Methods, Private Interface Methods, and Design Patterns
// ── Static interface methods — factories and utilities ─────────────────
interface Shape {
double area();
// Static factory methods — natural home for shape creation:
static Shape circle(double radius) {
return () -> Math.PI * radius * radius; // lambda implementing the SAM
}
static Shape rectangle(double width, double height) {
return () -> width * height;
}
static Shape square(double side) {
return rectangle(side, side); // delegates to another static method
}
}
Shape c = Shape.circle(5);
Shape r = Shape.rectangle(4, 6);
Shape s = Shape.square(3);
System.out.printf("Circle: %.2f%n", c.area()); // 78.54
System.out.printf("Rectangle: %.2f%n", r.area()); // 24.00
System.out.printf("Square: %.2f%n", s.area()); // 9.00
// Real JDK example — Comparator's static factories:
Comparator<String> byLength = Comparator.comparingInt(String::length);
Comparator<String> natural = Comparator.naturalOrder();
Comparator<String> reversed = Comparator.reverseOrder();
// ── Static methods are NOT inherited/overridden polymorphically ───────
interface Factory {
static String create() { return "Interface factory"; }
}
class Impl implements Factory {
static String create() { return "Class factory"; } // SEPARATE method, not an override
}
System.out.println(Factory.create()); // "Interface factory"
System.out.println(Impl.create()); // "Class factory"
// Impl.create() is NOT polymorphic with Factory.create() — entirely independent
// ── Private interface methods — Java 9+, code reuse without API exposure
interface ValidationRules {
default boolean isValidEmail(String email) {
return notBlank(email) && email.contains("@") && hasValidDomain(email);
}
default boolean isValidUsername(String username) {
return notBlank(username) && username.length() >= 3 && username.length() <= 20;
}
// Private helper — shared logic, NOT part of the public API:
private boolean notBlank(String s) {
return s != null && !s.isBlank();
}
// Private helper used only by isValidEmail:
private boolean hasValidDomain(String email) {
int at = email.indexOf('@');
return at > 0 && email.indexOf('.', at) > at;
}
}
class UserValidator implements ValidationRules {}
UserValidator v = new UserValidator();
System.out.println(v.isValidEmail("user@example.com")); // true
System.out.println(v.isValidUsername("ab")); // false (too short)
// v.notBlank("test") — COMPILE ERROR: notBlank() is private to the interface
// ── Template Method pattern using interfaces (impossible before default methods)
interface DataProcessor<T> {
// Template method — defines the algorithm skeleton:
default T process(String input) {
String validated = validate(input);
T parsed = parse(validated);
T transformed = transform(parsed);
log(transformed);
return transformed;
}
// Customizable steps — abstract, implemented differently per processor:
String validate(String input);
T parse(String validated);
T transform(T parsed);
// Shared default behavior — can be overridden if needed:
default void log(T result) {
System.out.println("Processed result: " + result);
}
}
class NumberProcessor implements DataProcessor<Integer> {
@Override public String validate(String input) {
if (!input.matches("-?\d+")) throw new IllegalArgumentException("Not a number");
return input;
}
@Override public Integer parse(String validated) { return Integer.parseInt(validated); }
@Override public Integer transform(Integer parsed) { return parsed * 2; }
}
// This class ALSO extends another class — impossible with abstract-class Template Method:
class LoggingNumberProcessor extends BaseLogger implements DataProcessor<Integer> {
@Override public String validate(String input) { return input; }
@Override public Integer parse(String validated) { return Integer.parseInt(validated); }
@Override public Integer transform(Integer parsed) { return parsed + 100; }
}
NumberProcessor np = new NumberProcessor();
System.out.println(np.process("21")); // logs "Processed result: 42", returns 42
class BaseLogger { /* some unrelated base class functionality */ }