
Now that you understand lambda expressions, let’s explore functional interfaces, which are essential for using lambdas effectively.
1. What is a Functional Interface?
A functional interface is an interface that contains exactly one abstract method. It may have multiple default or static methods, but only one abstract method is required for it to be considered functional.
Why Functional Interfaces?
- They enable the use of lambda expressions.
- They make code concise and easier to read.
- They support functional programming in Java.
2. @FunctionalInterface
Annotation
Java provides the @FunctionalInterface
annotation to ensure that an interface meets the functional interface rules. If an interface with this annotation has more than one abstract method, the compiler will throw an error.
Example of a Functional Interface
@FunctionalInterface
interface Greeting {
void sayHello(); // Single abstract method
}
✅ Since there is only one abstract method, this is a valid functional interface.
3. Using Functional Interfaces with Lambda Expressions
A functional interface can be implemented using lambda expressions, making the code cleaner and more readable.
Example: Traditional Implementation (Before Java 8)
public class Main {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
@Override
public void sayHello() {
System.out.println("Hello, Functional Interface!");
}
};
greeting.sayHello();
}
}
Example: Using Lambda Expression (Java 8+)
public class Main {
public static void main(String[] args) {
Greeting greeting = () -> System.out.println("Hello, Lambda!");
greeting.sayHello();
}
}
✅ Benefits of using lambda expressions:
- Less code – No need for an anonymous class.
- Easier to read – Simple and clear syntax.
4. Built-in Functional Interfaces in Java (java.util.function
Package)
Java provides several pre-defined functional interfaces in the java.util.function
package. Here are some commonly used ones:
Functional Interface | Method | Description | Example |
---|---|---|---|
Predicate<T> | boolean test(T t) | Tests if a condition is met | Predicate<Integer> isEven = (n) -> n % 2 == 0; |
Function<T, R> | R apply(T t) | Transforms a value | Function<String, Integer> length = str -> str.length(); |
Consumer<T> | void accept(T t) | Performs an action | Consumer<String> print = str -> System.out.println(str); |
Supplier<T> | T get() | Provides a value | Supplier<Double> random = () -> Math.random(); |
5. Example Using a Built-in Functional Interface
Using Predicate<T>
to Check if a Number is Even
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isEven = (n) -> n % 2 == 0;
System.out.println(isEven.test(10)); // true
System.out.println(isEven.test(7)); // false
}
}
✅ The Predicate<Integer>
checks if a number is even, returning true
or false
.
Lesson Reflection
Think about the following questions:
- Why do you think Java introduced functional interfaces when it already had regular interfaces?
- How does using lambda expressions with functional interfaces improve code readability and maintainability?
- Can you think of real-world applications where built-in functional interfaces like
Predicate
,Function
, orConsumer
would be useful?
Examples for functional interfaces
Let’s go through examples for each of the remaining built-in functional interfaces from java.util.function
.
1. Function<T, R>
– Transforming a Value
The Function<T, R>
interface takes an input of type T
and returns a value of type R
.
Example: Convert a String to Its Length
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> getLength = str -> str.length();
System.out.println(getLength.apply("Java")); // Output: 4
System.out.println(getLength.apply("Functional Interface")); // Output: 21
}
}
✅ Here, Function<String, Integer>
takes a String and returns an Integer (its length).
2. Consumer<T>
– Performing an Action (No Return Value)
The Consumer<T>
interface accepts a value but does not return anything. It’s useful for operations like printing, logging, or modifying values.
Example: Print a List of Names
import java.util.function.Consumer;
import java.util.List;
public class Main {
public static void main(String[] args) {
Consumer<String> printName = name -> System.out.println("Hello, " + name + "!");
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(printName);
}
}
✅ Here, Consumer<String>
takes a String and prints a greeting message without returning anything.
3. Supplier<T>
– Providing a Value (No Input Required)
The Supplier<T>
interface does not take any arguments, but returns a value of type T
. It’s useful for generating values like random numbers, timestamps, or database connections.
Example: Generate a Random Number
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
Supplier<Double> randomNumber = () -> Math.random();
System.out.println(randomNumber.get()); // Output: (random number)
System.out.println(randomNumber.get()); // Output: (random number)
}
}
✅ Here, Supplier<Double>
generates a random number whenever get()
is called.
Below are the answers to the reflection questions:
1. Which of these functional interfaces do you find most useful? Why?
It depends on the use case, but Function<T, R>
and Predicate<T>
are often the most useful in real-world applications.
Function<T, R>
is valuable when transforming data, such as converting user input, processing API responses, or performing mathematical calculations.Predicate<T>
is essential for filtering data, such as checking if a user is authorized, validating input fields, or filtering a list of products based on price.Consumer<T>
is useful for performing actions like logging or printing results.Supplier<T>
is great when delayed execution is needed, such as fetching configuration values or generating unique IDs.
If I had to pick just one, Function<T, R>
would be the most versatile because it allows for data transformation, which is a common operation in programming.
2. Can you think of a scenario where a Supplier<T>
might be useful besides generating random numbers?
Yes! Here are a few real-world use cases for Supplier<T>
:
Lazy Initialization of Expensive Resources
- Suppose we have a database connection that is expensive to create. Instead of creating it immediately, we use
Supplier<T>
to create it only when needed.
Supplier<Connection> dbConnection = () -> {
System.out.println("Connecting to the database...");
return DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
};
Fetching System Properties or Configuration Values
- Imagine an application that needs a value from a configuration file. We can use a
Supplier<String>
to fetch it on demand.
Supplier<String> configValue = () -> System.getProperty("user.home");
System.out.println("User Home Directory: " + configValue.get());
This ensures the value is not fetched until actually needed, improving efficiency.
Generating Unique Identifiers
- We can use a
Supplier<UUID>
to generate unique identifiers for user sessions or database records.
Supplier<UUID> uniqueIdSupplier = UUID::randomUUID;
System.out.println("Generated ID: " + uniqueIdSupplier.get());
Every time we call uniqueIdSupplier.get()
, a new UUID is created.
Caching Expensive Computations
- Suppose an operation (e.g., loading a large file) is expensive, and we want to defer it until needed.
Supplier<String> expensiveComputation = () -> {
System.out.println("Performing expensive operation...");
return "Computed Result";
};
The computation only runs when expensiveComputation.get()
is called, avoiding unnecessary execution.
next concept: Streams API