Functional Interfaces in Java
Learn how functional interfaces enable lambda expressions and functional programming
Think of functional interfaces like recipes with exactly one instruction! A recipe is only useful if it tells you to do one specific thing (like 'bake a cake'). A functional interface is the same - it's an interface with exactly one abstract method. When you use one with a lambda expression, you're basically filling in that one instruction with code!
What is a Functional Interface?
A functional interface is an interface with exactly one abstract method. It's the contract that allows lambda expressions to work. When you create a lambda expression, you're actually implementing that one method! The @FunctionalInterface annotation helps Java verify that your interface is truly functional.
// NOT a functional interface - has 2 abstract methodsinterface Printer { void print(String text); void printError(String error); // Second method - NOT functional!}// IS a functional interface - has exactly 1 abstract method@FunctionalInterfaceinterface SimplePrinter { void print(String text);}// This is functional - default methods don't count!@FunctionalInterfaceinterface AdvancedPrinter { void print(String text); // ONE abstract method default void printWithPrefix(String text) { print("PREFIX: " + text); } default void printWithSuffix(String text) { print(text + " :SUFFIX"); }}// Using the functional interface with lambdaSimplePrinter printer = (text) -> System.out.println(text);printer.print("Hello!"); // Output: Hello!// Using advanced printerAdvancedPrinter advPrinter = (text) -> System.out.println(text);advPrinter.printWithPrefix("World"); // Output: PREFIX: WorldThe @FunctionalInterface Annotation
This annotation tells Java to check that the interface has exactly one abstract method. It's optional but recommended - it helps catch mistakes at compile time!
// Example 1: Valid functional interface@FunctionalInterfaceinterface Calculator { int calculate(int a, int b);}// This compiles fine!Calculator adder = (a, b) -> a + b;System.out.println(adder.calculate(5, 3)); // Output: 8// ============================================// Example 2: Adding a second abstract method@FunctionalInterfaceinterface InvalidCalculator { int add(int a, int b); int subtract(int a, int b); // COMPILER ERROR! // Error: "Invalid '@FunctionalInterface' annotation // - Multiple non-overriding abstract methods found"}// ============================================// Example 3: Without annotation (still functional, but risky)// This IS functional, but easy to break accidentallyinterface Processor { void process(String data);}// Later, someone might add another method:interface Processor { void process(String data); void validate(String data); // Oops! Now it's broken for lambdas}// ============================================// Example 4: Using @FunctionalInterface prevents mistakes@FunctionalInterfaceinterface SafeProcessor { void process(String data);}// If someone tries to add another abstract method:@FunctionalInterfaceinterface SafeProcessor { void process(String data); void validate(String data); // COMPILE ERROR - Good! // The annotation caught the mistake!}// ============================================// TIP: Always use @FunctionalInterface to protect your code!Built-in Functional Interfaces
Java provides many ready-made functional interfaces in the java.util.function package for common operations:
import java.util.function.*;public class BuiltInFunctionalInterfaces { public static void main(String[] args) { // 1. Predicate<T> - T -> boolean // Tests a condition, returns true or false Predicate<Integer> isPositive = n -> n > 0; System.out.println("Is 5 positive? " + isPositive.test(5)); // true System.out.println("Is -3 positive? " + isPositive.test(-3)); // false Predicate<String> isLonger = str -> str.length() > 5; System.out.println("Is 'hello' longer? " + isLonger.test("hello")); // false System.out.println("Is 'hello world' longer? " + isLonger.test("hello world")); // true // ============================================ // 2. Function<T, R> - T -> R // Takes T, transforms it, returns R Function<String, Integer> getLength = str -> str.length(); System.out.println("Length of 'Java': " + getLength.apply("Java")); // 4 Function<Integer, String> toHex = num -> Integer.toHexString(num); System.out.println("15 in hex: " + toHex.apply(15)); // f // ============================================ // 3. Consumer<T> - T -> void // Takes T, does something with it, returns nothing Consumer<String> printer = msg -> System.out.println("Message: " + msg); printer.accept("Hello!"); // Output: Message: Hello! Consumer<Integer> multiplier = num -> { System.out.println(num + " * 2 = " + (num * 2)); System.out.println(num + " * 3 = " + (num * 3)); }; multiplier.accept(4); // ============================================ // 4. Supplier<T> - () -> T // Takes nothing, returns T Supplier<Double> randomNum = () -> Math.random(); System.out.println("Random: " + randomNum.get()); // Different each time Supplier<String> greeting = () -> "Hello, World!"; System.out.println(greeting.get()); // Hello, World! // ============================================ // 5. BiFunction<T, U, R> - (T, U) -> R // Takes 2 inputs (different types), returns R BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; System.out.println("3 * 4 = " + multiply.apply(3, 4)); // 12 BiFunction<String, String, String> combine = (s1, s2) -> s1 + " " + s2; System.out.println(combine.apply("Hello", "World")); // Hello World // ============================================ // 6. UnaryOperator<T> - T -> T // Takes T, returns the same type T (special Function) UnaryOperator<Integer> square = n -> n * n; System.out.println("Square of 5: " + square.apply(5)); // 25 UnaryOperator<String> makeUpper = str -> str.toUpperCase(); System.out.println("java -> " + makeUpper.apply("java")); // JAVA // ============================================ // 7. BinaryOperator<T> - (T, T) -> T // Takes 2 of same type T, returns T (special BiFunction) BinaryOperator<Integer> max = (a, b) -> a > b ? a : b; System.out.println("Max of 10 and 20: " + max.apply(10, 20)); // 20 BinaryOperator<String> longer = (s1, s2) -> s1.length() > s2.length() ? s1 : s2; System.out.println("Longer: " + longer.apply("cat", "elephant")); // elephant }}// Quick Reference:// Predicate<T> : T -> boolean (Test/condition)// Function<T, R> : T -> R (Transform)// Consumer<T> : T -> void (Do something)// Supplier<T> : () -> T (Provide/generate)// BiFunction<T,U,R> : (T, U) -> R (Transform with 2 inputs)// UnaryOperator<T> : T -> T (Transform same type)// BinaryOperator<T> : (T, T) -> T (Combine 2 of same type)Creating Custom Functional Interfaces
You can create your own functional interfaces for specific needs in your application:
// Custom functional interfaces for your specific domain// 1. Validator - checks if something is valid@FunctionalInterfaceinterface Validator<T> { boolean isValid(T data);}// 2. Transformer - converts one type to another@FunctionalInterfaceinterface Transformer<T, R> { R transform(T input);}// 3. Logger - logs messages with different levels@FunctionalInterfaceinterface Logger { void log(String message);}// 4. EventHandler - handles events@FunctionalInterfaceinterface EventHandler<T> { void handleEvent(T event);}// 5. Filter - filters items from a list@FunctionalInterfaceinterface Filter<T> { boolean matches(T item);}// ============================================// Using custom functional interfaces// ============================================public class CustomFunctionalInterfaceDemo { public static void main(String[] args) { // Example 1: Email Validator Validator<String> emailValidator = email -> email.contains("@") && email.contains("."); System.out.println("Is 'user@email.com' valid? " + emailValidator.isValid("user@email.com")); // true System.out.println("Is 'invalidemail' valid? " + emailValidator.isValid("invalidemail")); // false // ============================================ // Example 2: Password Strength Validator Validator<String> strongPassword = pass -> pass.length() >= 8 && pass.matches(".*[A-Z].*") && pass.matches(".*[0-9].*"); System.out.println("Is 'pass123' strong? " + strongPassword.isValid("pass123")); // false System.out.println("Is 'Pass12345' strong? " + strongPassword.isValid("Pass12345")); // true // ============================================ // Example 3: Custom Transformer Transformer<String, Integer> stringToInt = str -> Integer.parseInt(str); Transformer<Integer, String> intToHex = num -> "0x" + Integer.toHexString(num); int result = stringToInt.transform("42"); System.out.println("String '42' as int: " + result); // 42 System.out.println("Integer 42 as hex: " + intToHex.transform(42)); // 0x2a // ============================================ // Example 4: Logger Implementations Logger consoleLogger = msg -> System.out.println("[LOG] " + msg); Logger errorLogger = msg -> System.err.println("[ERROR] " + msg); consoleLogger.log("Application started"); // [LOG] Application started errorLogger.log("Something went wrong!"); // [ERROR] Something went wrong! // ============================================ // Example 5: Event Handler EventHandler<String> messageHandler = msg -> System.out.println("Event received: " + msg); messageHandler.handleEvent("User clicked button"); // Event received: User clicked button messageHandler.handleEvent("File uploaded"); // Event received: File uploaded // ============================================ // Example 6: Filter for collections Filter<Integer> evenNumbers = n -> n % 2 == 0; Filter<String> longStrings = str -> str.length() > 5; System.out.println("Is 4 even? " + evenNumbers.matches(4)); // true System.out.println("Is 'hello' long? " + longStrings.matches("hello")); // false System.out.println("Is 'hello world' long? " + longStrings.matches("hello world")); // true }}// Remember: Only create custom functional interfaces when:// 1. Built-in ones don't fit your needs// 2. Your interface has a specific domain meaning// 3. It makes your code more readable and maintainableUsing Functional Interfaces with Lambdas
Functional interfaces are the bridge between traditional interfaces and lambda expressions:
import java.util.*;import java.util.function.*;public class FunctionalInterfaceWithLambda { public static void main(String[] args) { // ============================================ // The Key Insight: // Lambda expression = Implementation of functional interface's ONE method // ============================================ // Traditional way (before Java 8) Predicate<Integer> isEvenOld = new Predicate<Integer>() { @Override public boolean test(Integer num) { return num % 2 == 0; } }; System.out.println("Is 4 even (old)? " + isEvenOld.test(4)); // true // Modern way with lambda (Java 8+) Predicate<Integer> isEven = n -> n % 2 == 0; System.out.println("Is 4 even (new)? " + isEven.test(4)); // true // ============================================ // Example: Filtering with Predicate List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Predicate<Integer> greaterThan5 = n -> n > 5; List<Integer> filtered = new ArrayList<>(); for (Integer num : numbers) { if (greaterThan5.test(num)) { filtered.add(num); } } System.out.println("Numbers > 5: " + filtered); // [6, 7, 8, 9, 10] // ============================================ // Example: Transforming with Function List<String> words = Arrays.asList("hello", "world", "java"); Function<String, Integer> getLength = str -> str.length(); Function<String, String> uppercase = str -> str.toUpperCase(); for (String word : words) { System.out.println(word + " -> length: " + getLength.apply(word)); System.out.println(word + " -> uppercase: " + uppercase.apply(word)); } // ============================================ // Example: Processing with Consumer List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Consumer<String> printWithPrefix = name -> System.out.println("User: " + name); Consumer<String> printWithDetails = name -> { System.out.println("Name: " + name); System.out.println("Length: " + name.length()); System.out.println("---"); }; System.out.println("Simple output:"); names.forEach(printWithPrefix); System.out.println("\nDetailed output:"); names.forEach(printWithDetails); // ============================================ // Example: Providing values with Supplier Supplier<Long> timeSupplier = () -> System.currentTimeMillis(); Supplier<UUID> uuidSupplier = () -> UUID.randomUUID(); System.out.println("Current time: " + timeSupplier.get()); System.out.println("New UUID: " + uuidSupplier.get()); // ============================================ // Example: Combining with BiFunction BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; BiFunction<String, String, String> concat = (s1, s2) -> s1 + s2; System.out.println("5 + 3 = " + add.apply(5, 3)); // 8 System.out.println("5 * 3 = " + multiply.apply(5, 3)); // 15 System.out.println("Hello + World = " + concat.apply("Hello", "World")); // HelloWorld // ============================================ // Helper method using functional interface List<Integer> evens = filterList(numbers, n -> n % 2 == 0); List<Integer> odds = filterList(numbers, n -> n % 2 != 0); System.out.println("Even numbers: " + evens); // [2, 4, 6, 8, 10] System.out.println("Odd numbers: " + odds); // [1, 3, 5, 7, 9] } // Method that accepts a functional interface static <T> List<T> filterList(List<T> list, Predicate<T> predicate) { List<T> result = new ArrayList<>(); for (T item : list) { if (predicate.test(item)) { result.add(item); } } return result; }}Advanced Functional Interfaces
Java provides specialized functional interfaces for different scenarios like two-parameter operations and transformations:
import java.util.function.*;public class AdvancedFunctionalInterfaces { public static void main(String[] args) { // ============================================ // BiFunction - Takes 2 different parameters, returns result // ============================================ BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; BiFunction<Integer, Integer, Integer> subtract = (a, b) -> a - b; BiFunction<String, Integer, String> repeat = (str, times) -> { StringBuilder result = new StringBuilder(); for (int i = 0; i < times; i++) { result.append(str); } return result.toString(); }; System.out.println("5 + 3 = " + add.apply(5, 3)); // 8 System.out.println("10 - 4 = " + subtract.apply(10, 4)); // 6 System.out.println("'Hi' * 3 = " + repeat.apply("Hi", 3)); // HiHiHi // ============================================ // UnaryOperator - Takes 1 parameter, returns same type // ============================================ UnaryOperator<Integer> square = n -> n * n; UnaryOperator<String> reverse = str -> new StringBuilder(str).reverse().toString(); UnaryOperator<Integer> negate = n -> -n; System.out.println("Square of 7: " + square.apply(7)); // 49 System.out.println("Reverse 'hello': " + reverse.apply("hello")); // olleh System.out.println("Negate 10: " + negate.apply(10)); // -10 // ============================================ // BinaryOperator - Takes 2 of SAME type, returns same type // ============================================ BinaryOperator<Integer> max = (a, b) -> a > b ? a : b; BinaryOperator<Integer> min = (a, b) -> a < b ? a : b; BinaryOperator<String> longer = (s1, s2) -> s1.length() > s2.length() ? s1 : s2; System.out.println("Max of 10 and 20: " + max.apply(10, 20)); // 20 System.out.println("Min of 10 and 20: " + min.apply(10, 20)); // 10 System.out.println("Longer string: " + longer.apply("cat", "elephant")); // elephant // ============================================ // BiPredicate - Takes 2 parameters, returns boolean // ============================================ BiPredicate<Integer, Integer> isSum10 = (a, b) -> a + b == 10; BiPredicate<String, String> sameLength = (s1, s2) -> s1.length() == s2.length(); BiPredicate<Double, Double> almostEqual = (a, b) -> Math.abs(a - b) < 0.01; System.out.println("Is 3 + 7 = 10? " + isSum10.test(3, 7)); // true System.out.println("Is 5 + 6 = 10? " + isSum10.test(5, 6)); // false System.out.println("'hi' same length as 'go'? " + sameLength.test("hi", "go")); // true System.out.println("3.14159 ~= 3.14? " + almostEqual.test(3.14159, 3.14)); // true // ============================================ // BiConsumer - Takes 2 parameters, returns nothing // ============================================ BiConsumer<String, Integer> printNTimes = (str, n) -> { for (int i = 0; i < n; i++) { System.out.println(i + ": " + str); } }; BiConsumer<String, String> printKeyValue = (key, value) -> System.out.println(key + " = " + value); System.out.println("Print 'Hello' 2 times:"); printNTimes.accept("Hello", 2); System.out.println("\nKey-Value pairs:"); printKeyValue.accept("language", "Java"); printKeyValue.accept("version", "21"); // ============================================ // Function composition - Chaining transformations // ============================================ Function<Integer, Integer> addOne = n -> n + 1; Function<Integer, Integer> multiplyTwo = n -> n * 2; Function<Integer, Integer> square2 = n -> n * n; // Using compose: first apply addOne, then multiplyTwo Function<Integer, Integer> addThenMultiply = multiplyTwo.compose(addOne); System.out.println("(5 + 1) * 2 = " + addThenMultiply.apply(5)); // 12 // Using andThen: first apply addOne, then multiplyTwo (reverse order) Function<Integer, Integer> addThenMultiply2 = addOne.andThen(multiplyTwo); System.out.println("(5 + 1) * 2 = " + addThenMultiply2.apply(5)); // 12 // Multiple compositions Function<Integer, Integer> chain = addOne.andThen(multiplyTwo).andThen(square2); System.out.println("((5 + 1) * 2)^2 = " + chain.apply(5)); // 144 // ============================================ // Predicate composition - Combining conditions // ============================================ Predicate<Integer> isPositive = n -> n > 0; Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isSmall = n -> n < 100; // Combining predicates with logical operations Predicate<Integer> isPositiveEven = isPositive.and(isEven); Predicate<Integer> isEvenOrSmall = isEven.or(isSmall); Predicate<Integer> notPositive = isPositive.negate(); System.out.println("Is 4 positive and even? " + isPositiveEven.test(4)); // true System.out.println("Is 4 even or small? " + isEvenOrSmall.test(4)); // true System.out.println("Is -5 not positive? " + notPositive.test(-5)); // true }}// Comparison Table:// Function<T,R> : T -> R (1 input, possibly different output type)// UnaryOperator<T> : T -> T (1 input, same output type)// BiFunction<T,U,R> : (T, U) -> R (2 different inputs, different output)// BinaryOperator<T> : (T, T) -> T (2 same inputs, same output)// Predicate<T> : T -> boolean (1 input, test condition)// BiPredicate<T,U> : (T, U) -> boolean (2 inputs, test condition)// Consumer<T> : T -> void (1 input, do something)// BiConsumer<T,U> : (T, U) -> void (2 inputs, do something)Real-World Examples
Let's see how functional interfaces solve practical programming problems:
import java.util.*;import java.util.function.*;import java.util.stream.*;public class RealWorldFunctionalInterfaces { static class User { String name; int age; String email; User(String name, int age, String email) { this.name = name; this.age = age; this.email = email; } @Override public String toString() { return name + " (" + age + ", " + email + ")"; } } public static void main(String[] args) { List<User> users = Arrays.asList( new User("Alice", 25, "alice@example.com"), new User("Bob", 30, "bob@example.com"), new User("Charlie", 22, "charlie@example.com"), new User("David", 35, "david@example.com"), new User("Eve", 28, "eve@example.com") ); // ============================================ // Example 1: Filtering users with Predicate // ============================================ System.out.println("=== Example 1: Filtering Users ==="); Predicate<User> isAdult = user -> user.age >= 18; Predicate<User> isOldEnough = user -> user.age > 25; Predicate<User> hasGmailAccount = user -> user.email.endsWith("@gmail.com"); List<User> adults = users.stream() .filter(isAdult) .collect(Collectors.toList()); System.out.println("Adults: " + adults); List<User> older = users.stream() .filter(isOldEnough) .collect(Collectors.toList()); System.out.println("Older than 25: " + older); // ============================================ // Example 2: Transforming users with Function // ============================================ System.out.println("\n=== Example 2: Transforming Data ==="); Function<User, String> getUserName = user -> user.name; Function<User, Integer> getUserAge = user -> user.age; Function<User, String> getUserEmail = user -> user.email; List<String> names = users.stream() .map(getUserName) .collect(Collectors.toList()); System.out.println("Names: " + names); Integer totalAge = users.stream() .map(getUserAge) .reduce(0, Integer::sum); System.out.println("Total age: " + totalAge); // ============================================ // Example 3: Processing with Consumer // ============================================ System.out.println("\n=== Example 3: Processing Users ==="); Consumer<User> sendWelcomeEmail = user -> System.out.println("Sending email to " + user.email); Consumer<User> printUserInfo = user -> System.out.println(" - " + user.name + " is " + user.age + " years old"); System.out.println("Sending welcome emails to all users:"); users.forEach(sendWelcomeEmail); System.out.println("\nUser information:"); users.forEach(printUserInfo); // ============================================ // Example 4: Creating default values with Supplier // ============================================ System.out.println("\n=== Example 4: Supplying Default Values ==="); Supplier<User> guestUser = () -> new User("Guest", 0, "guest@example.com"); Supplier<List<User>> emptyUserList = ArrayList::new; User guest = guestUser.get(); System.out.println("Default guest: " + guest); List<User> newList = emptyUserList.get(); System.out.println("New list created: " + newList); // ============================================ // Example 5: Combining with BiFunction (age check) // ============================================ System.out.println("\n=== Example 5: Combining Operations ==="); BiFunction<User, Integer, Boolean> canRegister = (user, minAge) -> user.age >= minAge; BiConsumer<String, String> sendNotification = (email, message) -> System.out.println("To: " + email + " | Message: " + message); int minimumAge = 21; users.forEach(user -> { if (canRegister.apply(user, minimumAge)) { System.out.println(user.name + " can register"); sendNotification.accept(user.email, "Welcome! You're registered."); } else { System.out.println(user.name + " cannot register (too young)"); } }); // ============================================ // Example 6: Complex filtering with composed predicates // ============================================ System.out.println("\n=== Example 6: Composed Predicates ==="); Predicate<User> isYoungAdult = user -> user.age >= 18 && user.age <= 30; Predicate<User> hasValidEmail = user -> user.email.contains("@"); Predicate<User> isValidUser = isYoungAdult.and(hasValidEmail); List<User> validYoungAdults = users.stream() .filter(isValidUser) .collect(Collectors.toList()); System.out.println("Valid young adults: " + validYoungAdults); // ============================================ // Example 7: Function chaining // ============================================ System.out.println("\n=== Example 7: Function Chaining ==="); Function<String, String> toUpperCase = String::toUpperCase; Function<String, String> addPrefix = s -> "USER: " + s; Function<String, String> processName = toUpperCase.andThen(addPrefix); System.out.println("Processing names:"); users.stream() .map(User::toString) .map(processName) .forEach(System.out::println); // ============================================ // Example 8: Custom validation // ============================================ System.out.println("\n=== Example 8: Email Validation ==="); Predicate<String> isValidEmail = email -> email.matches("[A-Za-z0-9+_.-]+@(.+)$"); users.stream() .filter(user -> isValidEmail.test(user.email)) .map(User::toString) .forEach(user -> System.out.println("Valid: " + user)); }}Key Concepts
Single Abstract Method
A functional interface has exactly one abstract method. Multiple default methods are allowed, but only one method that needs to be implemented.
@FunctionalInterface Annotation
This optional annotation marks an interface as functional and makes the compiler check that it has exactly one abstract method.
Lambda-Compatible
Because functional interfaces have exactly one method, they're perfect for lambda expressions - the lambda becomes that one method's implementation!
Type Safety
Functional interfaces provide type-safe functional programming - the compiler knows what the lambda should do.
Standard Library Support
Java provides many built-in functional interfaces (Predicate, Function, Consumer, Supplier, BiFunction) covering common patterns.
Best Practices
- ✓Always use @FunctionalInterface annotation when creating functional interfaces
- ✓Keep functional interfaces simple - one clear, well-named method
- ✓Use built-in functional interfaces from java.util.function when possible
- ✓Prefer built-in interfaces to custom ones for common operations
- ✓Document what your functional interface does clearly
- ✓Use appropriate naming conventions (predicate, consumer, supplier, function)
- ✓Combine functional interfaces with streams and lambda expressions for powerful code
Common Mistakes
✗ Creating a functional interface with more than one abstract method
Why it's wrong: If you have multiple abstract methods, it's not functional! Only lambda expressions can't implement it.
✗ Forgetting that default methods don't count towards the abstract method limit
Why it's wrong: You can have multiple default methods and still be functional - only abstract methods matter!
✗ Not using @FunctionalInterface annotation
Why it's wrong: Without it, you might accidentally add a second abstract method later and break compatibility.
✗ Reinventing the wheel with custom functional interfaces
Why it's wrong: Java already provides Function, Predicate, Consumer, Supplier - use them instead of creating custom ones!
✗ Using complex logic in lambda expressions
Why it's wrong: Lambda expressions should be simple! If your functional interface implementation is complex, use an explicit method.
Interview Tips
- 💡Explain that a functional interface has exactly one abstract method
- 💡Know common built-in functional interfaces and when to use each
- 💡Understand the @FunctionalInterface annotation and its purpose
- 💡Be able to distinguish between default methods and abstract methods
- 💡Explain how functional interfaces enable lambda expressions
- 💡Know the difference between Function, Predicate, Consumer, and Supplier
- 💡Be ready to create custom functional interfaces when needed
- 💡Understand BiFunction, UnaryOperator, and BinaryOperator use cases