7.5 Advanced Interfaces
Since Java 8, interfaces can contain implementation code beyond just abstract method declarations. The addition of default, static, and private methods has significantly expanded the role of interfaces.
1. default Methods (Java 8+)
Provide a default implementation in the interface. Implementing classes can optionally override them.
Why Are They Needed?
Adding a new method to an existing interface would break every class that implements it. default methods were introduced to extend interfaces while maintaining backward compatibility.
interface Vehicle {
void move(); // abstract method (must implement)
default void stop() { // default method (optional override)
System.out.println("Vehicle stopping.");
}
default void fuel() {
System.out.println("Using standard fuel.");
}
}
class ElectricCar implements Vehicle {
@Override
public void move() {
System.out.println("Running on electric motor.");
}
@Override
public void fuel() { // Override only when needed
System.out.println("Charging with electricity.");
}
// stop() uses default implementation if not overridden
}
ElectricCar car = new ElectricCar();
car.move(); // Running on electric motor.
car.stop(); // Vehicle stopping. (default implementation)
car.fuel(); // Charging with electricity. (overridden)
Resolving the Diamond Problem
When multiple interfaces have the same default method, the implementing class must override it.
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // explicitly call A's default method
// or provide a completely new implementation
}
}
2. static Methods (Java 8+)
Define utility methods in the interface callable as InterfaceName.methodName() without an instance. Cannot be overridden.
interface Validator<T> {
boolean validate(T value);
// Factory method pattern — very useful
static <T> Validator<T> of(Validator<T> validator) {
return validator;
}
static Validator<String> notEmpty() {
return s -> s != null && !s.isBlank();
}
static Validator<Integer> positive() {
return n -> n > 0;
}
}
// Usage
Validator<String> nameValidator = Validator.notEmpty();
System.out.println(nameValidator.validate("Alice")); // true
System.out.println(nameValidator.validate("")); // false
Validator<Integer> ageValidator = Validator.positive();
System.out.println(ageValidator.validate(25)); // true
System.out.println(ageValidator.validate(-1)); // false
3. private Methods (Java 9+)
Extract common helper code used only inside the interface. Eliminates code duplication between default methods.
interface Logger {
void log(String message);
default void logInfo(String message) {
log(formatMessage("INFO", message)); // calls private method
}
default void logError(String message) {
log(formatMessage("ERROR", message)); // calls private method
}
// private: not visible outside the interface
private String formatMessage(String level, String message) {
return String.format("[%s] %s: %s",
java.time.LocalTime.now(), level, message);
}
}
4. Functional Interfaces and Lambdas
An interface with exactly one abstract method is a functional interface. Mark it with @FunctionalInterface and implement it concisely with a lambda.
@FunctionalInterface
interface StringTransformer {
String transform(String input);
// Only one abstract method allowed (default/static may have multiple)
default StringTransformer andThen(StringTransformer after) {
return s -> after.transform(this.transform(s));
}
}
// Lambda implementations
StringTransformer toUpper = s -> s.toUpperCase();
StringTransformer exclaim = s -> s + "!!!";
StringTransformer trim = String::trim; // method reference
// Chain with andThen
StringTransformer pipeline = trim.andThen(toUpper).andThen(exclaim);
System.out.println(pipeline.transform(" hello ")); // HELLO!!!
Key Functional Interfaces in java.util.function
import java.util.function.*;
import java.util.List;
import java.util.Arrays;
// --- Predicate<T>: takes T, returns boolean ---
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> hasA = s -> s.contains("a");
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.and(hasA).test("banana")); // true (and combination)
System.out.println(isLong.or(hasA).test("ok")); // false (or combination)
System.out.println(isLong.negate().test("Hi")); // true (negation)
// --- Function<T, R>: takes T, returns R ---
Function<String, Integer> strLen = String::length;
Function<Integer, String> intToStr = n -> "Number: " + n;
Function<String, String> combined = strLen.andThen(intToStr); // compose
System.out.println(combined.apply("Hello")); // Number: 5
// --- BiFunction<T, U, R>: takes two args, returns R ---
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("Java! ", 3)); // Java! Java! Java!
// --- Consumer<T>: takes T, no return ---
Consumer<String> print = System.out::println;
Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> printAndLog = print.andThen(log); // run sequentially
printAndLog.accept("test"); // executes both actions in order
// --- BiConsumer<T, U>: takes two args, no return ---
BiConsumer<String, Integer> printPair = (k, v) -> System.out.println(k + " = " + v);
printPair.accept("age", 30); // age = 30
// --- Supplier<T>: takes nothing, returns T ---
Supplier<List<String>> listFactory = ArrayList::new;
List<String> newList = listFactory.get(); // new ArrayList created
// --- UnaryOperator<T>: takes T, returns T (specialization of Function<T,T>) ---
UnaryOperator<String> addBracket = s -> "[" + s + "]";
UnaryOperator<Integer> doubleIt = n -> n * 2;
System.out.println(addBracket.apply("Java")); // [Java]
// --- BinaryOperator<T>: takes two T, returns T ---
BinaryOperator<Integer> sum = (a, b) -> a + b;
BinaryOperator<String> concat = String::concat;
System.out.println(sum.apply(10, 20)); // 30
System.out.println(concat.apply("Hello", " World")); // Hello World
Interface design principles:
-
Keep interfaces small: Smaller, focused interfaces are better (Interface Segregation Principle — ISP). Many small, cohesive interfaces beat one large one.
-
Don't abuse default methods:
defaultmethods are for backward compatibility, not for embedding complex business logic. -
Function composition patterns: Leverage
Function.andThen(),Predicate.and()etc. to write code in a declarative style.// Pipeline style (readable)
Predicate<String> validEmail =
((Predicate<String>) s -> s.contains("@"))
.and(s -> s.contains("."))
.and(s -> s.length() > 5);