Skip to Content
DocsJavaFunctional Interface

Java Functional Interfaces

What is a Functional Interface?

A functional interface is an interface with exactly one abstract method (SAM — Single Abstract Method). It can have multiple default or static methods, but only one abstract method.

Functional interfaces are the foundation of lambda expressions and method references in Java.

@FunctionalInterface public interface Processor { void process(String input); // single abstract method // These don't count against SAM rule: default void processAll(List<String> items) { items.forEach(this::process); } static Processor noOp() { return input -> {}; } }

The @FunctionalInterface Annotation

This annotation is optional but recommended. It:

  • Documents intent clearly
  • Triggers compile-time validation
  • Prevents accidental addition of abstract methods
@FunctionalInterface public interface Calculator { int calculate(int a, int b); } // Compile error if you add another abstract method: @FunctionalInterface public interface Calculator { int calculate(int a, int b); int subtract(int a, int b); // ERROR: not a functional interface }

Built-in Functional Interfaces (java.util.function)

Java provides 43+ functional interfaces. Here are the essential ones:

Core Four

InterfaceMethodInput → OutputUse Case
Function<T,R>R apply(T t)T → RTransform data
Consumer<T>void accept(T t)T → voidSide effects
Supplier<T>T get()() → TLazy values, factories
Predicate<T>boolean test(T t)T → booleanFiltering, validation

Primitive Specializations (avoid boxing overhead)

InterfaceMethodPurpose
IntFunction<R>R apply(int)int → R
ToIntFunction<T>int applyAsInt(T)T → int
IntConsumervoid accept(int)Consume int
IntSupplierint getAsInt()Supply int
IntPredicateboolean test(int)Test int
IntUnaryOperatorint applyAsInt(int)int → int
IntBinaryOperatorint applyAsInt(int, int)(int, int) → int

Similar variants exist for long and double.

Two-Argument Variants

InterfaceMethodInput → Output
BiFunction<T,U,R>R apply(T t, U u)(T, U) → R
BiConsumer<T,U>void accept(T t, U u)(T, U) → void
BiPredicate<T,U>boolean test(T t, U u)(T, U) → boolean

Special Cases

InterfaceMethodPurpose
UnaryOperator<T>T apply(T t)Same type in/out (extends Function<T,T>)
BinaryOperator<T>T apply(T t1, T t2)Two same types → one (extends BiFunction<T,T,T>)
Runnablevoid run()No input, no output
Callable<V>V call() throws ExceptionNo input, returns value, can throw

Practical Examples

1. Function<T, R> — Transformation

// Basic transformation Function<String, Integer> length = String::length; Function<String, String> toUpper = String::toUpperCase; int len = length.apply("hello"); // 5 // Chaining with andThen and compose Function<String, String> trim = String::trim; Function<String, String> pipeline = trim .andThen(toUpper) .andThen(s -> s.replace(" ", "_")); String result = pipeline.apply(" hello world "); // "HELLO_WORLD" // Real-world: DTO mapping Function<User, UserDTO> toDto = user -> new UserDTO( user.getId(), user.getEmail(), user.getFullName() ); List<UserDTO> dtos = users.stream() .map(toDto) .toList();

2. Consumer — Side Effects

// Basic consumer Consumer<String> print = System.out::println; Consumer<String> log = msg -> logger.info("Received: {}", msg); // Chaining consumers Consumer<Order> processOrder = order -> orderService.validate(order); Consumer<Order> notifyCustomer = order -> emailService.send(order.getEmail()); Consumer<Order> updateInventory = order -> inventoryService.reduce(order.getItems()); Consumer<Order> fullPipeline = processOrder .andThen(notifyCustomer) .andThen(updateInventory); orders.forEach(fullPipeline); // Real-world: Event handling public class EventBus { private final Map<Class<?>, List<Consumer<?>>> handlers = new ConcurrentHashMap<>(); public <T> void subscribe(Class<T> eventType, Consumer<T> handler) { handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>()) .add(handler); } @SuppressWarnings("unchecked") public <T> void publish(T event) { List<Consumer<?>> eventHandlers = handlers.get(event.getClass()); if (eventHandlers != null) { eventHandlers.forEach(h -> ((Consumer<T>) h).accept(event)); } } }

3. Supplier — Lazy Evaluation & Factories

// Lazy initialization Supplier<ExpensiveObject> lazyObject = () -> { System.out.println("Creating expensive object..."); return new ExpensiveObject(); }; // Object only created when get() is called ExpensiveObject obj = lazyObject.get(); // Memoization pattern public static <T> Supplier<T> memoize(Supplier<T> supplier) { AtomicReference<T> cache = new AtomicReference<>(); return () -> { T value = cache.get(); if (value == null) { synchronized (cache) { value = cache.get(); if (value == null) { value = supplier.get(); cache.set(value); } } } return value; }; } Supplier<Config> configSupplier = memoize(() -> loadConfigFromFile()); // Real-world: Default values public <T> T getOrDefault(T value, Supplier<T> defaultSupplier) { return value != null ? value : defaultSupplier.get(); } String name = getOrDefault(user.getNickname(), () -> user.getFirstName()); // Factory pattern Map<String, Supplier<PaymentProcessor>> processors = Map.of( "CREDIT_CARD", CreditCardProcessor::new, "PAYPAL", PayPalProcessor::new, "CRYPTO", CryptoProcessor::new ); PaymentProcessor processor = processors.get(paymentType).get();

4. Predicate — Filtering & Validation

// Basic predicates Predicate<String> notEmpty = s -> s != null && !s.isEmpty(); Predicate<String> isEmail = s -> s.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"); Predicate<Integer> isPositive = n -> n > 0; // Combining predicates Predicate<String> validEmail = notEmpty.and(isEmail); Predicate<Integer> inRange = isPositive.and(n -> n <= 100); // Negation Predicate<String> isBlank = notEmpty.negate(); // Real-world: Specification pattern public class UserSpecifications { public static Predicate<User> isActive() { return user -> user.getStatus() == Status.ACTIVE; } public static Predicate<User> hasRole(Role role) { return user -> user.getRoles().contains(role); } public static Predicate<User> createdAfter(LocalDate date) { return user -> user.getCreatedAt().isAfter(date); } public static Predicate<User> emailVerified() { return user -> user.isEmailVerified(); } } // Usage List<User> eligibleUsers = users.stream() .filter(isActive() .and(hasRole(Role.PREMIUM)) .and(emailVerified())) .toList(); // Validation framework public class Validator<T> { private final List<Predicate<T>> rules = new ArrayList<>(); private final List<String> messages = new ArrayList<>(); public Validator<T> addRule(Predicate<T> rule, String errorMessage) { rules.add(rule); messages.add(errorMessage); return this; } public ValidationResult validate(T object) { List<String> errors = new ArrayList<>(); for (int i = 0; i < rules.size(); i++) { if (!rules.get(i).test(object)) { errors.add(messages.get(i)); } } return new ValidationResult(errors.isEmpty(), errors); } } Validator<User> userValidator = new Validator<User>() .addRule(u -> u.getEmail() != null, "Email is required") .addRule(u -> u.getAge() >= 18, "Must be 18 or older") .addRule(u -> u.getPassword().length() >= 8, "Password too short"); ValidationResult result = userValidator.validate(newUser);

5. BiFunction & BiConsumer — Two Arguments

// BiFunction: combining two values BiFunction<String, String, String> concat = String::concat; BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; // Real-world: Map merge operations Map<String, Integer> inventory = new HashMap<>(); BiFunction<Integer, Integer, Integer> sumQuantities = Integer::sum; inventory.merge("apple", 5, sumQuantities); inventory.merge("apple", 3, sumQuantities); // apple -> 8 // BiConsumer: processing key-value pairs BiConsumer<String, Integer> printEntry = (k, v) -> System.out.println(k + " = " + v); inventory.forEach(printEntry); // Real-world: Configurable mapper public class ConfigurableMapper<S, T> { private final Map<String, BiFunction<S, T, ?>> fieldMappers = new HashMap<>(); public ConfigurableMapper<S, T> map(String field, BiFunction<S, T, ?> mapper) { fieldMappers.put(field, mapper); return this; } public void apply(S source, T target) { fieldMappers.forEach((field, mapper) -> { Object value = mapper.apply(source, target); // apply value to target field via reflection }); } }

6. UnaryOperator & BinaryOperator

// UnaryOperator: same type transformation UnaryOperator<String> addPrefix = s -> "PREFIX_" + s; UnaryOperator<Integer> doubleIt = n -> n * 2; // Chaining UnaryOperator<String> process = ((UnaryOperator<String>) String::trim) .andThen(String::toUpperCase) .andThen(addPrefix); // Real-world: List transformation List<String> names = new ArrayList<>(List.of(" alice ", " bob ")); names.replaceAll(String::trim); // replaceAll takes UnaryOperator // BinaryOperator: reduction operations BinaryOperator<Integer> max = Integer::max; BinaryOperator<BigDecimal> sum = BigDecimal::add; // Real-world: Stream reductions Optional<BigDecimal> total = orders.stream() .map(Order::getAmount) .reduce(BigDecimal::add); // Custom accumulator BinaryOperator<Map<String, Integer>> mergeMaps = (m1, m2) -> { Map<String, Integer> result = new HashMap<>(m1); m2.forEach((k, v) -> result.merge(k, v, Integer::sum)); return result; };

Advanced Patterns

1. Function Composition

public class Pipeline<T> { private Function<T, T> pipeline = Function.identity(); public Pipeline<T> addStep(Function<T, T> step) { pipeline = pipeline.andThen(step); return this; } public T execute(T input) { return pipeline.apply(input); } } Pipeline<String> textPipeline = new Pipeline<String>() .addStep(String::trim) .addStep(String::toLowerCase) .addStep(s -> s.replaceAll("\\s+", " ")) .addStep(s -> s.substring(0, Math.min(100, s.length()))); String cleaned = textPipeline.execute(rawInput);

2. Currying & Partial Application

// Currying: transform multi-arg function into chain of single-arg functions Function<Integer, Function<Integer, Integer>> curriedAdd = a -> b -> a + b; Function<Integer, Integer> add5 = curriedAdd.apply(5); int result = add5.apply(3); // 8 // Real-world: Configurable formatter Function<String, Function<LocalDate, String>> dateFormatter = pattern -> date -> date.format(DateTimeFormatter.ofPattern(pattern)); Function<LocalDate, String> isoFormatter = dateFormatter.apply("yyyy-MM-dd"); Function<LocalDate, String> euFormatter = dateFormatter.apply("dd/MM/yyyy"); // Partial application with BiFunction public static <T, U, R> Function<U, R> partial(BiFunction<T, U, R> biFunc, T firstArg) { return u -> biFunc.apply(firstArg, u); } BiFunction<String, String, String> greet = (greeting, name) -> greeting + ", " + name + "!"; Function<String, String> sayHello = partial(greet, "Hello"); Function<String, String> sayGoodbye = partial(greet, "Goodbye"); sayHello.apply("Alice"); // "Hello, Alice!" sayGoodbye.apply("Bob"); // "Goodbye, Bob!"

3. Exception Handling

// Problem: Functional interfaces don't allow checked exceptions // Solution: Wrapper interfaces @FunctionalInterface public interface ThrowingFunction<T, R, E extends Exception> { R apply(T t) throws E; static <T, R, E extends Exception> Function<T, R> unchecked( ThrowingFunction<T, R, E> f) { return t -> { try { return f.apply(t); } catch (Exception e) { throw new RuntimeException(e); } }; } static <T, R, E extends Exception> Function<T, R> withDefault( ThrowingFunction<T, R, E> f, R defaultValue) { return t -> { try { return f.apply(t); } catch (Exception e) { return defaultValue; } }; } } // Usage List<URL> urls = paths.stream() .map(ThrowingFunction.unchecked(path -> new URL(path))) .toList(); // Or with Either/Result pattern public sealed interface Result<T> permits Success, Failure { static <T> Result<T> of(Supplier<T> supplier) { try { return new Success<>(supplier.get()); } catch (Exception e) { return new Failure<>(e); } } } record Success<T>(T value) implements Result<T> {} record Failure<T>(Exception error) implements Result<T> {} List<Result<URL>> results = paths.stream() .map(path -> Result.of(() -> new URL(path))) .toList();

4. Method References

// Four types of method references: // 1. Static method: ClassName::staticMethod Function<String, Integer> parse = Integer::parseInt; // 2. Instance method of particular object: instance::method String prefix = "Hello"; Function<String, String> greet = prefix::concat; // 3. Instance method of arbitrary object: ClassName::instanceMethod Function<String, String> upper = String::toUpperCase; BiFunction<String, String, Boolean> startsWith = String::startsWith; // 4. Constructor: ClassName::new Supplier<ArrayList<String>> listFactory = ArrayList::new; Function<String, StringBuilder> sbFactory = StringBuilder::new; // Array constructor IntFunction<String[]> arrayFactory = String[]::new; String[] array = arrayFactory.apply(10); // new String[10]

5. Composing Predicates for Query Building

public class QueryBuilder<T> { private Predicate<T> predicate = t -> true; public QueryBuilder<T> where(Predicate<T> condition) { predicate = predicate.and(condition); return this; } public QueryBuilder<T> or(Predicate<T> condition) { predicate = predicate.or(condition); return this; } public QueryBuilder<T> not(Predicate<T> condition) { predicate = predicate.and(condition.negate()); return this; } public List<T> execute(Collection<T> data) { return data.stream().filter(predicate).toList(); } } List<Product> results = new QueryBuilder<Product>() .where(p -> p.getPrice() > 100) .where(p -> p.getCategory().equals("Electronics")) .or(p -> p.isOnSale()) .not(p -> p.isDiscontinued()) .execute(products);

Best Practices

1. Prefer Built-in Interfaces

// ❌ Don't create custom interface when built-in exists interface StringProcessor { String process(String input); } // ✅ Use UnaryOperator<String> instead UnaryOperator<String> processor = String::toUpperCase;

2. Use Primitive Specializations for Performance

// ❌ Causes boxing/unboxing overhead Function<Integer, Integer> doubler = n -> n * 2; // ✅ Use primitive specialization IntUnaryOperator doubler = n -> n * 2; // Performance matters in streams int sum = numbers.stream() .mapToInt(Integer::intValue) // convert to IntStream .map(n -> n * 2) // uses IntUnaryOperator .sum();

3. Keep Lambdas Short

// ❌ Too complex for inline lambda list.stream() .filter(item -> { if (item == null) return false; String processed = item.trim().toLowerCase(); return processed.length() > 5 && processed.matches("[a-z]+") && !blacklist.contains(processed); }) .toList(); // ✅ Extract to method or compose predicates private boolean isValidItem(String item) { if (item == null) return false; String processed = item.trim().toLowerCase(); return processed.length() > 5 && processed.matches("[a-z]+") && !blacklist.contains(processed); } list.stream().filter(this::isValidItem).toList();

4. Avoid Side Effects in Functions

// ❌ Side effects in Function List<String> sideEffectList = new ArrayList<>(); Function<String, String> badFunction = s -> { sideEffectList.add(s); // side effect! return s.toUpperCase(); }; // ✅ Use Consumer for side effects, Function for transformation Consumer<String> collector = sideEffectList::add; Function<String, String> transformer = String::toUpperCase; list.forEach(item -> { String transformed = transformer.apply(item); collector.accept(transformed); });

5. Document Custom Functional Interfaces

/** * Processes a payment transaction and returns the result. * * @param <T> the type of payment request * @param <R> the type of payment response */ @FunctionalInterface public interface PaymentProcessor<T extends PaymentRequest, R extends PaymentResponse> { /** * Processes the payment. * * @param request the payment request * @return the payment response * @throws PaymentException if processing fails */ R process(T request) throws PaymentException; }

Java 21+ Considerations

Pattern Matching with Functional Interfaces

// Using pattern matching in lambdas (Java 21+) Function<Object, String> describe = obj -> switch (obj) { case Integer i -> "Integer: " + i; case String s -> "String: " + s; case List<?> l -> "List of size: " + l.size(); case null -> "null value"; default -> "Unknown: " + obj.getClass(); }; // With records record Point(int x, int y) {} record Circle(Point center, int radius) {} record Rectangle(Point topLeft, int width, int height) {} Function<Object, Double> area = shape -> switch (shape) { case Circle(var c, var r) -> Math.PI * r * r; case Rectangle(var p, var w, var h) -> (double) w * h; default -> 0.0; };

Sequenced Collections (Java 21+)

// New methods work well with functional interfaces SequencedCollection<String> seq = new LinkedHashSet<>(List.of("a", "b", "c")); Consumer<String> process = System.out::println; seq.reversed().forEach(process); // c, b, a

Summary

InterfaceMethodUse When You Need To
Function<T,R>apply(T): RTransform a value
Consumer<T>accept(T): voidPerform side effects
Supplier<T>get(): TLazily provide values
Predicate<T>test(T): booleanFilter or validate
UnaryOperator<T>apply(T): TTransform same type
BinaryOperator<T>apply(T,T): TReduce two to one
BiFunction<T,U,R>apply(T,U): RCombine two values
Runnablerun(): voidExecute without I/O
Callable<V>call(): VExecute and return

Functional interfaces enable cleaner, more composable code. Master the core four (Function, Consumer, Supplier, Predicate), use primitive specializations for performance, and leverage composition methods (andThen, compose, and, or) to build powerful pipelines.


Last updated on