Functional Interfaces
The foundation that makes lambda expressions possible in Java
Predicate: n -> n > 0What is a Functional Interface?
A Functional Interface is simply an interface with exactly one abstract method. That's it! This single method is the "function" that your lambda expression will implement.
💡 Think of it this way:
A functional interface is like a contract with one job. When you write a lambda, you're providing the implementation for that one job. Java knows which method you're implementing because there's only one choice!
The Rules:
// This IS a functional interface (one abstract method)@FunctionalInterfacepublicinterfaceCalculator{intcalculate(int a,int b);// THE abstract method// These DON'T count against the "one method" rule:defaultvoidprintInfo(){System.out.println("Calculator interface");}staticCalculatorcreateAdder(){return(a, b)-> a + b;}}// This is NOT a functional interface (two abstract methods)publicinterfaceNotFunctional{voidmethod1();voidmethod2();// Oops! Two abstract methods}The @FunctionalInterface Annotation
The @FunctionalInterface annotation is optional but highly recommended. It tells the compiler to verify that your interface follows the functional interface rules.
✅ What it does:
- Compile-time check for exactly one abstract method
- Prevents accidental addition of second abstract method
- Documents your intent clearly
- IDE support for lambda conversion
⚠️ Important:
- The annotation is NOT required
- Interfaces without it can still be functional
Runnable,Comparatorwork with lambdas (pre-Java 8)- Think of it like
@Override– helpful safety check
// With annotation - compiler enforces the rule@FunctionalInterfacepublicinterfaceMyFunction{voidexecute();// void anotherMethod(); // Compiler ERROR if you add this!}// Without annotation - still works, but no safety netpublicinterfaceAlsoFunctional{voidexecute();// If someone adds another method, lambdas will break!}Built-in Functional Interfaces (java.util.function)
Java 8 comes with a rich set of pre-built functional interfaces. You'll use these 90% of the time instead of creating your own!
| Interface | Input → Output | Method | Use Case |
|---|---|---|---|
| Predicate<T> | T → boolean | test(T) | Filtering, checking conditions |
| Function<T,R> | T → R | apply(T) | Transforming/mapping values |
| Consumer<T> | T → void | accept(T) | Performing actions (printing, saving) |
| Supplier<T> | () → T | get() | Creating/providing values |
| UnaryOperator<T> | T → T | apply(T) | Transform same type |
| BinaryOperator<T> | (T, T) → T | apply(T,T) | Combining two values (add, max) |
🎯 Memory Trick:
Predicate = tests (returns true/false)
Function = transforms (input → different output)
Consumer = consumes (takes input, returns nothing)
Supplier = supplies (takes nothing, returns output)
Detailed Examples of Each Interface
importjava.util.function.*;// ========== PREDICATE: Test if something is true ==========// "Does this thing match my condition?"Predicate<String> isEmpty = s -> s.isEmpty();Predicate<Integer> isPositive = n -> n >0;Predicate<String> startsWithA = s -> s.startsWith("A");System.out.println(isEmpty.test(""));// trueSystem.out.println(isPositive.test(-5));// falseSystem.out.println(startsWithA.test("Alice"));// true// Used in: stream.filter(), removeIf(), Optional.filter()// ========== FUNCTION: Transform one thing to another ==========// "Convert X to Y"Function<String,Integer> length = s -> s.length();Function<Integer,String> intToString = n ->"Number: "+ n;System.out.println(length.apply("Hello"));// 5System.out.println(intToString.apply(42));// "Number: 42"// Used in: stream.map(), Optional.map()// ========== CONSUMER: Do something with the input ==========// "Take this and DO something (print, save, send)"Consumer<String> printer = s ->System.out.println(s);Consumer<List<String>> clearList = list -> list.clear();
printer.accept("Hello World!");// Prints: Hello World!// Used in: forEach(), Optional.ifPresent()// ========== SUPPLIER: Create/provide something ==========// "Give me a value (I don't need any input)"Supplier<Double> randomNumber =()->Math.random();Supplier<LocalDate> today =()->LocalDate.now();Supplier<List<String>> newList =()->newArrayList<>();System.out.println(randomNumber.get());// 0.73628...System.out.println(today.get());// 2024-01-15// Used in: Optional.orElseGet(), Stream.generate()Two-Parameter Variants (Bi-Interfaces)
When you need to work with two inputs, Java provides "Bi" versions:
// BiPredicate<T, U> - Test with TWO inputsBiPredicate<String,Integer> hasLength =(s, len)-> s.length()== len;System.out.println(hasLength.test("Hello",5));// trueSystem.out.println(hasLength.test("Hi",5));// false// BiFunction<T, U, R> - Transform TWO inputs into outputBiFunction<String,String,String> concat =(a, b)-> a +" "+ b;BiFunction<Integer,Integer,Integer> multiply =(a, b)-> a * b;System.out.println(concat.apply("Hello","World"));// "Hello World"System.out.println(multiply.apply(5,3));// 15// BiConsumer<T, U> - Consume TWO inputsBiConsumer<String,Integer> printNTimes =(s, n)->{for(int i =0; i < n; i++){System.out.println(s);}};
printNTimes.accept("Hello",3);// Prints "Hello" 3 times// PRACTICAL: BiConsumer with Map.forEach()Map<String,Integer> scores =newHashMap<>();
scores.put("Alice",95);
scores.put("Bob",87);
scores.forEach((name, score)->System.out.println(name +" scored "+ score));📌 Note:
There's no BiSupplier because suppliers don't take inputs anyway!
Composing Functions (Chaining)
One powerful feature of functional interfaces is composition – combining simple functions to build complex ones. Think of it like LEGO blocks!
// ===== PREDICATE COMPOSITION =====Predicate<String> notEmpty = s ->!s.isEmpty();Predicate<String> notTooLong = s -> s.length()<20;// Combine with AND, OR, NEGATEPredicate<String> validInput = notEmpty.and(notTooLong);Predicate<String> invalidInput = notEmpty.negate();// IS emptySystem.out.println(validInput.test("Hello"));// true (not empty AND not too long)System.out.println(validInput.test(""));// false (empty)// ===== FUNCTION COMPOSITION =====Function<String,String> trim =String::trim;Function<String,String> toUpper =String::toUpperCase;Function<String,Integer> length =String::length;// andThen: Apply THIS first, THEN the argument// trim → toUpper → lengthFunction<String,Integer> pipeline = trim.andThen(toUpper).andThen(length);System.out.println(pipeline.apply(" hello "));// 5// compose: Apply ARGUMENT first, then THIS// trim first → then lengthFunction<String,Integer> composed = length.compose(trim);System.out.println(composed.apply(" hello "));// 5// ===== CONSUMER COMPOSITION =====Consumer<String> log = s ->System.out.println("LOG: "+ s);Consumer<String> save = s ->saveToDatabase(s);// Chain consumers with andThenConsumer<String> logAndSave = log.andThen(save);
logAndSave.accept("data");// First logs, then saves💡 Composition Methods:
and(),or(),negate()– for PredicatesandThen(),compose()– for FunctionsandThen()– for Consumers
💡 Tips & Best Practices
✅ DO: Use Built-in Interfaces First
Before creating your own functional interface, check if one already exists in java.util.function. It probably does!
✅ DO: Add @FunctionalInterface
Always annotate your custom functional interfaces. It's free documentation and compiler safety.
✅ DO: Use Primitive Specializations
For primitives, use IntPredicate, LongFunction, DoubleConsumer etc. to avoid boxing overhead.
// Avoid boxing (slower)Predicate<Integer> isPositive = n -> n >0;// Use primitive version (faster)IntPredicate isPositiveFast = n -> n >0;❌ DON'T: Create Interfaces for Standard Patterns
Don't create StringValidator when Predicate<String> works perfectly.
💡 TIP: Name Your Lambdas When Complex
For readability, assign complex lambdas to well-named variables:
// Hard to read
stream.filter(p -> p.getAge()>18&& p.getCountry().equals("USA")&& p.isActive());// Easier to readPredicate<Person> isEligibleVoter = p ->
p.getAge()>18&&
p.getCountry().equals("USA")&&
p.isActive();
stream.filter(isEligibleVoter);📝 Quick Summary
Key Concepts:
- Functional Interface = 1 abstract method
- Enables lambda expressions
- Use
@FunctionalInterfaceannotation - Default/static methods don't count
The Big Four:
Predicate– returns booleanFunction– transforms valuesConsumer– performs actionsSupplier– provides values