Interfaces & Abstract Classes

Default Methods

15 min Lesson 4 of 14

Default Methods

Java 8 introduced one of the most practically important additions to interfaces: default methods. A default method is a method declared directly in an interface that carries a concrete body. Any class that implements the interface inherits the default implementation automatically — but it is still free to override it.

The Problem Default Methods Solve

Before Java 8, interfaces could only contain abstract method signatures. This created a painful versioning problem: once a library published an interface and real-world code implemented it, you could never add a new method to that interface without breaking every single implementer. Every existing class would suddenly fail to compile because it was missing the new method.

The Java Collections Framework faced this exact problem when lambdas and streams arrived in Java 8. The Collection hierarchy needed new methods like forEach, stream, and removeIf. Without default methods, adding any of these to Iterable or Collection would have broken millions of programs worldwide.

Key insight: Default methods are a backward-compatibility tool. They let an interface evolve over time by shipping a sensible default implementation, so existing implementers do not need to change.

Syntax

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

public interface Greeter { // abstract — implementers must provide this String getName(); // default — a concrete body lives right here in the interface default String greet() { return "Hello, " + getName() + "!"; } }

Any class that implements Greeter must provide getName(), but gets greet() for free:

public class EnglishGreeter implements Greeter { private final String name; public EnglishGreeter(String name) { this.name = name; } @Override public String getName() { return name; } // greet() is inherited from the interface — no need to repeat it } public class Main { public static void main(String[] args) { Greeter g = new EnglishGreeter("Alice"); System.out.println(g.greet()); // Hello, Alice! } }

Overriding a Default Method

An implementer can always choose to override the default with its own logic, just like overriding any other method:

public class FormalGreeter implements Greeter { private final String title; private final String name; public FormalGreeter(String title, String name) { this.title = title; this.name = name; } @Override public String getName() { return title + " " + name; } @Override public String greet() { // replaces the interface default entirely return "Good day, " + getName() + ". I trust you are well."; } }

Calling the Interface Default from the Override

If you want to extend the default behaviour rather than replace it, use InterfaceName.super.methodName():

public class VerboseGreeter implements Greeter { private final String name; public VerboseGreeter(String name) { this.name = name; } @Override public String getName() { return name; } @Override public String greet() { // start with the interface default, then append extra text return Greeter.super.greet() + " Welcome aboard!"; } } // Output: Hello, Bob! Welcome aboard!

Real-World Example: Evolving a Plugin Interface

Imagine you ship a library with this interface at version 1.0:

// v1.0 — original public interface DataProcessor { void process(String data); }

Dozens of users implement it. At version 1.1 you want every processor to support a bulk-process operation, but you do not want to break existing code. A default method is the answer:

// v1.1 — backward-compatible extension public interface DataProcessor { void process(String data); // still abstract default void processBatch(java.util.List<String> items) { for (String item : items) { process(item); // calls the abstract method the implementer provides } } }

All existing implementers recompile without changes. They get processBatch automatically — it just calls process once per item using whatever logic the subclass has. High-performance implementers can override it with a vectorised or batched approach.

Best practice: Write default methods in terms of the interface's own abstract methods whenever possible. This keeps the contract clean: the default is an automatic derivation, and implementers only need to implement the abstract core.

The Diamond Problem and Resolution Rules

Because a class can implement multiple interfaces (covered in the next lesson), two interfaces could both supply a default method with the same signature. Java does not silently pick one — the compiler forces you to resolve the conflict:

public interface A { default String hello() { return "Hello from A"; } } public interface B { default String hello() { return "Hello from B"; } } // Compiler error: class C inherits unrelated defaults for hello() public class C implements A, B { // You MUST override to resolve @Override public String hello() { return A.super.hello(); // explicitly choose A's default } }
Conflict rule: If two interfaces provide a default for the same method, the implementing class must override that method — the compiler refuses to compile until the ambiguity is resolved. This is intentional: Java does not guess.

What Default Methods Are Not

  • They are not a replacement for abstract classes. They cannot hold instance state (no fields).
  • They are not intended to add business logic to an interface. Their primary purpose is API evolution and convenience helpers.
  • They are not the same as static interface methods (those belong to the interface type itself, not to instances — covered in the next lesson).

Summary

Default methods give interfaces a concrete body for one or more methods. They exist primarily to let established interfaces gain new capabilities without breaking every class that already implements them. The implementing class always has the final word: it can inherit the default, override it, or call the default through InterfaceName.super.method() while adding its own logic. When two interfaces conflict on the same default method name, the compiler demands an explicit resolution.