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.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// NOT a functional interface - has 2 abstract methods
interface Printer {
void print(String text);
void printError(String error); // Second method - NOT functional!
}
// IS a functional interface - has exactly 1 abstract method
@FunctionalInterface
interface SimplePrinter {
void print(String text);
}
// This is functional - default methods don't count!
@FunctionalInterface
interface 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 lambda
SimplePrinter printer = (text) -> System.out.println(text);
printer.print("Hello!"); // Output: Hello!
// Using advanced printer
AdvancedPrinter advPrinter = (text) -> System.out.println(text);
advPrinter.printWithPrefix("World"); // Output: PREFIX: World

The @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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Example 1: Valid functional interface
@FunctionalInterface
interface 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
@FunctionalInterface
interface 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 accidentally
interface 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
@FunctionalInterface
interface SafeProcessor {
void process(String data);
}
// If someone tries to add another abstract method:
@FunctionalInterface
interface 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Custom functional interfaces for your specific domain
// 1. Validator - checks if something is valid
@FunctionalInterface
interface Validator<T> {
boolean isValid(T data);
}
// 2. Transformer - converts one type to another
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input);
}
// 3. Logger - logs messages with different levels
@FunctionalInterface
interface Logger {
void log(String message);
}
// 4. EventHandler - handles events
@FunctionalInterface
interface EventHandler<T> {
void handleEvent(T event);
}
// 5. Filter - filters items from a list
@FunctionalInterface
interface 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 maintainable

Using Functional Interfaces with Lambdas

Functional interfaces are the bridge between traditional interfaces and lambda expressions:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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