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
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();
}
}
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);
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));
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);
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();
}
}
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);
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);
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();
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"));
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(""));
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");
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());
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));
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));
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.
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 ");
}
}
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; }
}
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));
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.
Originally published on my personal blog: https://matevosian.tech/blog/post/Java-interface-evolution

Top comments (0)