DEV Community

Cover image for Java Interface Evolution: Best Practices and Strategies
Vardan Matevosian
Vardan Matevosian

Posted on

Java Interface Evolution: Best Practices and Strategies

 
 

Introduction

In this post we will explore the evolution of Java interfaces, focusing on features introduced in Long-Term Support (LTS) releases since Java 8.
We will examine how these features have enhanced interface design.

 
 

Timeline navigation

 

📍
March 2014 - Java 8
The Functional Revolution

 

Java 8 revolutionized interfaces by allowing them to include non-abstract methods, enabling backward-compatible API evolution and supporting functional programming via functional interfaces.

 

New interface features:

  • Default methods (default keyword) – provide concrete implementations.
  • Static methods in interfaces.
  • Functional interfaces – interfaces with exactly one abstract method (used with lambdas).

 
 

Default methods

Java 8 introduced a significant change to interfaces: the ability to define method implementations directly within the interface using the default keyword. This feature addresses the "interface evolution problem", where adding a new method to an existing interface would break all implementing classes.

Before Java 8, any change to an interface required all implementing classes to be modified to provide an implementation for the new method. This was a major obstacle to evolving APIs, as it could lead to widespread code changes and potential compatibility issues. Default methods provide a way to add new functionality to an interface without breaking existing implementations.

 
 

public interface MyInterface {

    void existingMethod();

    default void newDefaultMethod() {
        System.out.println("This is a default method.");
    }

}

public class MyClass implements MyInterface {

    public void existingMethod() {
        System.out.println("Implementing existing method.");
    }

    // No need to implement newDefaultMethod() unless specific behavior is desired
    public void someMethod() {
        newDefaultMethod();
    }

}
Enter fullscreen mode Exit fullscreen mode

 
 

Real JDK example of default method

 

Interface: java.util.List
 

Purpose: Sort the list in place using a Comparator.
 

public interface List<E> extends Collection<E> {
    default void sort(Comparator<? super E> c) {
        Collections.sort(this, c);
    }
}


// Usage:
List<String> list = new ArrayList<>(Arrays.asList("banana", "apple", "cherry"));
list.sort(Comparator.naturalOrder());

// Output: [apple, banana, cherry]
System.out.println(list); 

Enter fullscreen mode Exit fullscreen mode

 

 

Interface: java.lang.Iterable
 

Purpose: Enables concise iteration with lambdas.
 

public interface Iterable<T> {
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

List<String> list = Arrays.asList("apple", "banana", "cherry");
list.forEach(item -> System.out.println(item));

Enter fullscreen mode Exit fullscreen mode

 

 

Interface: java.util.Map
 

Purpose: Java 8 added several useful default methods to the Map interface.
 

public interface Map<K, V> {
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key)) ? v : defaultValue;
    }

    default V putIfAbsent(K key, V value) {
     V v = get(key);
     if (v == null) {
       v = put(key, value);
     }
     return v;
   }

   // Other default methods like remove, replace, compute, merge, etc.
}

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
Integer i = map.getOrDefault("banana", 0);

// i will be 0 since "banana" is not in the map
System.out.println(i);

Enter fullscreen mode Exit fullscreen mode

 

 

Static methods

Java 8 also introduced static methods in interfaces,
allowing developers to define utility methods that are related to the interface but do not require an instance of the implementing class to be invoked.
This feature helps organize code better and provides a way to group related functionality.

 
 

public interface MyInterface {
    static void utilityMethod() {
        System.out.println("This is a static method in the interface.");
    }
}

// Usage:

public class MyClass implements MyInterface {
    public void someMethod() {
        // Calling the static method
        MyInterface.utilityMethod();
    }
}

Enter fullscreen mode Exit fullscreen mode

 

 

Real JDK example of static method

 

Interface: java.util.Collections
 

Purpose: Provides utility methods for collection operations.
 

public final class Collections {
    public static <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }
}

// Usage:

List<String> empty = Collections.emptyList();

// Output: []
System.out.println(empty);

Enter fullscreen mode Exit fullscreen mode

 

 

Interface: java.util.Map
 

Purpose: Provides utility methods when using Map like "of()" method.
 

public interface Map<K, V> {
    static <K, V> Map<K, V> of() {
        return new HashMap<>();
    }

    static <K, V> Map<K, V> of(K k1, V v1) {
        Map<K, V> map = new HashMap<>();
        map.put(k1, v1);
        return map;
    }

// Additional overloaded of() methods for more key-value pairs
}


// Usage:

Map<String, Integer> map = Map.of("apple", 1, "banana", 2);

// Output: {apple=1, banana=2}
 System.out.println(map);


Enter fullscreen mode Exit fullscreen mode

 

 

Functional interfaces

Java 8 introduced the concept of functional interfaces, which are interfaces that contain exactly one abstract method.
This feature is crucial for enabling lambda expressions and method references, allowing for more concise and expressive code.

 
 

@FunctionalInterface
public interface MyFunctionalInterface {
    void singleAbstractMethod();

}

// Usage:

MyFunctionalInterface func = () -> System.out.println("Lambda expression implementation.");

// Output: Lambda expression implementation.
func.singleAbstractMethod(); 

Enter fullscreen mode Exit fullscreen mode

 

 

Real JDK example of functional interface

 

The java.util.function package was added in Java 8 and contains dozens of general-purpose functional interfaces.
Here are the most widely used ones—with real usage examples:
 

 

Interface: java.util.function.Predicate<T>
 

Purpose: Represents a boolean-valued function of one argument.
 

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

// Usage:

Predicate<String> isEmpty = str -> str.isEmpty();

// Output: true
System.out.println(isEmpty.test(""));

// Output: false
System.out.println(isEmpty.test("hello")); 

Enter fullscreen mode Exit fullscreen mode

 

Interface: java.util.function.Function<T, T>
 

Purpose: Represents a function that accepts one argument and produces a result.
 

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

// Usage:

Function<String, Integer> stringLength = str -> str.length();

// Output: 5
System.out.println(stringLength.apply("hello"));

// Output: 0
System.out.println(stringLength.apply("")); 

Enter fullscreen mode Exit fullscreen mode

 

Interface: java.util.function.Consumer<T>
 

Purpose: Represents an operation that accepts a single input argument and returns no result.
 

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

// Usage:

Consumer<String> print = str -> System.out.println(str);

// Output: hello
print.accept("hello");

// Output: world
print.accept("world");

Enter fullscreen mode Exit fullscreen mode

 

Interface: java.util.function.Supplier<T>
 

Purpose: Represents a supplier of results.
 

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

// Usage:

Supplier<Double> randomValue = () -> Math.random();

// Output: Random double value
System.out.println(randomValue.get());

// Output: Another random double value
System.out.println(randomValue.get()); 
Enter fullscreen mode Exit fullscreen mode

 

Interface: java.util.function.UnaryOperator<T> (extends Function<T, T>)
 

Purpose: Represents a function that takes one argument and returns a result of the same type.
 

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
}

// Usage:

UnaryOperator<Integer> square = x -> x * x;

// Output: 25
System.out.println(square.apply(5));

// Output: 0
System.out.println(square.apply(0)); 
Enter fullscreen mode Exit fullscreen mode

 

Interface: java.util.function. BinaryOperator<T> (extends BiFunction<T, T, T>)
 

Purpose: Represents an operation upon two operands of the same type, producing a result of the same type.
 

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
}

// Usage:

BinaryOperator<Integer> add = (x, y) -> x + y;

// or

BinaryOperator<Integer> addWithMethodReference = Integer::sum;

// Output: 15
System.out.println(add.apply(5, 10));

// Output: 0
System.out.println(addWithMethodReference.apply(0, 0)); 

Enter fullscreen mode Exit fullscreen mode

 

Some of the interfaces that was lunched before Java 8, updated to be functional interfaces:

 
 

Interface Package Abstract Method Common Use
Runnable java.lang void run() Threading, executors
Callable<V> java.util.concurrent V call() Threading with return value
Comparator<T> java.util int compare(T a, T b) Sorting
ActionListener java.awt.event void actionPerformed(...) GUI events

 
 

Why These Matter

 
 

These features significantly enhanced Java's interface capabilities:
Backward compatibility: Millions of existing Map or List implementations (e.g., ArrayList, HashMap, or custom ones) automatically got these new methods without recompilation.
Cleaner code: Eliminated boilerplate (e.g., manual loops for filtering).
Functional style: Enabled fluent, declarative data processing.

 

 

📍
September 2018 - Java 11
Private methods - inherited from Java 9

 

Java 11 is an LTS release but did not add any new interface features.
However, it includes all interface enhancements from Java 9,
which introduced private methods in interfaces (standardized and fully supported in Java 11).

Private methods in interfaces allow developers to encapsulate common logic that can be reused by default methods within the same interface.

 
 


public interface DataProcessor {

  default void process(String data) {
    String cleaned = sanitize(data);
    System.out.println("Processed: " + cleaned);
  }

  private String sanitize(String input) {
    return input.trim().toLowerCase();
  }

  @SuppressWarnings("checkstyle:WhitespaceAround")
  static DataProcessor create() {
    return new DataProcessor() {
    };
  }
}

// Usage:

public class TestInterfacePrivateMethod {

    public static void main(String[] args) {
        DataProcessor processor = DataProcessor.create();

        // Output: Processed: hello world
        processor.process("  HELLO WORLD  ");
    }

}

Enter fullscreen mode Exit fullscreen mode

 

 

📍
September 2021 - Java 17
Sealed interfaces

 

Java 17 introduces sealed interfaces (along with sealed classes),
allowing you to restrict which classes or interfaces can implement your interface.
This enhances domain modeling and works seamlessly with pattern matching.

New interface feature: Sealed interfaces with permits clause

 
 

Rules:

  • Subtypes must be listed in permits (or in the same file).
  • Each permitted subtype must have a class modifier: final, sealed, or non-sealed.

 

sealed: restricts who can extend/implement you.
final: cannot be extended.
non-sealed: can be extended, even though your parent is sealed.

Note: non-sealed is only allowed on classes (or interfaces) that directly extend
or implement a sealed type and are listed in its permits clause.

 
 


// Sealed interface. Can only be implemented by Circle, Rectangle, and Polygon.
public sealed interface Shape permits Circle, Rectangle, Polygon {}

// Final class. Cannot be extended further.
final class Circle implements Shape {
  public final double radius;
  public Circle(double radius) { this.radius = radius; }
}

// Sealed subclass (continues restriction)
sealed class Polygon implements Shape permits Quadrilateral {
  public final int sides;
  public Polygon(int sides) { this.sides = sides; }
}

// Can be extended further
non-sealed class Rectangle implements Shape {
  public final double width, height;
  public Rectangle(double w, double h) { width = w; height = h; }
}


Enter fullscreen mode Exit fullscreen mode

 

 

📍
September 2023 - Java 21
Better Integration

 

Java 21 does not add new interface-specific features,
but it enhances how interfaces are used through: Pattern matching for switch
Simplifies type checks and casts when working with interface implementations.

 
 

sealed interface Expr permits Constant, Add {}

record Constant(int value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}

public static int eval(Expr e) {
  return switch (e) {
    case Constant c -> c.value;
    case Add a -> eval(a.left) + eval(a.right);
  };
}

// Usage:

Expr expr = new Add(new Constant(3), new Constant(5));

// Output: 8
System.out.println(eval(expr));


Enter fullscreen mode Exit fullscreen mode

 
 

Best practices

  • Use default methods sparingly: Default methods should be used to add new functionality or provide optional implementations, not to fundamentally change the interface's contract.

  • Consider the impact on implementing classes: When adding default methods, carefully consider how they will affect existing implementations.

  • Use static methods for utility functions: Static methods should be used for utility functions that are closely related to the interface.

  • Document default and static methods clearly: Provide clear and concise documentation for all default and static methods, explaining their purpose and usage.

  • Avoid state in interfaces: Interfaces should generally not maintain state. Default methods should primarily operate on the state of the implementing class.

 
 

Conclusion

SAST remains one of the cornerstones of a mature SSDLC. It provides early, actionable insights into code security, reduces risk, empowers developers, and fortifies applications before deployment.
Implemented with care, SAST becomes more than just a detection tool; it becomes a catalyst for a long-term secure engineering culture.

Java interface evolution

 
 

Originally published on my personal blog: https://matevosian.tech/blog/post/Java-interface-evolution

 
 

Top comments (0)