Skip to main content
Advertisement

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.

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
}

ElectricCar car = new ElectricCar();
car.move(); // Running on electric motor.
car.stop(); // Vehicle stopping. (default)
car.fuel(); // Charging with electricity. (overridden)

2. static Methods (Java 8+)

Utility methods callable as InterfaceName.methodName(). Cannot be overridden.

interface Validator<T> {
boolean validate(T value);

static Validator<String> notEmpty() {
return s -> s != null && !s.isBlank();
}

static Validator<Integer> positive() {
return n -> n > 0;
}
}

Validator<String> nameValidator = Validator.notEmpty();
System.out.println(nameValidator.validate("Alice")); // true
System.out.println(nameValidator.validate("")); // false

3. private Methods (Java 9+)

Extract common helper code for use only within the interface. Eliminates code duplication between default methods.

interface Logger {
void log(String message);

default void logInfo(String message) {
log(formatMessage("INFO", message));
}

default void logError(String message) {
log(formatMessage("ERROR", message));
}

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);

default StringTransformer andThen(StringTransformer after) {
return s -> after.transform(this.transform(s));
}
}

StringTransformer toUpper = s -> s.toUpperCase();
StringTransformer exclaim = s -> s + "!!!";
StringTransformer trim = String::trim;

StringTransformer pipeline = trim.andThen(toUpper).andThen(exclaim);
System.out.println(pipeline.transform(" hello ")); // HELLO!!!

java.util.function Package - Key Functional Interfaces

import java.util.function.*;

// 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
System.out.println(isLong.negate().test("Hi")); // true

// 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);
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> both = print.andThen(log);
both.accept("test"); // Runs both actions sequentially

// Supplier<T>: takes nothing, returns T
Supplier<List<String>> listFactory = ArrayList::new;
List<String> newList = listFactory.get();

// UnaryOperator<T>: takes T, returns T
UnaryOperator<String> bracket = s -> "[" + s + "]";
System.out.println(bracket.apply("Java")); // [Java]

// BinaryOperator<T>: takes two T, returns T
BinaryOperator<Integer> sum = (a, b) -> a + b;
System.out.println(sum.apply(10, 20)); // 30

Pro Tip

Interface design principles:

  1. Keep interfaces small: Smaller, focused interfaces are better (Interface Segregation Principle).

  2. Don't abuse default methods: They exist for backward compatibility, not complex business logic.

  3. Function composition: Leverage Function.andThen(), Predicate.and() etc. for declarative code.

    Predicate<String> validEmail =
    ((Predicate<String>) s -> s.contains("@"))
    .and(s -> s.contains("."))
    .and(s -> s.length() > 5);
Advertisement