Exploring java.util.function Functional Interfaces in Java
Java has evolved significantly since its inception, and one of the most impactful changes came with the introduction of functional programming concepts in Java 8. At the heart of this paradigm shift lies the java.util.function
package, which provides a variety of functional interfaces for writing concise, expressive, and reusable code.
What Is java.util.function
?
The java.util.function
package comprises a set of interfaces designed for functional programming. These interfaces represent common operations such as transformation, filtering, and mapping. Most of them are single-method interfaces annotated with the @FunctionalInterface
annotation, which ensures that they can be implemented using lambda expressions. This feature streamlines coding by reducing boilerplate code and improving readability.
Here’s the formatted version of your content to keep it clean and organized:
Key Functional Interfaces
Here are the most commonly used functional interfaces in java.util.function
:
1. Predicate<T>:
- Represents a condition or filter.
- Method:
boolean test(T t)
- Example:
- Define a Predicate:
static Predicate<Integer> condition = x -> x % 2 == 0;
The Predicate
condition takes an integer to operate on.
- Using the Predicate in the filterList method:
public static List<Integer> filterList(List<Integer> numbers, Predicate<Integer> condition) {
return numbers.stream().filter(condition).toList();
}
The filterList
method uses Java Streams to filter elements based on the provided Predicate
. The .filter(condition)
applies the condition to each element in the stream.
- Execution in the main method:
public static void main(String args[]) {
List<Integer> numbers = Arrays.asList(1, 3, 4, 5, 6);
System.out.println(filterList(numbers, condition)); // Outputs: [4, 6]
}
2. Function<T, R>:
- Represents a transformation from type
T
to typeR
. - Method:
R apply(T t)
- Example:
Function<Integer, String> intToString = num -> "Number: " + num;
System.out.println(intToString.apply(5)); // "Number: 5"
3. Consumer<T>:
- Represents an operation that consumes an object without returning a result.
- Method:
void accept(T t)
- Example 1:
Consumer<String> print = System.out::println;
print.accept("Hello, World!"); // Prints: Hello, World!
- Example 2:
- Define a Consumer:
static Consumer<Integer> consumer = x -> {
System.out.print((x * x) + " ");
};
- Pass the consumer in the consumeFromList method to consume
public static void consumeFromList(List<Integer> numbers, Consumer<Integer> consumer) {
numbers.forEach(consumer); //output: 4,6
}
- Execution in the main method:
public static void main(String args[]){
List<Integer> numbers = Arrays.asList(1,3,4,5,6);
consumeFromList(numbers,consumer); //output: 1 9 16 25 36
}
4. Supplier<T>:
- Represents a supplier of objects, often used for lazy evaluation.
- Method:
T get()
- Example:
Supplier<Double> randomValue = Math::random; System.out.println(randomValue.get()); // Generates a random number.
5. BiFunction<T, U, R>:
- Represents a function that takes two arguments and produces a result.
- Method:
R apply(T t, U u)
- Example:
BiFunction<Integer, Integer, String> sumToString = (a, b) -> "Sum: " + (a + b); System.out.println(sumToString.apply(3, 4)); // "Sum: 7"
6. UnaryOperator<T> and BinaryOperator<T>:
UnaryOperator<T>:
- A functional interface that operates on a single operand and produces a result of the same type.
-
It’s a specialization of the
Function
interface where both input and output types are the same. - Example:
UnaryOperator<Integer> square = x -> x * x; System.out.println(square.apply(4)); // Outputs: 16
BinaryOperator<T>:
- A functional interface that takes two operands of the same type and produces a result of the same type.
-
It’s a specialization of the
BiFunction
interface where all types (input and output) are the same. - Example:
BinaryOperator<Integer> sum = (x, y) -> x + y; System.out.println(sum.apply(3, 5)); // Outputs: 8
Currying
Currying
is a functional programming concept where a function with multiple arguments is transformed into a sequence of functions, each taking a single argument. Instead of passing all arguments at once, we can pass them one at a time.
This approach allows us to create more specialized functions from a general one by “fixing” some of the arguments.
- Let’s define a function curriedSum that starts with an argument x, then returns another function for y, and finally another function for z. The result of all these functions is the sum of x + y + z;
Function<Integer, Function<Integer, Function<Integer, Integer>>> curriedSum = x -> y -> z -> x + y + z;
- Let’s execute the function by passing arguments one by one using apply()
int result = curriedSum.apply(1).apply(2).apply(3);
System.out.println(result); // Output: 6
Using andThen:
The andThen
method of functional interfaces allows us to compose two functions such that the output of the first function becomes the input of the second function.
Let’s compose two functions to convert a list of integers to their squares and then sum the squared values.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Define the square function
Function<Integer, Integer> square = x -> x * x;
// Create a function to sum the squares of the list
Function<Void, Integer> sumSquares = unused ->
numbers.stream().map(square).reduce(0, Integer::sum);
// Use andThen to format the result as a string
Function<Void, String> resultAsString = sumSquares.andThen(sum -> "The sum of squares is: " + sum);
// Calculate the result
String result = resultAsString.apply(null);
System.out.println(result); // Output: "The sum of squares is: 55"
Explation:
:
square
: Computes the square of each number.sumSquares
: Sums up the squares of the numbers in the list.andThen
: Chains an additional operation, converting the integer result into a formatted string.- Execution: The combined function first computes the sum of squares, then formats the result as a string.
Using compose:
compose is a method used to combine two functions into a single function. It allows us to execute functions in sequence, where the output of one function becomes the input to the next. The order of execution in is right-to-left, meaning the second function is executed first, and its result is passed to the first function
// First function: Multiply the number by 2
Function<Integer, Integer> multiplyBy2 = x -> x * 2;
// Second function: Add 3 to the number
Function<Integer, Integer> add3 = x -> x + 3;
// Composed function: Add 3 first, then multiply by 2
Function<Integer, Integer> composedFunction = multiplyBy2.compose(add3);
// Apply the composed function
int result = composedFunction.apply(5); // (5 + 3) * 2 = 16
System.out.println(result); // Output: 16
Explanation:
-
add3
: This is executed first, adding3
to the input (5 + 3 = 8
). -
multiplyBy2
: This takes the result ofadd3
and multiplies it by2
(8 * 2 = 16
). -
compose
: Combines these two functions into one cohesive operation.
Conclusion
The java.util.function
package has made functional programming accessible to Java developers, revolutionizing the way we write and reason about code. By mastering its interfaces, you can unlock new levels of expressiveness and efficiency in your applications.
Most of the functional interfaces in this package are higher-order functions, meaning they take or return other functions, enabling powerful composition and modularity. Interfaces like Supplier
and Consumer
are exceptions, as they focus on producing or consuming values directly without involving other functions.
Author: Mohammad J Iqbal