java.util.function.Function<T, R>
is a functional interface. It takes a type T
and returns type R
. We mostly use it to transform a type into a different one. But it doesn’t end there. We can use this into a lambda expression to change a value as well. For example-
Function<Integer, Integer> doubleIt = a -> a * 2;
Over here, we are receiving and in integer but returning after doubling it. Thus, we can pass this function as an argument of a method as well.
static Integer transform(Integer value, Function<Integer, Integer> func) {
var applied = func.apply(value);
return applied;
}
and use it as follows-
var doubled = transform(4, doubleIt);
System.out.println("doubled = " + doubled);
It will print 8.
Similarly, we can pass many other lambda expressions into the transform method.
Function<Integer, Integer> squareIt = a -> a * a;
Function<Integer, Integer> cubeIt = a -> a * a * a;
var squared = transform(4, squareIt);
System.out.println("square of 4 = " + squared);
var cubed = transform(5, cubeIt);
System.out.println("cube of 5 = " + cubed);
We may have another requirement, for example, we need to cube a value and then increment it by 1. We can do it in two steps.
Function<Integer, Integer> cubeIt = a -> a * a * a;
Function<Integer, Integer> incrementByOne = a -> a + 1;
var cubedOf5 = cubeIt.apply(5);
var cubedAndIncremented = incrementByOne.apply(cubedOf5);
However, the above code doesn’t look concise at all.
We can fix it using the default method of Function
interface, which is andThen()
, and it takes a function as an argument. Using it, we can chain up multiple functions together. The above code would look like this -
transform(5, cubeIt.andThen(incrementByOne));
This is a one-liner and looks concise and straightforward.
We can use this concept to implement the decorator pattern. This pattern lets you attach new behaviours to objects at runtime.
We will make coffee with as many ingredients as we can put into a cup using the decorator pattern. When we’d do it, our coffee cup would like this-
var ingredients = List.of("Tim Horton");
var coffeeCup = new CoffeeCup(ingredients);
var coffee = getCoffeeWithExtra(coffeeCup,
Coffee::withDarkCookieCrumb,
Coffee::withSaltedCaramelFudge,
Coffee::withSweetenedMilk,
Coffee::withVanillaAlmondExtract);
System.out.println("Coffee with " + String.join(", ", coffee.ingredients()));
We will grab a Tim Horton and then keep adding extra ingredients to it using the getCoffeeWithExtra()
method.
Let’s look at the getCoffeeWithExtra() method.
static Coffee getCoffeeWithExtra(Coffee coffee, Function<Coffee, Coffee>... ingredients) {
var reduced = Stream.of(ingredients)
.reduce(Function.identity(), Function::andThen);
return reduced.apply(coffee);
}
This method takes a Coffee and an array of functions that transform the coffee, exactly the way we saw in the transform method. We have reduced the function array into one using stream and then applied it to the coffee and returned it. Let’s look into the Coffee interface. It has only one method and a few static methods.
The stream has this reduce function, which takes an identity function as its first parameter; the second parameter is another functional interface named BinaryOperator
.
The identity function is a function that doesn’t do anything; basically, it returns the same value that it gets as a parameter. Without the method reference, it looks like the following-
var reduce1 = Stream.of(ingredients)
.reduce(kopi -> kopi, (func1, func2) -> func1.andThen(func2));
Note(From javaDoc): The reduce function performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value. This is equivalent to:
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
Let’s look at the Coffee interface now.
@FunctionalInterface
interface Coffee {
List<String> ingredients();
static Coffee withSaltedCaramelFudge(Coffee coffee) {
return () -> coffee.add("Salted Caramel Fudge");
}
default List<String> add(String item) {
return new ArrayList<>(ingredients()) {{
add(item);
}};
}
static Coffee withSweetenedMilk(Coffee coffee) {
return () -> coffee.add("Sweetened Milk");
}
static Coffee withDarkCookieCrumb(Coffee coffee) {
return () -> coffee.add("Dark Cookie Crumb");
}
static Coffee withVanillaAlmondExtract(Coffee coffee) {
return () -> coffee.add("Vanilla/Almond Extract");
}
}
These static methods are for convenience. We can keep adding these methods as our ingredient list grows. The benefit is, when we will use them, we will be able to use method references, which will make our code concise.
Let’s put everything together.
These static methods are for convenience. We can keep adding these methods as our ingredient list grows. The benefit is, when we will use them, we will be able to use method references, which will make our code concise.
Let’s put everything together.
package com.bazlur;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
public class Day012 {
public static void main(String[] args) {
var ingredients = List.of("Tim Horton");
var coffeeCup = new CoffeeCup(ingredients);
var coffee = getCoffeeWithExtra(coffeeCup,
Coffee::withDarkCookieCrumb,
Coffee::withSaltedCaramelFudge,
Coffee::withSweetenedMilk,
Coffee::withVanillaAlmondExtract);
System.out.println("Coffee with " + String.join(", ", coffee.ingredients()));
}
@SafeVarargs
static Coffee getCoffeeWithExtra(Coffee coffee, Function<Coffee, Coffee>... ingredients) {
var reduced = Stream.of(ingredients)
.reduce(Function.identity(), Function::andThen);
return reduced.apply(coffee);
}
@FunctionalInterface
interface Coffee {
static Coffee withSaltedCaramelFudge(Coffee coffee) {
return () -> coffee.add("Salted Caramel Fudge");
}
default List<String> add(String item) {
return new ArrayList<>(ingredients()) {{
add(item);
}};
}
List<String> ingredients();
static Coffee withSweetenedMilk(Coffee coffee) {
return () -> coffee.add("Sweetened Milk");
}
static Coffee withDarkCookieCrumb(Coffee coffee) {
return () -> coffee.add("Dark Cookie Crumb");
}
static Coffee withVanillaAlmondExtract(Coffee coffee) {
return () -> coffee.add("Vanilla/Almond Extract");
}
}
record CoffeeCup(List<String> initialIngredient) implements Coffee {
@Override
public List<String> ingredients() {
return initialIngredient;
}
}
}
% java Day012.java
Coffee with Tim Horton, Dark Cookie Crumb, Salted Caramel Fudge, Sweetened Milk, Vanilla/Almond Extract