Passing Java Functions in Variables

17 Sep 2022

Java’s functional interfaces were introduced back in Java 8, so they’ve been around for a long time, at this point.

One of the best insights I have read about these interfaces is in a Stack Overflow answer written by Stuart Marks:

The reason you’re having difficulty grasping the meaning of functional interfaces such as those in java.util.function is that the interfaces defined here do not have any meaning! They are present primarily to represent structure, not semantics.

He goes on to contrast these functional interfaces with an interface such as the List interface. For List, it is clear what data structure is being represented: it’s a list, of course! And it is clear what you can do with that list: you can add elements to it, you can iterate over it, and so on…

But what can you do with Consumer<T> and its accept(T t) method? A method which, as the JavaDoc says:

Performs this operation on the given argument.

What is “this operation”? That’s quite abstract, compared to List.

Stuart Marks goes on to clarify that these functional interfaces:

are interfaces that merely represent the structure of a function, such as the number of arguments, the number of return values, and (sometimes) whether an argument or return value is a primitive.

That, for me, is an extremely helpful insight. The functional interface provides the structure. You provide the semantics. And because its a functional interface, you can use Java’s lambda expressions as implementations - and that gives you a lot of flexibility in how they can be used. For example, you can assign a lambda function to a variable.

Some example code, with notes, is shown below.

But first:

Just because you can pass functions around in this way, doesn’t mean you always should.

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.function.Function;  
import java.util.function.Consumer;  
import java.util.function.IntConsumer;  
import java.util.function.IntUnaryOperator;  
import java.util.function.Predicate;  
import java.util.function.Supplier;  

...

// lambda assigned to a reference, where  
// Function has an input and an output:  
Function<Integer, Integer> addOne = x -> x + 1;  

int result1 = addOne.apply(123);  
int result2 = foo(addOne, 234);  

// a method assigned to a reference, where   
// Consumer takes an input (void return):  
final Consumer<Integer> simpleReference1 = App::someMethod1;  
simpleReference1.accept(1);  

// a method assigned to a reference, where   
// IntConsumer takes an int input (void return):  
final IntConsumer simpleReference2 = App::someMethod2;  
simpleReference2.accept(1);  

// a method assigned to a reference, where   
// IntUnaryOperator uses int for its input and output:  
final IntUnaryOperator simpleReference3 = App::someMethod3;  
int foo = simpleReference3.applyAsInt(1);  

final Consumer<Integer> another = i -> System.out.println(i);  
another.accept(2);  

MyInterface myInterface = (p1, p2) -> {  
    return p2 + p1;  
};  

Another example:

Consider the incredibly simple Java class MyBean:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class MyBean {

    private final int id;

    public MyBean(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

}

And the following class, which stores three functions in a map:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

public class MapOfFunctions {

    private final Map<String, Function<MyBean, String>> functions = new HashMap<>();

    public MapOfFunctions() {
        functions.put("print_it", MyBean::toString);
        functions.put("print_id", bean -> Integer.toString(bean.getId()));
        functions.put("id_to_hex", bean -> {
            int i = bean.getId();
            return String.format("0x%x%n", i);
        });
    }

    public String doFunctionWork(MyBean myBean, String functionKey) {
        return functions.get(functionKey).apply(myBean);
    }

}

We can execute any of these functions as follows:

Java
1
2
3
MapOfFunctions mapOfFunctions = new MapOfFunctions();
String s = mapOfFunctions.doFunctionWork(new MyBean(16), "id_to_hex");
System.out.println(s); // 0x10

The semantics are provided by me in those three example functions, stored in the map, and called using the apply() method from the Function interface.