Skip to content
Java java8 7 min read

Default Methods

Before Java 8, every method declared in an interface was implicitly abstract — any class that implemented the interface had to provide a body for every single method. Default methods changed that rule: you can now write a method directly inside an interface, complete with an implementation, and any implementing class gets it for free.

Why Default Methods Exist

Imagine you maintain a library that ships a popular Collection interface used by thousands of classes. One day you need to add a forEach() method. Without default methods you’d have only bad options:

  • Add the abstract method → every existing implementation breaks immediately.
  • Create a new interface → all callers must migrate.

Default methods gave the Java team a clean escape hatch. When Java 8 added forEach(), stream(), and spliterator() to java.util.Collection, existing code compiled and ran without a single change. That backward-compatibility story is the main reason default methods exist.

Note: Default methods are sometimes called defender methods or virtual extension methods in the Java specification.

Basic Syntax

Use the default keyword before the return type, inside the interface body:

public interface Greeter {

    // Abstract — must be implemented by every class
    String getName();

    // Default — has a body; implementing classes inherit this for free
    default void greet() {
        System.out.println("Hello, " + getName() + "!");
    }
}

Any class that implements Greeter can call greet() without overriding it:

public class EnglishGreeter implements Greeter {

    @Override
    public String getName() {
        return "Alice";
    }
    // greet() is inherited — no need to write it
}

public class Main {
    public static void main(String[] args) {
        Greeter g = new EnglishGreeter();
        g.greet();
    }
}

Output:

Hello, Alice!

Overriding a Default Method

A class is free to override a default method just like any other inherited method:

public class FormalGreeter implements Greeter {

    @Override
    public String getName() {
        return "Dr. Smith";
    }

    @Override
    public void greet() {
        System.out.println("Good day, " + getName() + ". How do you do?");
    }
}

Output:

Good day, Dr. Smith. How do you do?

Tip: Override a default method whenever the general behavior doesn’t fit your class — the whole point is flexibility, not forced uniformity.

Calling the Interface’s Default from the Override

If you override a default method but still want the original logic, use InterfaceName.super.methodName():

public class VerboseGreeter implements Greeter {

    @Override
    public String getName() {
        return "Bob";
    }

    @Override
    public void greet() {
        System.out.println("[LOG] Greeting initiated");
        Greeter.super.greet();   // delegate to the interface default
        System.out.println("[LOG] Greeting complete");
    }
}

Output:

[LOG] Greeting initiated
Hello, Bob!
[LOG] Greeting complete

Default Methods and Multiple Interfaces

A class can implement several interfaces, and that is where things get interesting. If two interfaces declare a default method with the same signature, the compiler forces you to resolve the conflict explicitly:

interface A {
    default String hello() { return "Hello from A"; }
}

interface B {
    default String hello() { return "Hello from B"; }
}

// Compiler error without the override below
public class C implements A, B {

    @Override
    public String hello() {
        // Pick one, combine both, or write your own
        return A.super.hello() + " & " + B.super.hello();
    }
}

public class ConflictDemo {
    public static void main(String[] args) {
        System.out.println(new C().hello());
    }
}

Output:

Hello from A & Hello from B

Warning: If you implement two interfaces that have conflicting default methods and you do not override the method in your class, the code will not compile. The compiler will never silently pick one over the other.

Interface Inheritance of Default Methods

Interfaces can extend other interfaces. A child interface may:

  1. Inherit the default method unchanged.
  2. Override it with another default implementation.
  3. Re-declare it as abstract, forcing implementing classes to provide a body.
interface Shape {
    double area();

    default String describe() {
        return "I am a shape with area " + area();
    }
}

interface Circle extends Shape {
    // Re-declares describe() as abstract — implementing classes must override it
    @Override
    String describe();
}

// Now any class implementing Circle MUST provide describe()
public class RedCircle implements Circle {
    private final double radius;

    public RedCircle(double radius) { this.radius = radius; }

    @Override
    public double area() { return Math.PI * radius * radius; }

    @Override
    public String describe() {
        return "Red circle, area = " + String.format("%.2f", area());
    }
}

Default Methods vs Abstract Classes

Both can provide concrete methods. Here is a quick comparison to help you decide:

FeatureInterface + defaultAbstract class
Can hold state (fields)No (only constants)Yes
ConstructorNoYes
Multiple inheritanceYes — implement many interfacesNo — extend only one class
Access modifiers on methodspublic only (until Java 9)Any
When to useDefine a contract + convenience helpersShared state + partial implementation

Tip: If you need instance fields or constructors, reach for an abstract class. If you just need to add behavior to an existing contract without breaking callers, a default method is the right tool.

Real-World Example — Comparable-Style Interface

import java.util.List;

public interface Sortable<T extends Comparable<T>> {

    List<T> getItems();

    default T findMin() {
        return getItems().stream()
                .min(Comparable::compareTo)
                .orElseThrow(() -> new IllegalStateException("Empty list"));
    }

    default T findMax() {
        return getItems().stream()
                .max(Comparable::compareTo)
                .orElseThrow(() -> new IllegalStateException("Empty list"));
    }
}

public class NumberBag implements Sortable<Integer> {

    private final List<Integer> numbers;

    public NumberBag(List<Integer> numbers) {
        this.numbers = numbers;
    }

    @Override
    public List<Integer> getItems() { return numbers; }
}

public class SortableDemo {
    public static void main(String[] args) {
        NumberBag bag = new NumberBag(List.of(5, 2, 9, 1, 7));
        System.out.println("Min: " + bag.findMin());
        System.out.println("Max: " + bag.findMax());
    }
}

Output:

Min: 1
Max: 9

NumberBag only needs to supply getItems() — the useful findMin() and findMax() come for free from the interface. This pattern keeps implementing classes thin.

Under the Hood

Bytecode representation

A default method is stored in the interface’s .class file just like a class method — it has a full Code attribute in the bytecode. At the call site the JVM uses a special invokeinterface instruction (same as abstract interface calls); it does not use invokevirtual.

vtable and itable

The JVM maintains an itable (interface method table) for each class. When a class inherits a default method without overriding it, the JVM links the itable slot directly to the interface’s method body. When a class does override it, the itable slot points to the class’s version instead. Either way, dynamic dispatch works at runtime with negligible overhead compared to regular virtual calls.

No diamond problem

Java avoids the C++ diamond problem because interfaces cannot carry state. If two interfaces both provide a default foo(), the worst outcome is a compile-time conflict that you must resolve explicitly — the JVM never has to guess which copy of shared mutable state to use. See vtable & Dynamic Dispatch for deeper detail on how the JVM resolves method calls.

Private interface methods (Java 9+)

Java 9 added private and private static methods to interfaces. These are helper methods you can factor out of long default methods — they are invisible to implementing classes and other interfaces.

public interface Logger {

    default void logInfo(String msg)  { log("INFO",  msg); }
    default void logError(String msg) { log("ERROR", msg); }

    // private helper — only visible inside this interface
    private void log(String level, String msg) {
        System.out.println("[" + level + "] " + msg);
    }
}

Common Pitfalls

  • Treating default methods like abstract methods — if you forget the default keyword, the method is abstract and all implementing classes must provide a body.
  • Assuming default methods can access fields — they cannot, because interfaces have no instance fields. They can only call other interface methods or use their parameters.
  • Ignoring conflict resolution — two interfaces with the same default method signature always requires an explicit override in the implementing class.
Last updated June 13, 2026
Was this helpful?