Interface default methods and static methods

1. Why Default Methods?

Imagine a city planner who designed a road system 20 years ago. Thousands of buildings were constructed along those roads. Now the city needs bike lanes. The planner cannot tear down every building and start over — the city must add bike lanes without breaking the existing infrastructure. That is exactly the problem Java 8 solved with default methods.

Before Java 8, interfaces in Java were pure contracts: they declared methods but provided no implementation. Every class that implemented an interface had to provide code for every method. This worked fine — until the Java team needed to evolve existing interfaces.

Consider the java.util.Collection interface. It has existed since Java 1.2 (1998). Millions of classes implement it — in the JDK itself, in third-party libraries, in enterprise applications worldwide. When Java 8 introduced the Stream API, the team needed to add a stream() method to Collection. But adding an abstract method to an interface would break every class that implements it.

The backward compatibility problem:

  • Adding a new abstract method to an interface forces every implementing class to provide an implementation
  • Third-party libraries and user code would fail to compile after upgrading to Java 8
  • The Java platform’s commitment to backward compatibility would be violated
  • Interface evolution would effectively be impossible

Default methods solve this by allowing interfaces to provide a method body using the default keyword. Existing implementations inherit the default behavior automatically, but can override it if needed. This is interface evolution without breaking existing code.

Here is a simple before-and-after to see the problem and solution:

// BEFORE Java 8: Adding a method breaks all implementations
interface Greeting {
    void sayHello(String name);

    // If we add this, every class implementing Greeting must be updated!
    // void sayGoodbye(String name);  // <-- Would break existing code
}

class EnglishGreeting implements Greeting {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "!");
    }
    // If sayGoodbye() were abstract, this class would fail to compile
}

// AFTER Java 8: Default methods allow safe evolution
interface GreetingV2 {
    void sayHello(String name);

    // Default method -- existing implementations inherit this automatically
    default void sayGoodbye(String name) {
        System.out.println("Goodbye, " + name + "!");
    }
}

class EnglishGreetingV2 implements GreetingV2 {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "!");
    }
    // sayGoodbye() is inherited from the interface -- no changes required!
}

public class DefaultMethodMotivation {
    public static void main(String[] args) {
        EnglishGreetingV2 greeting = new EnglishGreetingV2();
        greeting.sayHello("Alice");
        // Output: Hello, Alice!

        greeting.sayGoodbye("Alice");
        // Output: Goodbye, Alice!
    }
}

EnglishGreetingV2 did not have to change at all. It inherited the default implementation of sayGoodbye() for free. This is the fundamental value of default methods.

2. Default Method Syntax

A default method is declared in an interface using the default keyword before the return type. Unlike regular interface methods (which are implicitly abstract), a default method has a full method body.

Syntax:

default returnType methodName(parameters) {
    // method body
}

Key rules for default methods:

Rule Description
Must use default keyword Without it, the method is abstract and has no body
Must have a body Unlike abstract methods, a default method must provide an implementation
Implicitly public All interface methods are public; you cannot make a default method private or protected
Can be overridden Implementing classes can override the default method with their own version
Can call other interface methods A default method can invoke abstract methods declared in the same interface
Cannot access instance state Interfaces have no instance fields; default methods rely on abstract method contracts

2.1 Basic Default Method

interface Logger {
    // Abstract method -- implementing classes MUST provide this
    void log(String message);

    // Default method -- implementing classes inherit this, can override if needed
    default void logInfo(String message) {
        log("INFO: " + message);
    }

    default void logWarning(String message) {
        log("WARNING: " + message);
    }

    default void logError(String message) {
        log("ERROR: " + message);
    }
}

class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("[Console] " + message);
    }
    // logInfo, logWarning, logError are all inherited!
}

class FileLogger implements Logger {
    @Override
    public void log(String message) {
        // In a real app, this would write to a file
        System.out.println("[File] " + message);
    }

    // Override one default method for custom behavior
    @Override
    public void logError(String message) {
        log("CRITICAL ERROR: " + message);
        // In a real app, also send an alert
    }
}

public class DefaultMethodBasics {
    public static void main(String[] args) {
        Logger console = new ConsoleLogger();
        console.logInfo("Application started");
        // Output: [Console] INFO: Application started

        console.logWarning("Memory usage is high");
        // Output: [Console] WARNING: Memory usage is high

        console.logError("Database connection failed");
        // Output: [Console] ERROR: Database connection failed

        System.out.println();

        Logger file = new FileLogger();
        file.logInfo("Application started");
        // Output: [File] INFO: Application started

        file.logError("Database connection failed");
        // Output: [File] CRITICAL ERROR: Database connection failed
    }
}

Notice how ConsoleLogger inherits all three default methods, while FileLogger overrides only logError(). The default methods call the abstract log() method, which each implementation defines differently. This is the template method pattern inside an interface.

2.2 Default Methods Calling Abstract Methods

One of the most useful patterns is having default methods call abstract methods. The interface defines the structure and the implementing class fills in the details.

interface Validator {
    // Abstract: each implementation defines its own validation logic
    boolean isValid(T item);

    // Default: built on top of isValid()
    default void validate(T item) {
        if (!isValid(item)) {
            throw new IllegalArgumentException("Validation failed for: " + item);
        }
    }

    // Default: validate a collection
    default long countInvalid(java.util.List items) {
        return items.stream().filter(item -> !isValid(item)).count();
    }

    // Default: filter to only valid items
    default java.util.List filterValid(java.util.List items) {
        return items.stream().filter(this::isValid).collect(java.util.stream.Collectors.toList());
    }
}

class EmailValidator implements Validator {
    @Override
    public boolean isValid(String email) {
        return email != null && email.contains("@") && email.contains(".");
    }
}

class AgeValidator implements Validator {
    @Override
    public boolean isValid(Integer age) {
        return age != null && age >= 0 && age <= 150;
    }
}

public class DefaultCallingAbstract {
    public static void main(String[] args) {
        EmailValidator emailValidator = new EmailValidator();

        // Using inherited default methods
        System.out.println(emailValidator.isValid("alice@example.com"));
        // Output: true

        java.util.List emails = java.util.List.of(
            "alice@example.com", "invalid", "bob@test.org", "no-at-sign"
        );

        System.out.println("Invalid count: " + emailValidator.countInvalid(emails));
        // Output: Invalid count: 2

        System.out.println("Valid emails: " + emailValidator.filterValid(emails));
        // Output: Valid emails: [alice@example.com, bob@test.org]

        // validate() throws on invalid input
        try {
            emailValidator.validate("not-an-email");
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
            // Output: Validation failed for: not-an-email
        }
    }
}

Each Validator implementation only needs to define isValid(). The three default methods (validate(), countInvalid(), filterValid()) are all inherited for free. If a specific validator needs different behavior for any of these, it can override just that one method.

3. Default Methods in Action -- Real Java API Examples

Java 8 added dozens of default methods to existing interfaces in the JDK. Understanding these real examples shows how default methods were used to add major new capabilities without breaking any existing code.

3.1 Iterable.forEach()

The Iterable interface (parent of all collections) gained a forEach() default method so that every existing List, Set, and Queue could immediately use lambda-based iteration.

import java.util.List;
import java.util.ArrayList;

public class ForEachDefaultMethod {
    public static void main(String[] args) {
        List names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));

        // forEach() is a default method in Iterable -- no List change needed
        names.forEach(name -> System.out.println("Hello, " + name));
        // Output:
        // Hello, Alice
        // Hello, Bob
        // Hello, Charlie

        // Using method reference
        names.forEach(System.out::println);
        // Output:
        // Alice
        // Bob
        // Charlie
    }
}

3.2 Collection.stream() and Collection.parallelStream()

The Stream API is arguably the biggest Java 8 feature. It was made possible by adding stream() and parallelStream() as default methods to Collection.

import java.util.List;
import java.util.stream.Collectors;

public class StreamDefaultMethod {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Diana", "Eve");

        // stream() is a default method in Collection
        List shortNames = names.stream()
            .filter(name -> name.length() <= 3)
            .collect(Collectors.toList());

        System.out.println("Short names: " + shortNames);
        // Output: Short names: [Bob, Eve]

        // parallelStream() is also a default method
        long count = names.parallelStream()
            .filter(name -> name.startsWith("A") || name.startsWith("E"))
            .count();

        System.out.println("Names starting with A or E: " + count);
        // Output: Names starting with A or E: 2
    }
}

3.3 List.sort()

Before Java 8, you had to use Collections.sort(list, comparator). Java 8 added a sort() default method directly to the List interface.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ListSortDefaultMethod {
    public static void main(String[] args) {
        List names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));

        // Before Java 8:
        // Collections.sort(names);

        // Java 8: sort() is a default method on List
        names.sort(Comparator.naturalOrder());
        System.out.println("Sorted: " + names);
        // Output: Sorted: [Alice, Bob, Charlie]

        // Sort by length, then alphabetically
        names.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
        System.out.println("By length: " + names);
        // Output: By length: [Bob, Alice, Charlie]

        // Reverse order
        names.sort(Comparator.reverseOrder());
        System.out.println("Reversed: " + names);
        // Output: Reversed: [Charlie, Bob, Alice]
    }
}

3.4 Map.getOrDefault(), Map.putIfAbsent(), Map.forEach()

The Map interface received many default methods in Java 8. These eliminated common boilerplate patterns.

import java.util.HashMap;
import java.util.Map;

public class MapDefaultMethods {
    public static void main(String[] args) {
        Map scores = new HashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob", 87);

        // getOrDefault() -- no more null checks for missing keys
        int aliceScore = scores.getOrDefault("Alice", 0);
        int unknownScore = scores.getOrDefault("Unknown", 0);
        System.out.println("Alice: " + aliceScore + ", Unknown: " + unknownScore);
        // Output: Alice: 95, Unknown: 0

        // putIfAbsent() -- only puts if key is not already present
        scores.putIfAbsent("Bob", 100);     // Bob exists, no change
        scores.putIfAbsent("Charlie", 92);  // Charlie is new, added
        System.out.println("Bob: " + scores.get("Bob"));
        // Output: Bob: 87

        // forEach() with BiConsumer
        scores.forEach((name, score) ->
            System.out.println(name + " scored " + score)
        );
        // Output:
        // Alice scored 95
        // Bob scored 87
        // Charlie scored 92

        // merge() -- combine existing and new values
        scores.merge("Alice", 5, Integer::sum);  // Alice: 95 + 5 = 100
        System.out.println("Alice after bonus: " + scores.get("Alice"));
        // Output: Alice after bonus: 100

        // computeIfAbsent() -- compute value only if key is missing
        scores.computeIfAbsent("Diana", name -> name.length() * 10);
        System.out.println("Diana: " + scores.get("Diana"));
        // Output: Diana: 50

        // replaceAll() -- transform all values
        scores.replaceAll((name, score) -> score + 10);
        scores.forEach((name, score) -> System.out.println(name + ": " + score));
        // Output:
        // Alice: 110
        // Bob: 97
        // Charlie: 102
        // Diana: 60
    }
}

All of these methods (getOrDefault, putIfAbsent, forEach, merge, computeIfAbsent, replaceAll) are default methods on the Map interface. Every Map implementation -- HashMap, TreeMap, LinkedHashMap, and any custom implementation -- inherited them all without any code changes.

3.5 Summary of Key Default Methods Added in Java 8

Interface Default Method Purpose
Iterable forEach(Consumer) Lambda-based iteration over any Iterable
Collection stream() Create a sequential Stream
Collection parallelStream() Create a parallel Stream
Collection removeIf(Predicate) Remove elements matching a condition
List sort(Comparator) Sort the list in place
List replaceAll(UnaryOperator) Transform every element
Map getOrDefault(key, default) Get value or return default if missing
Map putIfAbsent(key, value) Only put if key is not present
Map forEach(BiConsumer) Iterate over key-value pairs
Map merge(key, value, BiFunction) Merge new value with existing
Map computeIfAbsent(key, Function) Compute value if key is missing
Map replaceAll(BiFunction) Transform all values

4. The Diamond Problem

Default methods introduce a form of multiple inheritance of behavior into Java. A class can implement multiple interfaces, and if two interfaces provide a default method with the same signature, the compiler faces a conflict: which default should the class inherit?

This is called the diamond problem, named after the diamond shape formed in the inheritance diagram. Java 8 defines clear rules for resolving these conflicts.

4.1 The Conflict Scenario

interface Camera {
    default String takePicture() {
        return "Camera: snap!";
    }
}

interface Phone {
    default String takePicture() {
        return "Phone: click!";
    }
}

// COMPILER ERROR: SmartDevice inherits unrelated defaults for takePicture()
// class SmartDevice implements Camera, Phone { }

// FIX: You MUST override the conflicting method
class SmartDevice implements Camera, Phone {
    @Override
    public String takePicture() {
        // Option 1: Provide your own implementation
        return "SmartDevice: capturing with AI!";
    }
}

public class DiamondProblemBasic {
    public static void main(String[] args) {
        SmartDevice device = new SmartDevice();
        System.out.println(device.takePicture());
        // Output: SmartDevice: capturing with AI!
    }
}

4.2 Choosing a Specific Interface's Default with super

When you override a conflicting default method, you can delegate to one of the parent interfaces using the InterfaceName.super.methodName() syntax.

interface Flyable {
    default String getSpeed() {
        return "Flying at 500 km/h";
    }
}

interface Swimmable {
    default String getSpeed() {
        return "Swimming at 30 km/h";
    }
}

class Duck implements Flyable, Swimmable {
    @Override
    public String getSpeed() {
        // Delegate to Flyable's default
        return Flyable.super.getSpeed();
    }

    public String getSwimSpeed() {
        // Can also call the other interface's default in any method
        return Swimmable.super.getSpeed();
    }
}

class FlyingFish implements Flyable, Swimmable {
    @Override
    public String getSpeed() {
        // Combine both!
        return Flyable.super.getSpeed() + " | " + Swimmable.super.getSpeed();
    }
}

public class DiamondProblemSuper {
    public static void main(String[] args) {
        Duck duck = new Duck();
        System.out.println(duck.getSpeed());
        // Output: Flying at 500 km/h
        System.out.println(duck.getSwimSpeed());
        // Output: Swimming at 30 km/h

        FlyingFish fish = new FlyingFish();
        System.out.println(fish.getSpeed());
        // Output: Flying at 500 km/h | Swimming at 30 km/h
    }
}

4.3 Diamond Problem Resolution Rules

Java uses three rules (applied in order) to resolve default method conflicts:

Priority Rule Description
1 (Highest) Classes win over interfaces If a superclass provides a concrete method, it takes priority over any interface default
2 More specific interface wins If interface B extends interface A, and both define the same default, B's version wins
3 Class must override If neither rule 1 nor 2 resolves it (two unrelated interfaces), the implementing class must explicitly override the method

4.4 Rule 1: Classes Win Over Interfaces

interface Printable {
    default String getOutput() {
        return "Printable default";
    }
}

class BasePrinter {
    public String getOutput() {
        return "BasePrinter concrete method";
    }
}

// BasePrinter.getOutput() wins over Printable.getOutput()
class LaserPrinter extends BasePrinter implements Printable {
    // No override needed -- BasePrinter's method takes priority
}

public class ClassWinsOverInterface {
    public static void main(String[] args) {
        LaserPrinter printer = new LaserPrinter();
        System.out.println(printer.getOutput());
        // Output: BasePrinter concrete method
    }
}

4.5 Rule 2: More Specific Interface Wins

interface Animal {
    default String sound() {
        return "Some generic animal sound";
    }
}

interface Dog extends Animal {
    @Override
    default String sound() {
        return "Woof!";
    }
}

// Dog is more specific than Animal, so Dog.sound() wins
class GoldenRetriever implements Animal, Dog {
    // No override needed -- Dog's default wins because Dog extends Animal
}

public class SpecificInterfaceWins {
    public static void main(String[] args) {
        GoldenRetriever dog = new GoldenRetriever();
        System.out.println(dog.sound());
        // Output: Woof!
    }
}

5. Static Methods in Interfaces

Java 8 also introduced static methods in interfaces. Unlike default methods, static methods belong to the interface itself, not to any implementing class. They cannot be overridden and are called using the interface name directly: InterfaceName.staticMethod().

Why static methods in interfaces?

  • Utility methods: Related helper methods can live in the interface instead of a separate utility class
  • Factory methods: Create instances of the interface without exposing implementation classes
  • Cohesion: Keep related code together rather than scattering it across companion classes like Collections (for Collection) or Paths (for Path)

5.1 Static Method Syntax and Rules

Rule Description
Must use static keyword Declared with static in the interface
Must have a body Static methods must provide an implementation
Called via interface name MyInterface.myStaticMethod(), never via an instance
Not inherited by implementing classes Unlike defaults, static methods are not inherited
Cannot be overridden Implementing classes cannot override interface static methods
Implicitly public All interface methods are public (private static allowed in Java 9+)

5.2 Utility Methods

interface StringUtils {
    // Static utility methods in an interface
    static boolean isNullOrEmpty(String str) {
        return str == null || str.isEmpty();
    }

    static boolean isNullOrBlank(String str) {
        return str == null || str.trim().isEmpty();
    }

    static String capitalize(String str) {
        if (isNullOrEmpty(str)) return str;
        return Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase();
    }

    static String repeat(String str, int times) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < times; i++) {
            sb.append(str);
        }
        return sb.toString();
    }
}

public class StaticUtilityMethods {
    public static void main(String[] args) {
        // Called via interface name -- NOT via an instance
        System.out.println(StringUtils.isNullOrEmpty(""));
        // Output: true

        System.out.println(StringUtils.isNullOrEmpty("Hello"));
        // Output: false

        System.out.println(StringUtils.isNullOrBlank("   "));
        // Output: true

        System.out.println(StringUtils.capitalize("jAVA"));
        // Output: Java

        System.out.println(StringUtils.repeat("Ha", 3));
        // Output: HaHaHa
    }
}

5.3 Factory Methods

Static methods in interfaces are ideal for the factory method pattern. The interface exposes creation methods while hiding the implementation classes.

interface Shape {
    double area();
    String describe();

    // Factory methods -- callers never need to know the implementation classes
    static Shape circle(double radius) {
        return new Circle(radius);
    }

    static Shape rectangle(double width, double height) {
        return new Rectangle(width, height);
    }

    static Shape square(double side) {
        return new Rectangle(side, side);
    }
}

// Implementation classes can be package-private
class Circle implements Shape {
    private final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

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

    @Override
    public String describe() {
        return String.format("Circle(radius=%.1f)", radius);
    }
}

class Rectangle implements Shape {
    private final double width;
    private final double height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public String describe() {
        return String.format("Rectangle(%.1f x %.1f)", width, height);
    }
}

public class StaticFactoryMethods {
    public static void main(String[] args) {
        // Create shapes using factory methods -- no need to know Circle or Rectangle
        Shape circle = Shape.circle(5.0);
        Shape rect = Shape.rectangle(4.0, 6.0);
        Shape square = Shape.square(3.0);

        System.out.println(circle.describe() + " -> area: " + String.format("%.2f", circle.area()));
        // Output: Circle(radius=5.0) -> area: 78.54

        System.out.println(rect.describe() + " -> area: " + String.format("%.2f", rect.area()));
        // Output: Rectangle(4.0 x 6.0) -> area: 24.00

        System.out.println(square.describe() + " -> area: " + String.format("%.2f", square.area()));
        // Output: Rectangle(3.0 x 3.0) -> area: 9.00
    }
}

5.4 Comparator.comparing() -- A Real-World Static Method

One of the most powerful static methods added in Java 8 is Comparator.comparing(). It is a static factory method on the Comparator interface that creates comparators from method references.

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ComparatorComparing {
    record Employee(String name, String department, int salary) {}

    public static void main(String[] args) {
        List employees = new ArrayList<>(List.of(
            new Employee("Alice", "Engineering", 95000),
            new Employee("Bob", "Marketing", 72000),
            new Employee("Charlie", "Engineering", 88000),
            new Employee("Diana", "Marketing", 85000),
            new Employee("Eve", "Engineering", 95000)
        ));

        // Comparator.comparing() is a STATIC method on the Comparator interface
        employees.sort(Comparator.comparing(Employee::salary));
        System.out.println("By salary (ascending):");
        employees.forEach(e -> System.out.println("  " + e));
        // Output:
        //   Employee[name=Bob, department=Marketing, salary=72000]
        //   Employee[name=Diana, department=Marketing, salary=85000]
        //   Employee[name=Charlie, department=Engineering, salary=88000]
        //   Employee[name=Alice, department=Engineering, salary=95000]
        //   Employee[name=Eve, department=Engineering, salary=95000]

        // Chain with default methods: reversed(), thenComparing()
        employees.sort(
            Comparator.comparing(Employee::department)
                      .thenComparing(Employee::salary)
                      .reversed()
        );
        System.out.println("\nBy department, then salary, reversed:");
        employees.forEach(e -> System.out.println("  " + e));
        // Output:
        //   Employee[name=Bob, department=Marketing, salary=72000]
        //   Employee[name=Diana, department=Marketing, salary=85000]
        //   Employee[name=Charlie, department=Engineering, salary=88000]
        //   Employee[name=Alice, department=Engineering, salary=95000]
        //   Employee[name=Eve, department=Engineering, salary=95000]
    }
}

Notice the interplay: Comparator.comparing() is a static method that creates a comparator, and reversed() and thenComparing() are default methods that modify it. Static and default methods work together to create a rich, fluent API.

6. Private Methods in Interfaces (Java 9+)

Java 9 added private methods and private static methods to interfaces. The motivation is simple: when multiple default methods in the same interface share common logic, you need a way to extract that shared code without making it part of the public API.

Before Java 9, you had two bad options:

  • Duplicate the shared code in each default method
  • Create a public or default helper method that pollutes the interface's API

6.1 Private Methods Between Defaults

interface Reportable {
    String getData();

    default String htmlReport() {
        return wrapInDocument("" + formatBody() + "");
    }

    default String textReport() {
        return wrapInDocument("=== REPORT ===\n" + formatBody() + "\n==============");
    }

    default String csvReport() {
        return wrapInDocument("data\n" + getData());
    }

    // Java 9+: Private method shared by multiple defaults
    private String formatBody() {
        String raw = getData();
        if (raw == null || raw.isEmpty()) {
            return "[No data available]";
        }
        return raw.trim();
    }

    // Java 9+: Private method to add metadata
    private String wrapInDocument(String content) {
        return "Generated: " + java.time.LocalDate.now() + "\n" + content;
    }
}

class SalesReport implements Reportable {
    @Override
    public String getData() {
        return "Q1: $1.2M, Q2: $1.5M, Q3: $1.1M, Q4: $1.8M";
    }
}

public class PrivateMethodsDemo {
    public static void main(String[] args) {
        SalesReport report = new SalesReport();

        System.out.println(report.textReport());
        // Output:
        // Generated: 2026-02-28
        // === REPORT ===
        // Q1: $1.2M, Q2: $1.5M, Q3: $1.1M, Q4: $1.8M
        // ==============

        System.out.println();
        System.out.println(report.htmlReport());
        // Output:
        // Generated: 2026-02-28
        // Q1: $1.2M, Q2: $1.5M, Q3: $1.1M, Q4: $1.8M
    }
}

6.2 Private Static Methods

Private static methods can be called from both static methods and default methods. They are useful when the shared logic does not depend on instance-level abstract methods.

interface Identifiable {
    String getName();

    // Static factory using private static helper
    static String generateId(String prefix) {
        return prefix + "-" + randomSuffix(8);
    }

    static String generateShortId(String prefix) {
        return prefix + "-" + randomSuffix(4);
    }

    // Java 9+: Private static method -- shared by static methods above
    private static String randomSuffix(int length) {
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        StringBuilder sb = new StringBuilder();
        java.util.Random random = new java.util.Random();
        for (int i = 0; i < length; i++) {
            sb.append(chars.charAt(random.nextInt(chars.length())));
        }
        return sb.toString();
    }

    // Default method can also call private static methods
    default String getDisplayId() {
        return randomSuffix(6) + "-" + getName().toUpperCase();
    }
}

class User implements Identifiable {
    private final String name;

    User(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

public class PrivateStaticMethodDemo {
    public static void main(String[] args) {
        // Static method usage
        System.out.println(Identifiable.generateId("USER"));
        // Output: USER-A7KF2M9B (random)

        System.out.println(Identifiable.generateShortId("ORD"));
        // Output: ORD-X3K9 (random)

        // Default method usage
        User user = new User("Alice");
        System.out.println(user.getDisplayId());
        // Output: R4TN2K-ALICE (random prefix)
    }
}

6.3 Summary of Interface Method Types by Java Version

Method Type Java Version Has Body? Access Modifier Inherited? Can Override?
Abstract 1.0+ No public (implicit) Yes Must implement
Default 8+ Yes public (implicit) Yes Yes (optional)
Static 8+ Yes public (implicit) No No
Private 9+ Yes private No No
Private static 9+ Yes private No No

7. Abstract vs Default vs Static -- Comparison

With three kinds of methods now possible in interfaces, it is important to understand when to use each one.

Feature Abstract Method Default Method Static Method
Keyword None (implicit) default static
Has body? No Yes Yes
Inherited? Yes (must implement) Yes (can override) No
Can access other methods? N/A Yes, including abstract Only other static methods
Called on Instance Instance Interface name
Primary use Define the contract Provide optional behavior Utility/factory methods
Example void process(); default void log() {...} static Foo create() {...}

When to Use Each

  • Abstract method: When every implementation MUST provide its own logic. This is the core contract of the interface.
  • Default method: When you want to provide a reasonable default that some implementations might override. Also used for backward-compatible interface evolution.
  • Static method: When the behavior is related to the interface but does not depend on any instance. Utility methods, factory methods, and helper functions.
interface Notification {
    // Abstract: every notification type must define this
    String getMessage();
    String getRecipient();

    // Default: common behavior with a sensible default
    default String getSubject() {
        return "Notification";
    }

    default String format() {
        return String.format("To: %s\nSubject: %s\n\n%s",
            getRecipient(), getSubject(), getMessage());
    }

    // Static: factory methods
    static Notification email(String to, String subject, String body) {
        return new Notification() {
            @Override
            public String getMessage() { return body; }
            @Override
            public String getRecipient() { return to; }
            @Override
            public String getSubject() { return subject; }
        };
    }

    static Notification sms(String to, String body) {
        return new Notification() {
            @Override
            public String getMessage() { return body; }
            @Override
            public String getRecipient() { return to; }
        };
    }
}

public class AbstractDefaultStaticDemo {
    public static void main(String[] args) {
        // Static factory methods create instances
        Notification email = Notification.email(
            "alice@example.com", "Welcome!", "Thanks for signing up."
        );
        Notification sms = Notification.sms(
            "+1234567890", "Your code is 9876"
        );

        // Default methods provide formatting
        System.out.println(email.format());
        // Output:
        // To: alice@example.com
        // Subject: Welcome!
        //
        // Thanks for signing up.

        System.out.println();
        System.out.println(sms.format());
        // Output:
        // To: +1234567890
        // Subject: Notification
        //
        // Your code is 9876
    }
}

8. Default Methods and Functional Interfaces

A functional interface has exactly one abstract method, making it usable with lambda expressions. But a functional interface can have any number of default and static methods. This is how Java 8 built rich, chainable APIs on top of functional interfaces.

The @FunctionalInterface annotation enforces the single-abstract-method rule. Default methods do not count toward this limit.

8.1 Predicate -- Defaults Enable Composition

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateDefaultMethods {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Anna", "Eve", "Alex");

        // Predicate has ONE abstract method: test(T t)
        Predicate startsWithA = name -> name.startsWith("A");
        Predicate longerThan3 = name -> name.length() > 3;

        // Default method: and() -- combines two predicates with logical AND
        Predicate startsWithAAndLong = startsWithA.and(longerThan3);
        List result1 = names.stream()
            .filter(startsWithAAndLong)
            .collect(Collectors.toList());
        System.out.println("Starts with A AND longer than 3: " + result1);
        // Output: Starts with A AND longer than 3: [Alice, Anna, Alex]

        // Default method: or() -- combines with logical OR
        Predicate startsWithE = name -> name.startsWith("E");
        List result2 = names.stream()
            .filter(startsWithA.or(startsWithE))
            .collect(Collectors.toList());
        System.out.println("Starts with A OR E: " + result2);
        // Output: Starts with A OR E: [Alice, Anna, Eve, Alex]

        // Default method: negate() -- logical NOT
        List result3 = names.stream()
            .filter(startsWithA.negate())
            .collect(Collectors.toList());
        System.out.println("Does NOT start with A: " + result3);
        // Output: Does NOT start with A: [Bob, Charlie, Eve]

        // Static method: Predicate.not() (Java 11+) -- even cleaner negation
        // List result4 = names.stream()
        //     .filter(Predicate.not(startsWithA))
        //     .collect(Collectors.toList());
    }
}

8.2 Function -- Defaults Enable Chaining

import java.util.function.Function;

public class FunctionDefaultMethods {
    public static void main(String[] args) {
        Function trim = String::trim;
        Function toUpper = String::toUpperCase;
        Function length = String::length;

        // Default method: andThen() -- apply this function, then the next
        Function trimThenUpper = trim.andThen(toUpper);
        System.out.println(trimThenUpper.apply("  hello world  "));
        // Output: HELLO WORLD

        // Default method: compose() -- apply the OTHER function first, then this
        Function upperThenLength = length.compose(toUpper);
        System.out.println(upperThenLength.apply("hello"));
        // Output: 5

        // Chain multiple transformations
        Function pipeline = trim
            .andThen(toUpper)
            .andThen(s -> s.replace(" ", "_"));

        System.out.println(pipeline.apply("  hello world  "));
        // Output: HELLO_WORLD

        // Static method: Function.identity()
        Function identity = Function.identity();
        System.out.println(identity.apply("unchanged"));
        // Output: unchanged
    }
}

8.3 Comparator -- A Rich Functional Interface

Comparator is the best example of how default and static methods transform a functional interface into a powerful API. It has one abstract method (compare()) plus 18 default and static methods.

Method Type Purpose
compare(T, T) Abstract The core comparison (1 abstract = functional interface)
reversed() Default Reverse the sort order
thenComparing() Default Add a secondary sort key
thenComparingInt() Default Secondary sort on int-valued key (avoids boxing)
comparing(Function) Static Create a comparator from a key extractor
comparingInt(ToIntFunction) Static Create a comparator from an int key (avoids boxing)
naturalOrder() Static Comparator for natural ordering
reverseOrder() Static Comparator for reverse natural ordering
nullsFirst(Comparator) Static Wraps a comparator to handle nulls (nulls sort first)
nullsLast(Comparator) Static Wraps a comparator to handle nulls (nulls sort last)

Without default methods, Comparator would be a bare single-method interface, and all this chaining functionality would have to live in a separate utility class.

9. Interface Evolution Pattern

Default methods make it possible to evolve interfaces over time without breaking existing implementations. This is one of the most important practical use cases in library development.

9.1 Adding New Methods to Existing Interfaces

Consider you have published a library with an interface, and users have implemented it. Later, you want to add new capabilities.

// Version 1.0 of your library
interface DataExporter {
    void export(java.util.List data, String destination);
}

// A client's implementation (you cannot change this)
class CsvExporter implements DataExporter {
    @Override
    public void export(java.util.List data, String destination) {
        System.out.println("Exporting " + data.size() + " rows to CSV: " + destination);
    }
}

// Version 2.0 -- add new features without breaking CsvExporter
interface DataExporterV2 extends DataExporter {

    // New default methods -- CsvExporter inherits these without any changes
    default void exportWithHeader(java.util.List data, String destination, String header) {
        System.out.println("Header: " + header);
        export(data, destination);
    }

    default boolean validate(java.util.List data) {
        return data != null && !data.isEmpty();
    }

    default void exportIfValid(java.util.List data, String destination) {
        if (validate(data)) {
            export(data, destination);
        } else {
            System.out.println("Skipping export: invalid data");
        }
    }
}

public class InterfaceEvolution {
    public static void main(String[] args) {
        // CsvExporter works with the extended interface (if it had implemented V2)
        // Demonstrating the concept:
        DataExporterV2 exporter = new DataExporterV2() {
            @Override
            public void export(java.util.List data, String destination) {
                System.out.println("Exporting " + data.size() + " rows to: " + destination);
            }
        };

        java.util.List data = java.util.List.of("row1", "row2", "row3");

        exporter.export(data, "output.csv");
        // Output: Exporting 3 rows to: output.csv

        exporter.exportWithHeader(data, "output.csv", "Sales Report");
        // Output:
        // Header: Sales Report
        // Exporting 3 rows to: output.csv

        exporter.exportIfValid(java.util.List.of(), "output.csv");
        // Output: Skipping export: invalid data

        exporter.exportIfValid(data, "output.csv");
        // Output: Exporting 3 rows to: output.csv
    }
}

9.2 The @implSpec Javadoc Tag

When you write a default method, other developers need to know what the default behavior is so they can decide whether to override it. Java introduced the @implSpec Javadoc tag for this purpose.

interface Cacheable {
    String getKey();
    Object getValue();

    /**
     * Returns the time-to-live in seconds for this cached entry.
     *
     * @implSpec The default implementation returns 3600 (1 hour).
     * Implementations that require shorter or longer cache durations
     * should override this method.
     *
     * @return time-to-live in seconds
     */
    default int getTtlSeconds() {
        return 3600;
    }

    /**
     * Determines whether this entry should be cached.
     *
     * @implSpec The default implementation returns true if both key and value
     * are non-null. Implementations may add additional criteria.
     *
     * @return true if this entry is eligible for caching
     */
    default boolean isCacheable() {
        return getKey() != null && getValue() != null;
    }
}

class UserSession implements Cacheable {
    private final String userId;
    private final Object sessionData;

    UserSession(String userId, Object sessionData) {
        this.userId = userId;
        this.sessionData = sessionData;
    }

    @Override
    public String getKey() { return "session:" + userId; }

    @Override
    public Object getValue() { return sessionData; }

    // Override: sessions expire faster
    @Override
    public int getTtlSeconds() { return 900; } // 15 minutes
}

class AppConfig implements Cacheable {
    private final String name;
    private final String value;

    AppConfig(String name, String value) {
        this.name = name;
        this.value = value;
    }

    @Override
    public String getKey() { return "config:" + name; }

    @Override
    public Object getValue() { return value; }

    // Inherits default TTL of 3600 seconds -- fine for configuration
}

public class ImplSpecDemo {
    public static void main(String[] args) {
        UserSession session = new UserSession("user-42", "session-data");
        System.out.println(session.getKey() + " TTL: " + session.getTtlSeconds() + "s");
        // Output: session:user-42 TTL: 900s

        AppConfig config = new AppConfig("theme", "dark");
        System.out.println(config.getKey() + " TTL: " + config.getTtlSeconds() + "s");
        // Output: config:theme TTL: 3600s

        System.out.println("Session cacheable: " + session.isCacheable());
        // Output: Session cacheable: true
    }
}

10. Default Methods vs Abstract Classes

Before Java 8, if you wanted an interface with some implemented methods, you had to use an abstract class. Now that interfaces can have default methods, when should you use each? The answer depends on what you need.

Feature Interface (with defaults) Abstract Class
Multiple inheritance A class can implement many interfaces A class can extend only one abstract class
Instance state (fields) Only constants (static final) Can have mutable instance fields
Constructors No constructors Can have constructors
Access modifiers on methods All public (private in Java 9+) public, protected, package-private, private
Non-static fields Not allowed Allowed
Method bodies Default, static, private (Java 9+) Any method can have a body
Lambda compatibility Yes (if functional interface) No
Object methods Cannot override equals/hashCode/toString Can override any Object method
Versioning Adding a default is backward compatible Adding a concrete method is also safe
Design intent "Can do" -- capability contract "Is a" -- shared base implementation

Decision Guide

  • Use an interface when you are defining a capability that multiple unrelated classes might have (e.g., Comparable, Serializable, Closeable).
  • Use an abstract class when you have shared state (fields) or when you need to provide a substantial base implementation with protected methods that subclasses customize.
  • Use both together: Define the contract in an interface, provide a skeletal implementation in an abstract class. This is the pattern used by AbstractList (abstract class) implementing List (interface).
// Interface: defines the capability contract
interface Auditable {
    String getCreatedBy();
    java.time.LocalDateTime getCreatedAt();
    String getLastModifiedBy();
    java.time.LocalDateTime getLastModifiedAt();

    default String getAuditSummary() {
        return String.format("Created by %s on %s, last modified by %s on %s",
            getCreatedBy(), getCreatedAt(),
            getLastModifiedBy(), getLastModifiedAt());
    }
}

// Abstract class: provides shared state and constructors
abstract class AuditableEntity implements Auditable {
    private final String createdBy;
    private final java.time.LocalDateTime createdAt;
    private String lastModifiedBy;
    private java.time.LocalDateTime lastModifiedAt;

    protected AuditableEntity(String createdBy) {
        this.createdBy = createdBy;
        this.createdAt = java.time.LocalDateTime.now();
        this.lastModifiedBy = createdBy;
        this.lastModifiedAt = this.createdAt;
    }

    @Override
    public String getCreatedBy() { return createdBy; }
    @Override
    public java.time.LocalDateTime getCreatedAt() { return createdAt; }
    @Override
    public String getLastModifiedBy() { return lastModifiedBy; }
    @Override
    public java.time.LocalDateTime getLastModifiedAt() { return lastModifiedAt; }

    protected void markModified(String modifiedBy) {
        this.lastModifiedBy = modifiedBy;
        this.lastModifiedAt = java.time.LocalDateTime.now();
    }
}

// Concrete class: inherits state management from abstract class
class Order extends AuditableEntity {
    private String status;

    Order(String createdBy) {
        super(createdBy);
        this.status = "PENDING";
    }

    public void updateStatus(String newStatus, String modifiedBy) {
        this.status = newStatus;
        markModified(modifiedBy);  // From abstract class
    }

    public String getStatus() { return status; }
}

public class InterfaceVsAbstractClass {
    public static void main(String[] args) {
        Order order = new Order("Alice");
        System.out.println("Status: " + order.getStatus());
        // Output: Status: PENDING

        // Default method from interface
        System.out.println(order.getAuditSummary());
        // Output: Created by Alice on 2026-02-28T..., last modified by Alice on 2026-02-28T...

        order.updateStatus("SHIPPED", "Bob");
        System.out.println("Updated: " + order.getAuditSummary());
        // Output: Created by Alice on ..., last modified by Bob on ...
    }
}

11. Common Mistakes

Default methods are powerful, but misuse can lead to confusing, fragile designs. Here are the most common mistakes.

Mistake 1: Overusing Default Methods -- Turning Interfaces into Classes

Default methods should provide optional convenience behavior, not heavy implementations. If your interface has more default methods than abstract methods, you might actually need an abstract class.

// BAD: This is basically an abstract class disguised as an interface
interface BadUserService {
    default void createUser(String name) {
        System.out.println("Creating user: " + name);
        validateUser(name);
        saveToDatabase(name);
        sendWelcomeEmail(name);
    }

    default void validateUser(String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Name required");
        }
    }

    default void saveToDatabase(String name) {
        System.out.println("Saving " + name + " to database");
    }

    default void sendWelcomeEmail(String name) {
        System.out.println("Sending email to " + name);
    }
    // Problem: No abstract methods! What does implementing this interface mean?
    // There is nothing to customize. This should be a class.
}

// GOOD: Interface defines the contract, defaults provide optional convenience
interface GoodUserService {
    // Abstract: implementations must provide these
    void save(String name);
    void sendNotification(String name, String message);

    // Default: optional convenience built on the abstract methods
    default void createAndNotify(String name) {
        save(name);
        sendNotification(name, "Welcome, " + name + "!");
    }
}

public class OverusingDefaults {
    public static void main(String[] args) {
        GoodUserService service = new GoodUserService() {
            @Override
            public void save(String name) {
                System.out.println("Saved: " + name);
            }

            @Override
            public void sendNotification(String name, String message) {
                System.out.println("Notification to " + name + ": " + message);
            }
        };

        service.createAndNotify("Alice");
        // Output:
        // Saved: Alice
        // Notification to Alice: Welcome, Alice!
    }
}

Mistake 2: Trying to Maintain State in Interfaces

Interfaces cannot have instance fields. Attempting to simulate state with workarounds like Map-based storage creates fragile, thread-unsafe, and memory-leaking code.

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// BAD: Simulating state in an interface -- DON'T DO THIS
interface BadStateful {
    // Using a static map to fake instance state -- terrible idea!
    Map COUNTERS = new ConcurrentHashMap<>();

    default void increment() {
        COUNTERS.merge(this, 1, Integer::sum);
    }

    default int getCount() {
        return COUNTERS.getOrDefault(this, 0);
    }
    // Problems:
    // 1. Memory leak: entries never get removed (even if object is GC'd)
    // 2. Breaks encapsulation: any class can access COUNTERS
    // 3. Violates the purpose of interfaces
}

// GOOD: Use an abstract class when you need state
abstract class GoodStateful {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class StateInInterfaces {
    public static void main(String[] args) {
        // The abstract class version is clean and predictable
        GoodStateful counter = new GoodStateful() {};
        counter.increment();
        counter.increment();
        System.out.println("Count: " + counter.getCount());
        // Output: Count: 2
    }
}

Mistake 3: Ignoring the Diamond Problem

When a class implements two interfaces with the same default method, forgetting to override causes a compiler error. Always check for conflicts when implementing multiple interfaces.

Mistake 4: Breaking the Interface Segregation Principle (ISP)

Adding too many default methods to a single interface violates ISP. Just because you can keep adding defaults does not mean you should. Split large interfaces into focused ones.

// BAD: One massive interface with too many concerns
interface BadWorker {
    void doWork();

    default void logWork() { System.out.println("Logging..."); }
    default void notifyManager() { System.out.println("Notifying..."); }
    default void updateMetrics() { System.out.println("Updating metrics..."); }
    default void sendReport() { System.out.println("Sending report..."); }
    default void archiveResults() { System.out.println("Archiving..."); }
    // A class that just needs doWork() is forced to inherit all this baggage
}

// GOOD: Segregated interfaces
interface Worker {
    void doWork();
}

interface Loggable {
    default void logWork() { System.out.println("Logging..."); }
}

interface Observable {
    default void notifyManager() { System.out.println("Notifying..."); }
    default void updateMetrics() { System.out.println("Updating metrics..."); }
}

// Classes compose only what they need
class SimpleWorker implements Worker {
    @Override
    public void doWork() { System.out.println("Working..."); }
    // No unnecessary defaults inherited
}

class MonitoredWorker implements Worker, Loggable, Observable {
    @Override
    public void doWork() {
        logWork();
        System.out.println("Working...");
        updateMetrics();
        notifyManager();
    }
}

public class InterfaceSegregation {
    public static void main(String[] args) {
        new SimpleWorker().doWork();
        // Output: Working...

        System.out.println();
        new MonitoredWorker().doWork();
        // Output:
        // Logging...
        // Working...
        // Updating metrics...
        // Notifying...
    }
}

Mistake 5: Default Methods That Override Object Methods

You cannot define default methods for equals(), hashCode(), or toString() in an interface. The compiler will reject it. These methods always come from Object or the implementing class.

interface Displayable {
    String getDisplayName();

    // COMPILER ERROR: default method toString in interface Displayable
    // overrides a member of java.lang.Object
    // default String toString() {
    //     return getDisplayName();
    // }

    // CORRECT: Use a differently named method
    default String toDisplayString() {
        return "[" + getDisplayName() + "]";
    }
}

class Product implements Displayable {
    private final String name;

    Product(String name) { this.name = name; }

    @Override
    public String getDisplayName() { return name; }

    // Override toString() in the CLASS, not the interface
    @Override
    public String toString() { return toDisplayString(); }
}

public class ObjectMethodsDemo {
    public static void main(String[] args) {
        Product p = new Product("Laptop");
        System.out.println(p.toDisplayString());
        // Output: [Laptop]
        System.out.println(p);
        // Output: [Laptop]
    }
}

12. Best Practices

Practice Do Don't
Keep defaults simple Build defaults on top of abstract methods Put complex business logic in defaults
Document behavior Use @implSpec to describe default behavior Leave default behavior undocumented
Prefer composition Use small, focused interfaces that classes compose Create one giant interface with many defaults
Use for evolution Add defaults when extending published interfaces Use defaults as an excuse for lazy interface design
Static for utilities Put factory and helper methods as static Create separate companion utility classes
Respect ISP Split interfaces by responsibility Force implementors to inherit irrelevant defaults
Override intentionally Override defaults when you have better behavior Override defaults just to log or add trivial wrappers
Handle diamond conflicts Explicitly override and delegate with super Assume the compiler will "figure it out"
Use private methods Extract shared logic into private methods (Java 9+) Duplicate code across multiple defaults
No state Rely on abstract method contracts for data Simulate instance state with static maps

13. Complete Practical Example -- Plugin System

This example demonstrates a realistic plugin system that uses default methods, static methods, private methods (Java 9+), the diamond problem, and interface evolution -- all in one cohesive design.

The scenario: A text processing framework where plugins can transform, validate, and format text. New plugins can be added without modifying the framework.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

// ============================================================
// Core Plugin Interface
// ============================================================
interface TextPlugin {
    // Abstract: every plugin must define its transformation
    String process(String input);

    // Abstract: every plugin must identify itself
    String getName();

    // Default: chain multiple processing steps
    default String processAll(List inputs) {
        return inputs.stream()
            .map(this::process)
            .collect(Collectors.joining("\n"));
    }

    // Default: safe processing with error handling
    default String safeProcess(String input) {
        if (input == null || input.isEmpty()) {
            return "[empty input]";
        }
        try {
            return process(input);
        } catch (Exception e) {
            return "[error: " + e.getMessage() + "]";
        }
    }

    // Default: logging wrapper
    default String processWithLogging(String input) {
        System.out.println("[" + getName() + "] Processing: " + input);
        String result = process(input);
        System.out.println("[" + getName() + "] Result: " + result);
        return result;
    }

    // Static: factory methods
    static TextPlugin uppercase() {
        return new TextPlugin() {
            @Override
            public String process(String input) { return input.toUpperCase(); }
            @Override
            public String getName() { return "UppercasePlugin"; }
        };
    }

    static TextPlugin replace(String target, String replacement) {
        return new TextPlugin() {
            @Override
            public String process(String input) {
                return input.replace(target, replacement);
            }
            @Override
            public String getName() {
                return "ReplacePlugin(" + target + "->" + replacement + ")";
            }
        };
    }

    static TextPlugin chain(TextPlugin... plugins) {
        return new TextPlugin() {
            @Override
            public String process(String input) {
                String result = input;
                for (TextPlugin plugin : plugins) {
                    result = plugin.process(result);
                }
                return result;
            }
            @Override
            public String getName() { return "ChainedPlugin"; }
        };
    }
}

// ============================================================
// Validation Capability (separate interface -- ISP)
// ============================================================
interface Validatable {
    default boolean isValidInput(String input) {
        return input != null && !input.isEmpty() && input.length() <= 10000;
    }

    default String validateAndProcess(String input, java.util.function.Function processor) {
        if (!isValidInput(input)) {
            throw new IllegalArgumentException("Invalid input");
        }
        return processor.apply(input);
    }
}

// ============================================================
// Metrics Capability (separate interface -- ISP)
// ============================================================
interface Measurable {
    default long measureProcessingTime(Runnable task) {
        long start = System.nanoTime();
        task.run();
        long elapsed = System.nanoTime() - start;
        return elapsed / 1_000_000; // milliseconds
    }
}

// ============================================================
// Concrete Plugins
// ============================================================
class TrimPlugin implements TextPlugin {
    @Override
    public String process(String input) {
        return input.trim();
    }

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

class CensorPlugin implements TextPlugin, Validatable {
    private final List bannedWords;

    CensorPlugin(List bannedWords) {
        this.bannedWords = bannedWords;
    }

    @Override
    public String process(String input) {
        String result = input;
        for (String word : bannedWords) {
            result = result.replaceAll("(?i)" + word, "***");
        }
        return result;
    }

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

    // Override Validatable default with stricter validation
    @Override
    public boolean isValidInput(String input) {
        return Validatable.super.isValidInput(input) && !input.isBlank();
    }
}

class MarkdownPlugin implements TextPlugin, Validatable, Measurable {
    @Override
    public String process(String input) {
        return input
            .replaceAll("\\*\\*(.+?)\\*\\*", "$1")
            .replaceAll("\\*(.+?)\\*", "$1")
            .replaceAll("`(.+?)`", "$1");
    }

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

// ============================================================
// Plugin Pipeline
// ============================================================
class PluginPipeline {
    private final List plugins = new ArrayList<>();

    void addPlugin(TextPlugin plugin) {
        plugins.add(plugin);
    }

    String execute(String input) {
        String result = input;
        for (TextPlugin plugin : plugins) {
            result = plugin.safeProcess(result);
        }
        return result;
    }

    void executeWithLogging(String input) {
        String result = input;
        for (TextPlugin plugin : plugins) {
            result = plugin.processWithLogging(result);
        }
        System.out.println("Final result: " + result);
    }
}

// ============================================================
// Main Application
// ============================================================
public class PluginSystemDemo {
    public static void main(String[] args) {
        System.out.println("=== Individual Plugins ===");

        // Using static factory methods
        TextPlugin upper = TextPlugin.uppercase();
        System.out.println(upper.process("hello world"));
        // Output: HELLO WORLD

        TextPlugin replacer = TextPlugin.replace("Java", "Java 8");
        System.out.println(replacer.process("Learning Java is fun!"));
        // Output: Learning Java 8 is fun!

        // Chained plugin via static factory
        TextPlugin pipeline = TextPlugin.chain(
            new TrimPlugin(),
            TextPlugin.uppercase(),
            TextPlugin.replace(" ", "_")
        );
        System.out.println(pipeline.process("  hello world  "));
        // Output: HELLO_WORLD

        System.out.println("\n=== Censor Plugin with Validation ===");

        CensorPlugin censor = new CensorPlugin(List.of("spam", "scam"));
        System.out.println(censor.process("This is spam and scam content"));
        // Output: This is *** and *** content

        System.out.println("Valid input: " + censor.isValidInput("test"));
        // Output: Valid input: true

        System.out.println("Blank valid: " + censor.isValidInput("   "));
        // Output: Blank valid: false (stricter override)

        System.out.println("\n=== Markdown Plugin with Timing ===");

        MarkdownPlugin markdown = new MarkdownPlugin();
        String html = markdown.process("This is **bold** and *italic* and `code`");
        System.out.println(html);
        // Output: This is bold and italic and code

        long ms = markdown.measureProcessingTime(() ->
            markdown.process("Processing **this** text")
        );
        System.out.println("Processing took: " + ms + "ms");
        // Output: Processing took: 0ms (very fast)

        System.out.println("\n=== Plugin Pipeline ===");

        PluginPipeline fullPipeline = new PluginPipeline();
        fullPipeline.addPlugin(new TrimPlugin());
        fullPipeline.addPlugin(new CensorPlugin(List.of("bad")));
        fullPipeline.addPlugin(TextPlugin.uppercase());

        String result = fullPipeline.execute("  this has bad words  ");
        System.out.println("Pipeline result: " + result);
        // Output: Pipeline result: THIS HAS *** WORDS

        System.out.println("\n=== Safe Processing (null/error handling) ===");

        TextPlugin upper2 = TextPlugin.uppercase();
        System.out.println(upper2.safeProcess(null));
        // Output: [empty input]
        System.out.println(upper2.safeProcess(""));
        // Output: [empty input]
        System.out.println(upper2.safeProcess("works fine"));
        // Output: WORKS FINE

        System.out.println("\n=== Batch Processing ===");

        List batch = List.of("hello", "world", "java");
        System.out.println(upper2.processAll(batch));
        // Output:
        // HELLO
        // WORLD
        // JAVA
    }
}

Concepts demonstrated in this example:

# Concept Where Used
1 Abstract methods in interfaces TextPlugin.process(), TextPlugin.getName()
2 Default methods processAll(), safeProcess(), processWithLogging()
3 Static factory methods TextPlugin.uppercase(), TextPlugin.replace(), TextPlugin.chain()
4 Default methods calling abstract methods safeProcess() calls process()
5 Interface Segregation Principle Separate Validatable and Measurable interfaces
6 Overriding default methods CensorPlugin.isValidInput() overrides Validatable's default
7 Calling super default with Interface.super CensorPlugin.isValidInput() delegates to Validatable.super
8 Multiple interface implementation MarkdownPlugin implements TextPlugin, Validatable, Measurable
9 Anonymous class with interface Static factory methods return anonymous implementations
10 Template method pattern via defaults safeProcess() wraps process() with null/error handling
11 Strategy pattern Different TextPlugin implementations plugged into PluginPipeline
12 Method references with defaults this::process in processAll()

14. Quick Reference

Topic Key Point
Default method syntax default returnType method() { body }
Static method syntax static returnType method() { body }
Private method syntax (9+) private returnType method() { body }
Why default methods exist Backward-compatible interface evolution (adding methods without breaking implementors)
Why static methods exist Utility and factory methods that belong to the interface, not instances
Diamond problem rule 1 Classes always win over interface defaults
Diamond problem rule 2 More specific (sub) interface wins over parent interface
Diamond problem rule 3 If unresolved, the class must explicitly override
Calling a specific default InterfaceName.super.method()
Functional interface compatibility Default and static methods do NOT count toward the single abstract method limit
Cannot override Object methods Defaults cannot define equals(), hashCode(), or toString()
Static methods not inherited Must call via InterfaceName.staticMethod(), never via an instance
Use interface when Defining a capability contract; need multiple inheritance
Use abstract class when Need instance state, constructors, or protected methods
Best pattern Interface for contract + abstract class for skeletal implementation



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *