Type Checking and Execution of Lambda Expressions
A lambda expression can only be defined in a context where a functional interface can be used: for example, in an assignment context, a method call context, or a cast context (p. 733). The compiler determines the target type that is required in the context where the lambda expression is defined. This target type is always a functional interface type. In the assignment context below, the target type is Predicate<Integer>, as it is the target of the assignment statement. Note that the type parameter T of the functional interface is Integer.
Predicate<Integer> p1 = i -> i % 2 == 0; // (1) Target type: Predicate<Integer>
The method type of a method declaration comprises its type parameters, formal parameter types, return type, and any exceptions the method throws.
The function type of a functional interface is the method type of its functional method. The target type Predicate<Integer> has the following method, where type parameter T is Integer:
public boolean test(Integer t); // Method type: Integer -> boolean
The function type of the target type Predicate<Integer> is the method type of the this test() method:
Integer -> boolean
The type of the lambda expression defined in a given context must be compatible with the function type of the target type. If the lambda expression has inferred-type parameters, their type is inferred from the context, and if necessary, from the function type. From the lambda body at (1), it is possible to infer that the type of i is int (from the expression i % 2), and the lambda body evaluates to a boolean value (from the evaluation of the == operator). Just from the context, it is thus possible to infer that the type of the lambda expression at (1) is:
int -> boolean
From the function type of the target type Predicate<Integer>, the compiler is able to determine that if the inferred int type of the parameter i in the lambda expression at (1) is promoted to Integer, the type of the lambda expression at (1) would then be compatible with the function type of the target type Predicate<Integer>, that is:
Integer -> boolean
The lambda expression defined at (1) above is equivalent to the following implementation using an anonymous class.
Predicate<Integer> p1 = new Predicate<>() { // Anonymous class
public boolean test(Integer i) {
return i % 2 == 0;
}
};
It is possible to determine that the type of the lambda expression at (1) is compatible with the method type of the only abstract method test() defined by the Predicate<T> functional interface because it is not ambiguous which method the lambda expression should implement in the functional interface. However, this would not work for an interface that had more than one abstract method, as it would not be clear which abstract method to implement.
In the following assignment, the target type is java.util.function.IntPredicate:
IntPredicate p2 = i -> i % 2 == 0; // (2) Target type: IntPredicate
The IntPredicate functional interface has the following abstract method:
public boolean test(int i); // Method type: int -> boolean
The function type of the target type IntPredicate is the method type of its abstract method:
int -> boolean
The compiler infers that the type of the inferred-type parameter i in the lambda expression at (2) should be int. As the lambda body returns a boolean value, the type of the lambda expression at (2) is
int -> boolean
The type of the lambda expression is compatible with the function type of the target type IntPredicate.
Note that in both examples, the lambda expression is the same, but their types are different in the two contexts. They represent two different values. The type of a lambda expression is determined by the context in which it is defined.
System.out.println(p1 == p2); // false
The process of type checking a lambda expression in a given context is called target typing. The presentation here is simplified, but suffices for our purpose to give an idea of what is involved.
The compiler does the type checking for using lambda expressions. The runtime environment provides the rest of the magic to make it all work. At runtime, the lambda expression is executed when the sole abstract method of the functional interface is invoked.
boolean result1 = p1.test(2021); // false
boolean result2 = p2.test(2020); // true
As mentioned earlier, this is an example of deferred execution. Lambda execution is similar to invoking a method on an object. We define a lambda expression as a function and use it like a method, letting the compiler and the runtime environment put it all together.