Grokking Spring Boot
Vote

0% completed

​

Lambdas and Functional Interfaces

Functional Interfaces

Lambda Syntax

Method References

The Standard Interfaces in java.util.function

Spring Methods That Accept Lambdas

When a Lambda Hurts Readability

Key Takeaways

Practice Questions

This code sorts the seats of an event by their labels, in the style Java used before lambdas existed:

seats.sort(new Comparator<Seat>() { @Override public int compare(Seat a, Seat b) { return a.label().compareTo(b.label()); } });

Six lines create an object whose only job is one comparison. The same sort written with a lambda takes one line:

seats.sort((a, b) -> a.label().compareTo(b.label()));

Both versions sort the seats the same way. Spring code uses the second form constantly, because many Spring methods accept a small piece of behavior as an argument. What follows explains which interface a lambda becomes and how a method reference shortens it. It then covers the standard interfaces in java.util.function and one Spring API that takes a lambda.

Functional Interfaces

A functional interface is an interface with exactly one abstract method, and it is the only kind of type a lambda can become. The Java documentation describes these interfaces as the target types for lambda expressions and method references. Comparator is one: its single abstract method is compare.

When the compiler meets a lambda, it looks at the type the context expects, which is called the target type. In the sort call, the target type is Comparator, the parameter type of List.sort. The compiler checks that the lambda's parameters and result match compare. It then creates an object that implements Comparator, with the lambda as the body of compare.

You can declare your own functional interface. The booking service might describe a rule about seats like this:

@FunctionalInterface interface SeatRule { boolean allows(Seat seat); } SeatRule bookable = seat -> !seat.taken(); SeatRule frontRow = seat -> seat.label().startsWith("A");

The annotation @FunctionalInterface is optional. It asks the compiler to report an error if someone adds a second abstract method. Default and static methods do not count, so an interface may have many of them and still be functional.

The compiler turns a lambda into an object of its target interface, and a call to the one abstract method runs the lambda body.
The compiler turns a lambda into an object of its target interface, and a call to the one abstract method runs the lambda body.

Notice that the lambda never names SeatRule or allows. The compiler takes both from the declared type of the variable, so the same lambda text could become another interface elsewhere.

Lambda Syntax

A lambda is a parameter list, an arrow, and a body, where the body is either one expression or a block of statements. Three forms cover almost every case:

FormExampleWhen to use it
One parameter, expression bodyseat -> !seat.taken()One short calculation
Several parameters(a, b) -> a.label().compareTo(b.label())Comparators and other two-input methods
Block bodyseat -> { log(seat); return !seat.taken(); }Several statements, with an explicit return

The parameter types are usually left out, because the compiler reads them from the target type. A lambda may also use local variables from the method around it. The language specification allows this only for variables that are final or effectively final, which means never assigned again after their first value.

That rule has a reason. A lambda may run later, after its method has returned, and possibly on another thread. If the variable could still change, the lambda would see a value that depends on timing.

Method References

A method reference is a lambda that only calls one existing method, written with two colons: Type::method or object::method. There are four kinds:

KindMethod referenceThe same lambda
Static methodInteger::parseInttext -> Integer.parseInt(text)
Method of a given objectSystem.out::printlnx -> System.out.println(x)
Method of the argumentSeat::labelseat -> seat.label()
ConstructorArrayList::new() -> new ArrayList<>()

The third kind appears most often. For example, Comparator.comparing(Seat::label) compares seats by their labels, and .reversed() turns it into the opposite order. A method reference reads well when the method name already says what the lambda would do. When the call needs extra arguments or a calculation, a lambda is clearer.

The Standard Interfaces in java.util.function

The package java.util.function holds ready-made functional interfaces for the common shapes of a method, so most APIs never declare their own. Four of them cover most needs:

InterfaceAbstract methodTakes, returnsBooking service example
Supplier<T>get()nothing, returns TCreate an empty list of seat labels
Consumer<T>accept(T)T, returns nothingPrint or send one message
Function<T, R>apply(T)T, returns RTurn a seat into its label
Predicate<T>test(T)T, returns booleanIs this seat free?

The SeatRule interface above has the same shape as Predicate<Seat>. Real code usually uses Predicate for two reasons: every Java developer knows it, and its default methods (and, or, negate) combine rules. The package also has variants for two inputs, such as BiFunction, and for primitives, such as IntPredicate.

The program below uses these ideas on five seats: sorting, the SeatRule interface, the four standard interfaces, and a Supplier that delays work. Run it and match each line of output to its code.

Java
Java

. . . .

It prints the following:

1. Sorting with a lambda and with a method reference lambda: [A10, A11, A12, B11, B12] method reference: [B12, B11, A12, A11, A10] 2. A lambda for our own interface A12: bookable false, front row true B12: bookable true, front row false 3. The four interfaces from java.util.function free in row A: [A11, A10] 4. A Supplier delays work until it is needed debug off: messages built = 0 DEBUG 5 seats checked debug on: messages built = 1

Part 3 combines two predicates with the method and, so only free seats in row A remain. Part 4 shows why many methods accept a Supplier instead of a ready value. The method debug calls message.get() only when debug output is on. With debug off, the summary is never built, and the counter stays at 0.

Spring Methods That Accept Lambdas

Many Spring types are functional interfaces, so a lambda is the shortest way to give Spring behavior that it calls later. One example is CommandLineRunner from Spring Boot. Its documentation says a bean of this type runs when it is contained within a SpringApplication. Its single abstract method is run(String... args).

In the booking service, a configuration class can declare such a bean with a lambda:

@Configuration class StartupConfig { private static final Logger log = LoggerFactory.getLogger(StartupConfig.class); @Bean CommandLineRunner logArguments() { return args -> log.info("Command-line arguments: {}", Arrays.toString(args)); } }

The method logArguments returns a lambda, and the declared return type makes CommandLineRunner its target type. When we started the jar with --server.port=8102, this line appeared directly after the "Started BookingApplication" line:

Command-line arguments: [--server.port=8102]

The order shows what happened. During startup, the container called logArguments and stored the lambda it returned as a bean. After startup, Spring Boot called run on every CommandLineRunner bean and passed it the command-line arguments.

A bean method returns a lambda, the container stores it, and Spring Boot calls its run method after startup.
A bean method returns a lambda, the container stores it, and Spring Boot calls its run method after startup.

Notice the gap between creating the lambda and running it. The body ran only after startup finished, long after logArguments had returned, which shows why the language allows a lambda to use only local variables that can no longer change.

When a Lambda Hurts Readability

Lambdas make short behavior shorter, but they have three limits:

  • Long bodies. A lambda of ten lines inside a method call hides the call itself. Move the body into a named private method and pass a method reference to it.
  • Checked exceptions. The method apply in Function declares no checked exceptions, so a lambda that calls a method throwing IOException must catch it inside the body.
  • No name. A named method says its purpose, while a lambda relies on the variable or parameter that holds it.

Key Takeaways

  • A functional interface has exactly one abstract method, and a lambda can become only such an interface.
  • The compiler takes the interface from the target type: the variable, parameter, or return type that receives the lambda.
  • A lambda may use local variables only if they are final or effectively final, because it may run later.
  • A method reference such as Seat::label is a shorter lambda that only calls one existing method.
  • Supplier, Consumer, Function, and Predicate cover most shapes, and Spring types such as CommandLineRunner accept lambdas too.

Lambdas become most useful in a chain of small steps over a collection of values. Streams applies them to all 600 seats of an event.

Practice Questions

1. A teammate adds a second abstract method, String reason(), to the SeatRule interface. What happens to the line SeatRule bookable = seat -> !seat.taken();, and what does @FunctionalInterface on SeatRule add?

<details><summary>Show answer</summary>

The line stops compiling, because a lambda can only implement an interface with exactly one abstract method. The compiler cannot know which of the two methods the lambda body belongs to, so every lambda for SeatRule fails with "SeatRule is not a functional interface". The annotation adds one more error at the interface itself, "Unexpected @FunctionalInterface annotation", which points to the change that caused the problem.

</details>

2. Inside a method, a developer writes int checked = 0; and then a lambda that runs checked++ for each seat. Why does this not compile, and why does Java have this rule?

<details><summary>Show answer</summary>

A lambda may only use local variables that are final or effectively final, and checked++ assigns the variable again. A lambda can run after its method returns, or on another thread, so a changing local variable would give results that depend on timing. A counter that must change belongs in an object, such as an AtomicInteger, or the loop should be written without a lambda.

</details>

3. Rewrite seat -> seat.label() and () -> new ArrayList<String>() as method references, and name the functional interface each one fits.

<details><summary>Show answer</summary>

They become Seat::label and ArrayList::new. The first takes a seat and returns a String, so it fits Function<Seat, String>. The second takes nothing and returns a new list, so it fits Supplier<List<String>>. The same method reference could fit another interface with the same shape, because the target type decides.

</details>

4. A method signature is void debug(boolean enabled, Supplier<String> message). Why is this better than void debug(boolean enabled, String message) when building the message is expensive?

<details><summary>Show answer</summary>

With a Supplier, the caller passes the work of building the message, not the finished message. The method calls get only when debug output is on, so when it is off the message is never built. With a String parameter, Java builds the String before the call, even when the method then ignores it. The program above shows this: the counter stayed at 0 while debug output was off.

</details>

Reading Progress

0%


Vote for new content

On This Page

Functional Interfaces

Lambda Syntax

Method References

The Standard Interfaces in java.util.function

Spring Methods That Accept Lambdas

When a Lambda Hurts Readability

Key Takeaways

Practice Questions