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]
Note: We could have used the function inline in the map call without declaring it separately.
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)?
Yes!!. The Function interface has two default methods named andThen() and compose() which allow us to compose methods.
Function andThen - To chain a function at the end
Method Signature:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
The andThen() method accepts a function as an argument and returns a composed function. The composed function first applies this function (the function instance on which andThen was called) to the input and then applies the after function to the result.
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)
The compose() method accepts a function as an argument and returns a composed function. The composed function first applies the before function to the input, and then applies this function (the function instance on which compose was called) to the result.
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(squareFunction);
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 = squareFunction.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)
It returns a composed function which applies this function first to the input. And then it applies the passed function (after) to the result. Note that it accepts a Function and still returns a BiFunction. Let us look at an example.
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> multiply = (a, b) -> a * b;
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.
For the same reason, a BiFunction does not have a compose method. The result of the before function passed as the argument would result in a single-valued output. We cannot invoke the (after) BiFunction with that.
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)
.
The created composed function mapping chain is,
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.