Overview
The Function interface in Java is primarily used for its map operation on a Java stream. In the last post, we saw about predicate chaining in Java. In this post, we will learn about Function chaining in Java.
Function interface in Java
A Function is a functional interface (has a single abstract method called accept) that accepts one argument and produces a result.
Example: We can create a stream of integers, map each integer element to double (2x) of its value, and collect the result as a list.
List<Integer> integers = List.of(1, 2, 3, 4, 5); Function<Integer, Integer> doubleFunction = i -> i * 2; System.out.println(integers.stream() .map(doubleFunction) .collect(Collectors.toList())); //[2, 4, 6, 8, 10]
System.out.println(integers.stream() .map(i -> i * 2) .collect(Collectors.toList())); //[2, 4, 6, 8, 10]
To square each element, we can define a different function and use it.
Function<Integer, Integer> square = i -> i * i; System.out.println(integers.stream() .map(square) .collect(Collectors.toList())); //[1, 4, 9, 16, 25]
Applying multiple functions
What if we want to apply both square and double functions to an element one after another?
Easier/Obvious options – We could either:
- Create a new function to do it.
- Use the previously created functions (doubleFunction and square) by chaining two Function#map calls
Creating a new function to square and double
We can create a new function (lambda expression) which maps an element by first squaring it and then doubling the previous result.
System.out.println(integers.stream() .map(i -> (i * i) * 2) .collect(Collectors.toList())); //[2, 8, 18, 32, 50]
Multiple map calls (Chaining map calls)
We could apply the map multiple times (chaining) as shown below. The first map squares the integer and the second map doubles the result of the previous map result.
Function<Integer, Integer> doubleFunction = i -> i * 2; Function<Integer, Integer> square = i -> i * i; System.out.println(integers.stream() .map(square) .map(doubleFunction) .collect(Collectors.toList())); //[2, 8, 18, 32, 50]
Can we do better (a more idiomatic approach)?
Function andThen – To chain a function at the end
Method Signature:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
Function#andThen example
Let us compose a function using the square and the double functions we have to build a function instance which first squares an integer and then doubles it.
Function<Integer, Integer> squareAndDouble = square.andThen(doubleFunction); System.out.println(integers.stream() .map(squareAndDouble) .collect(Collectors.toList())); // [2, 8, 18, 32, 50]
square.andThen(doubleFunction)
returns a new function which first applies the square function to the input and then applies double function to the previous output.
Similarly, we can compose the double function and the square function as (first double and then square),
Function<Integer, Integer> doubleAndSquare = doubleFunction.andThen(square); System.out.println(integers.stream() .map(doubleAndSquare) .collect(Collectors.toList())); //[4, 16, 36, 64, 100]
Function compose – To chain a function at the beginning
Method signature:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
Function#compose example
To apply square function followed by double function, we use the compose() method as shown below.
Function<Integer, Integer> squareAndDouble = doubleFunction.compose(square); System.out.println(integers.stream() .map(squareAndDouble) .collect(Collectors.toList())); //[2, 8, 18, 32, 50]
To apply it in the reverse order,
Function<Integer, Integer> doubleAndSquare = square.compose(doubleFunction); System.out.println(integers.stream() .map(doubleAndSquare) .collect(Collectors.toList())); //[4, 16, 36, 64, 100]
From this, we can infer that A.andThen(B) is the same as B.compose(A).
Chaining to a BiFunction
A BiFunction is a function, but it accepts two arguments and returns a result.
Example: We create a multiplication function which takes in two integers and returns the product of them.
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; System.out.println(multiply.apply(2, 3)); //6
The BiFunction also has an andThen() method with the below signature.
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after)
Example of BiFunction composition
With the multiply BiFunction we have, let us use the square function, i.e., we will take in two integers, multiply them and square the result.
Function<Integer, Integer> square = i -> i * i; BiFunction<Integer, Integer, Integer> multiplyAndSquare = multiply.andThen(square); System.out.println(multiplyAndSquare.apply(2, 3)); //36
It allows only Functions to be chained. This is because the result of applying a BiFunction will result in a single value. We cannot chain another BiFunction to it, as a BiFunction requires two arguments.
Creating a transformation chain
Let me finish this post by showing how we can create a series of transformation which involves changing the type of the value (result). Let us convert the previous result (of multiplying and squaring) to a string.
BiFunction<Integer, Integer, String> multiplyAndSquareWithResultAsString = multiplyAndSquare .andThen(String::valueOf); String result = multiplyAndSquareWithResultAsString.apply(2, 3); System.out.println(result); //36
String::valueOf
is a method reference of the lambda expression result -> String.valueOf(result)
.
multiply.andThen(square) .andThen(String::valueOf);
Conclusion
In this post we learned how we can do function chaining in Java 8. We learnt about the andThen and the compose methods in the Function interface. Finally, we also looked at the andThen method of BiFunction interface too.
To learn more about Java streams and other Java features, take a look at other java-stream and java-8 posts.