3.6 Processing Lambda Expressions
Up to now, you have seen how to produce lambda expressions and pass them to a method that expects a functional interface. In the following sections, you will see how to write your own methods that can consume lambda expressions.
3.6.1 Implementing Deferred Execution
The point of using lambdas is deferred execution. After all, if you wanted to execute some code right now, you’d do that, without wrapping it inside a lambda. There are many reasons for executing code later, such as:
- Running the code in a separate thread
- Running the code multiple times
- Running the code at the right point in an algorithm (for example, the comparison operation in sorting)
- Running the code when something happens (a button was clicked, data has arrived, and so on)
- Running the code only when necessary
Let’s look at a simple example. Suppose you want to repeat an action n times. The action and the count are passed to a repeat method:
repeat(10, () -> System.out.println("Hello, World!"));
To accept the lambda, we need to pick (or, in rare cases, provide) a functional interface. In this case, we can just use Runnable:
public static void repeat(int n, Runnable action) { for (int i = 0; i < n; i++) action.run(); }
Note that the body of the lambda expression is executed when action.run() is called.
Now let’s make this example a bit more sophisticated. We want to tell the action in which iteration it occurs. For that, we need to pick a functional interface that has a method with an int parameter and a void return. Instead of rolling your own, I strongly recommend that you use one of the standard ones described in the next section. The standard interface for processing int values is
public interface IntConsumer { void accept(int value); }
Here is the improved version of the repeat method:
public static void repeat(int n, IntConsumer action) { for (int i = 0; i < n; i++) action.accept(i); }
And here is how you call it:
repeat(10, i -> System.out.println("Countdown: " + (9 - i)));
3.6.2 Choosing a Functional Interface
In most functional programming languages, function types are structural. To specify a function that maps two strings to an integer, you use a type that looks something like Function2<String, String, Integer> or (String, String) -> int. In Java, you instead declare the intent of the function using a functional interface such as Comparator<String>. In the theory of programming languages this is called nominal typing.
Of course, there are many situations where you want to accept “any function” without particular semantics. There are a number of generic function types for that purpose (see Table 3–1), and it’s a very good idea to use one of them when you can.
Table 3–1 Common Functional Interfaces
Functional Interface |
Parameter types |
Return type |
Abstract method name |
Description |
Other methods |
Runnable |
none |
void |
run |
Runs an action without arguments or return value |
|
Supplier<T> |
none |
T |
get |
Supplies a value of type T |
|
Consumer<T> |
T |
void |
accept |
Consumes a value of type T |
andThen |
BiConsumer<T, U> |
T, U |
void |
accept |
Consumes a value of type T and U |
andThen |
Function<T, R> |
T |
R |
apply |
A function with argument of type T |
compose, andThen, identity |
BiFunction<T, U, R> |
T, U |
R |
apply |
A function with argument of type T and U |
andThen |
UnaryOperator<T> |
T |
T |
apply |
A unary operator on the type T |
compose, andThen, identity |
BinaryOperator<T> |
T, T |
T |
apply |
A binary operator on the type T |
andThen, maxBy, minBy |
Predicate<T> |
T |
boolean |
test |
A boolean-valued function |
and, or, negate, isEqual |
BiPredicate<T, U> |
T, U |
boolean |
test |
A boolean-valued function with two arguments |
and, or, negate |
For example, suppose you write a method to process files that match a certain criterion. Should you use the descriptive java.io.FileFilter class or a Predicate<File>? I strongly recommend that you use the standard Predicate<File>. The only reason not to do so would be if you already have many useful methods producing FileFilter instances.
Table 3–2 lists the 34 available specializations for primitive types int, long, and double. It is a good idea to use these specializations to reduce autoboxing. For that reason, I used an IntConsumer instead of a Consumer<Integer> in the example of the preceding section.
Table 3–2 Functional Interfaces for Primitive Types p, q is int, long, double; P, Q is Int, Long, Double
Functional Interface |
Parameter types |
Return type |
Abstract method name |
BooleanSupplier |
none |
boolean |
getAsBoolean |
PSupplier |
none |
p |
getAsp |
PConsumer |
p |
void |
accept |
ObjPConsumer<T> |
T, p |
void |
accept |
PFunction<T> |
p |
T |
apply |
PToQFunction |
p |
q |
applyAsQ |
ToPFunction<T> |
T |
p |
applyAsP |
ToPBiFunction<T, U> |
T, U |
p |
applyAsP |
PUnaryOperator |
p |
p |
applyAsP |
PBinaryOperator |
p, p |
p |
applyAsP |
PPredicate |
p |
boolean |
test |
3.6.3 Implementing Your Own Functional Interfaces
Ever so often, you will be in a situation where none of the standard functional interfaces work for you. Then you need to roll your own.
Suppose you want to fill an image with color patterns, where the user supplies a function yielding the color for each pixel. There is no standard type for a mapping (int, int) -> Color. You could use BiFunction<Integer, Integer, Color>, but that involves autoboxing.
In this case, it makes sense to define a new interface
@FunctionalInterface public interface PixelFunction { Color apply(int x, int y); }
Now you are ready to implement a method:
BufferedImage createImage(int width, int height, PixelFunction f) { BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) for (int y = 0; y < height; y++) { Color color = f.apply(x, y); image.setRGB(x, y, color.getRGB()); } return image; }
To call it, supply a lambda expression that yields a color value for two integers:
BufferedImage frenchFlag = createImage(150, 100, (x, y) -> x < 50 ? Color.BLUE : x < 100 ? Color.WHITE : Color.RED);