7. Functional Interfaces, Lambdas, and Events
In many programs, we need to pass not data but behavior to a method: the sort criterion, the filtering condition, or the processing to be performed when something happens. Java addresses this need with a simple concept upon which this entire chapter is based: the functional interface—that is, an interface that declares only a single abstract method. An object that implements such an interface “carries” a function, which can be stored in a variable, passed as a parameter, returned as the result of a method, and executed at will.
Before Java 8 (2014), you had to write a class—often an anonymous class—for each behavior you wanted to express, which was verbose. Java 8 introduced lambda expressions and method references, which allow you to write this behavior in a single line. We’ve already used them without going into detail with the API Stream in Chapter 5: in [persons.stream().filter(p -> p.age() >= 18)], we pass a lambda—that is, an instance of a functional interface—to the [filter] method. This chapter explains what lies behind this syntax. It concludes with events, which Java handles using “listeners”: objects—often lambdas—that register with an emitter to be notified of what is happening there. Java graphical user interfaces (Swing, JavaFX) and web frameworks alike rely on this mechanism: a button click or the arrival of a request (HTTP) are signaled to registered listeners. This course does not cover graphical user interfaces; all examples are console programs.
The programs in this chapter are compact source files located in the [07-lambdas] folder. They are run using [java LambdasNN.java]. Note that a compact source file automatically imports the entire [java.base] module: the interfaces from the [java.util.function], [java.util], or [java.util.concurrent] packages can be used there without any [import] statements.
7.1. Predefined functional interfaces
7.1.1. The concept of a functional interface
A functional interface is an interface that has exactly one abstract method. It may also have as many [default] and [static] methods as desired (see Chapter 4): these do not count. This single abstract method describes a model (also called a signature or a form) of a function: the types of its parameters and the type of its return value. A variable of the interface’s type can then refer to any object that provides a function of this form and execute it by calling the method.
The oldest example is the [java.lang.Runnable] interface, which has existed since Java 1.0 and represents a task to be executed (we’ll revisit it in connection with threads in Chapter 8). It consists of a single method: [void run();]. It describes a function:
- with no parameters;
- returning no result ([void]).
There are three ways to create an object of type [Runnable]:
- write a class that implements the interface, either named or anonymous. This was the only way to do it before Java 8;
- write a lambda expression: [Runnable task = () -> IO.println("task");];
- write a method reference to an existing method that has the correct form: [Runnable task = Functions::display;].
In the last two cases, the compiler creates the object that implements the interface itself: the lambda or the referenced method becomes the body of the method [run]. The function is then executed by calling the interface method: [task.run()]. Unlike other languages, Java does not have a specific call syntax: the interface method is always called by its name ([run], [apply], [test]…).
7.1.2. Interfaces in the java.util.function package
Before Java 8, each library defined its own single-method interfaces ([Runnable], [Comparator], [ActionListener]…). Java 8 introduced the [java.util.function] package, which provides 43 generic functional interfaces covering virtually all requirements. The main ones are as follows:
Interface | Abstract Method | Functions Represented |
[Runnable] ([java.lang]) | [void run()] | No parameters, no results |
[Supplier<T>] | [T get()] | No parameters, one result: a “supplier” |
[Consumer<T>] | [void accept(T t)] | one parameter, no result: a “consumer” |
[BiConsumer<T, U>] | [void accept(T t, U u)] | two parameters, no results |
[Function<T, R>] | [R apply(T t)] | one parameter, one result of type [R] (always the last type) |
[BiFunction<T, U, R>] | [R apply(T t, U u)] | two parameters, one result |
[UnaryOperator<T>] | [T apply(T t)] | one [Function<T, T>]: parameter and result of the same type |
[BinaryOperator<T>] | [T apply(T t1, T t2)] | a [BiFunction<T, T, T>] |
[Predicate<T>] | [boolean test(T t)] | one parameter, one Boolean result: a “predicate” |
[BiPredicate<T, U>] | [boolean test(T t, U u)] | two parameters, a Boolean result |
[Comparator<T>] ([java.util]) | [int compare(T o1, T o2)] | two parameters of the same type, an integer result |
[Callable<V>] ([java.util.concurrent]) | [V call() throws Exception] | Same as [Supplier<V>], but may throw a checked exception |
Java generics apply only to object types: a [Function<Integer, Integer>] receives and returns [Integer], which requires automatic conversions (boxing and unboxing) between [int] and [Integer]. To avoid these, the package provides specialized versions for the primitive types [int], [long], and [double]: [IntBinaryOperator] ([int applyAsInt(int left, int right)]), [IntUnaryOperator], [IntPredicate], [IntSupplier], [IntConsumer], [IntFunction<R>] (with [int] as a parameter), [ToIntFunction<T>] (with [int] as the result), and their equivalents [Long…] and [Double…]. There is no predefined interface with three or more parameters; in that case, you must define your own (see the following paragraph).
The program [Lambdas01.java] uses these interfaces with method references:
Results:
Comments:
- Lines 5–10: Creation of a [Runnable] object using an anonymous class—that is, a class without a name—declared and instantiated right where it is needed. It implements the [run] method, which calls the [display] method on line 52. This was the required syntax prior to Java 8;
- line 12: the same thing using a method reference: [Functions::display] refers to the static method [display] of the class [Functions]. The compiler verifies that it has the form of the method [run] (no parameters, no return value) and creates the object [Runnable] that calls it. This is the standard syntax today;
- line 14: the function is executed by calling the method of the interface. The method [display] on line 52 is executed (line 2 of the results);
- Lines 48–50: The referenced methods are grouped into a class named [Functions]. We would have liked to write them directly in the file, next to [main], and name them [Lambdas01::add]. However, the implicit class that wraps a compact source file cannot be named in the code (the compiler returns [cannot find symbol]);
- line 17: [BiFunction<Integer, Integer, Integer>] refers to a function with two parameters, [Integer], and returning a [Integer] (the last generic type). The method [add] on line 56, which works with [int], is suitable: the compiler adds the conversions between [int] and [Integer]. The function is called via its method [apply] (line 18);
- line 20: [Supplier<String>] refers to a parameterless function that returns a string. It is called via its method [get];
- line 23: the primitive version [IntBinaryOperator] operates directly on [int] values, without conversion. Its method is called [applyAsInt]. This is the preferred type for computationally intensive tasks;
- Line 27: [Consumer<String>] refers to a function that takes one parameter, [String], and returns nothing. The static method [IO.println(Object)] is appropriate: a [String] is a [Object]. It is executed via its method [accept];
- line 31: a predicate is a function that returns a Boolean; it is executed via its method [test] (line 32). The method [removeIf] of the interface [Collection] (line 35) expects a parameter of type [Predicate]: it removes from the collection all elements for which the predicate returns [true], and returns [true] if it has removed at least one element;
- line 40: the method [sort] of the interface [List] accepts a [Comparator] that compares two elements (result can be negative, zero, or positive, like [compareTo]). Here, the names are sorted in ascending order by length. The method [compareLengths] (line 72) uses [Integer.compare], which compares two [int] objects without the risk of an overflow (which a simple subtraction does not guarantee);
- Lines 44–45: A method reference, like a lambda, is an object. Its class, displayed on line 10 of the results, is a class generated at runtime by the virtual machine, whose name contains [$$Lambda] followed by an address that changes from one execution to the next. On line 11 of the results, we see that this class implements the [BiFunction] interface. A lambda is therefore not a new type of object: it is an instance of an ordinary class that implements a functional interface.
7.2. Defining Your Own Functional Interfaces
The predefined interfaces are often sufficient, but you can define your own. Simply write an interface with a single abstract method, for example:
- [interface Operation { int calculate(int n1, int n2); }]
This interface represents a function that takes two integers and returns an integer. Compared to [IntBinaryOperator]*, the type *[Operation] has a name that describes its purpose, its method has a descriptive name ([calculate] rather than [applyAsInt]*), and its parameters have names ([n1], [n2]) that development tools display. It is preceded by the annotation [@FunctionalInterface]*: this is not required, but it instructs the compiler to verify that the interface is functional, and it documents the developer’s intent.
A variable [op] of type [Operation] references only one function at a time. Unlike other languages, which offer “multicast delegates,” Java does not provide a way to store multiple functions in a single variable and execute them all with a single call: when needed, functions are placed in a list and executed one by one. This is exactly what we will do with event listeners.
Consider the example [Lambdas02.java]:
Results:
Comments:
- lines 47–55: definition of the functional interface [Operation]. In a compact source file, type declarations (classes, interfaces, records, etc.) can be placed before or after the method [main]; we place them after;
- Lines 52–54: A functional interface can have methods [default]. The method [then] constructs a new [Operation]: this method executes the current operation, then applies the function [next] to its result. It returns a lambda, which we will examine in the next section. The composition methods for the predefined interfaces ([andThen], [compose]…, etc.) are written based on this model, which we will discuss later;
- Lines 64–81: The class [Calculations] has two instance methods, [add] and [subtract], and one static method, [increase], all three of which have the same form as the method [calculate];
- line 5: the variable [op] is of type [Operation]. It is assigned a reference to the static method [Calculations.increase];
- line 7: the operation is executed by the method [calculate]. The method [Calculations.increase] is executed, with the parameters passed to [calculate];
- line 11: an instance [c1] of the class [Calculations] is created;
- Line 13: The reference [c1::add] refers to the method [add] of the object [c1]. [increase] was a static method, while [add] is an instance method: this makes no difference. In the second case, the object created by the compiler also stores the object [c1] on which the method will be called;
- line 17: the same is done with the instance method [subtract];
- lines 22–28: To execute multiple operations, they are placed in a list [List<Operation>], which is then iterated over. Method references and lambdas can be mixed (line 25). Each result is retrieved (lines 8–12 of the results);
- line 31: the method [default] [then] is applied to the operation [c1::add]. The conversion [(Operation)] is necessary: a method reference has no type of its own, and you cannot call a method on it until the compiler knows which interface it implements (we’ll come back to this). [squaredSum.calculate(1, 2)] calculates (1+2)², which is 9 (lines 13–14 of the results);
- line 35: [Operation] and [IntBinaryOperator] have exactly the same form, but they are two different types with no inheritance relationship. The assignment [IntBinaryOperator other = op;] would be rejected ([incompatible types: Operation cannot be converted to IntBinaryOperator]). In contrast, the method reference [op::calculate] has the correct form: it creates a [IntBinaryOperator] that calls the [calculate] method of the [op] object ([c1::subtract] at this stage of the program);
- lines 57–61: a functional interface can be generic. [Transformation<T, R>] represents a function that takes a [T] and returns a [R]: this is equivalent to [Function<T, R>], with names chosen by us;
- Line 39: [String::length] is suitable for a [Transformation<String, Integer>]: the method [length] will be called on the string passed as a parameter. Line 40: ["*"::repeat] refers to the method [repeat(int)] of the string object ["*"]; it takes an integer and returns a string. These are two different types of method references, which we will discuss in detail in the following section.
The annotation [@FunctionalInterface] comes into play if a second abstract method—for example, [int invert(int n);]—is accidentally added to the interface [Operation]. The compiler then flags the annotation itself and each of the lambdas and method references that use the interface (abbreviated output):
The messages from the [javac] compiler are in English: the JDK does not provide a French translation.
7.3. Anonymous classes, lambda expressions, and method references
7.3.1. From anonymous classes to lambdas
In the previous examples, each function is a named method written elsewhere in the program. For short functions used only once, this is cumbersome. Up until Java 7, the only alternative was the anonymous class: the class that implements the interface is written right where the object is created. Java 8 introduced a much more concise syntax—lambda expressions—which have virtually replaced anonymous classes for functional interfaces. The program [Lambdas03.java] demonstrates the different forms:
Results:
Comments:
- Lines 5–10: an anonymous class. The operator [new] is followed by the interface name, empty parentheses, and the body of a class that implements the interface. The compiler generates a class (with a name of its choosing, [Lambdas03$1]) and creates an instance of it. Six lines to express [n1 + n2];
- lines 14–16: the same thing using a lambda expression. We write the list of parameters, the [->] operator (an arrow, which is read as “gives”), then the body. The interface name and method name have disappeared: the compiler knows them from the variable’s type, which is called the lambda’s target type. A lambda whose body is a block enclosed in curly braces is a block-bodied lambda;
- line 20: the parameter types have disappeared. The compiler infers them from the target type [IntBinaryOperator]: [n1] and [n2] are of type [int]. The body is reduced to an expression, whose value is the result of the lambda: this is an expression-bodied lambda. This is the most common form;
- line 24: with a single parameter whose type is not specified, parentheses are optional. [x -> x * x] reads “x gives x times x.” Line 25: a lambda with one parameter of type [String] returns a Boolean: it is a [Predicate<String>];
- Lines 29–30: Without parameters, empty parentheses are used. A lambda whose body is a method call of type [void] is suitable for a [Runnable];
- lines 35–40: A lambda with a block body can contain any statements: declarations, loops, tests, etc.;
- line 44: The underscore character [_] denotes an anonymous parameter (Java 22). It is used when the interface’s signature requires parameters that the lambda does not need, which is common for event listeners. Multiple such parameters can be included in the same lambda;
- lines 49–51: The most common use of lambdas is to pass them directly as parameters to a method that expects a functional interface. Compare with [Lambdas01.java]: it is no longer necessary to write a method [isEven]. Here we are working on a copy of the list, because [removeIf] modifies the list it operates on;
- line 52: a lambda of type [Comparator<Integer>] that reverses the usual comparison: the list is sorted in descending order.
Let’s review the forms of a lambda expression:
Form | Example |
typed parameters, block body | [(int a, int b) -> { return a + b; }] |
untyped parameters, expression body | [(a, b) -> a + b] |
a single untyped parameter | [x -> x * x] |
no parameters | [() -> "Hello"] |
anonymous parameters | [(_, _) -> IO.println("received")] |
Parameters declared with [var] | [(var a, var b) -> a + b] |
Parameters are either all typed or all untyped: you cannot mix the two. Within a block body, a lambda that returns a result must have a [return] statement on every execution path, just like a method.
A lambda is not quite the same as a shorthand anonymous class. In an anonymous class, [this] refers to the instance of the anonymous class; in a lambda, [this] refers to the object of the method that encloses the lambda. Furthermore, an anonymous class creates a new scope: it can declare a variable with the same name as a variable in the method, which a lambda cannot do. Finally, an anonymous class can implement an interface with multiple methods or inherit from an abstract class, which is impossible with a lambda. In this course, we will use lambdas whenever possible.
7.3.2. Additional Information: Target Type and Method References
The [Lambdas04.java] program defines the concept of a target type and presents the four types of method references:
Results:
Comments:
- Line 5: A lambda does not have a type of its own. Its type is determined by the context: the type of the variable that receives it, the type of the parameter to which it is passed, or the type specified by a conversion. The declaration [var twice = x -> x * 2;] is therefore rejected by the compiler: [cannot infer type for local variable twice (lambda expression needs an explicit target-type)]. It would not know which interface to implement;
- line 6: with a conversion to [IntUnaryOperator], the target type is known, and [var] becomes valid. In practice, one would write [IntUnaryOperator twice = x -> x * 2;] instead;
- Lines 9–10: The same lambda expression, [() -> "Hello"], implements a [Supplier<String>] here and a [Callable<String>] there. These are two objects of different classes, which share only the form of their method;
- line 14: Since Java 11, lambda parameters can be declared using [var], which allows you to add a modifier ([final]) or an annotation to them—something that is not possible with an untyped parameter;
- lines 17–37: the four types of method references:
- line 19: a reference to a static method, [Class::staticMethod]. [Integer::parseInt] is equivalent to the lambda [s -> Integer.parseInt(s)];
- line 23: a reference to an instance method of a given object, [object::method]. [name::toUpperCase] is equivalent to [() -> name.toUpperCase()]: the object [name] is fixed once and for all;
- Lines 27–28: a reference to an instance method of any object of a class, [Class::instanceMethod]. The object on which the method will be called is not yet known: it becomes the first parameter of the function. [String::toUpperCase] is equivalent to [s -> s.toUpperCase()] (one parameter), and [String::compareToIgnoreCase] is equivalent to [(s1, s2) -> s1.compareToIgnoreCase(s2)] (two parameters), hence [Comparator<String>];
- lines 32–34: the reference to a constructor, [Class::new]. [ArrayList::new] is equivalent to [() -> new ArrayList<>()]; [StringBuilder::new], with a target type of [Function<String, StringBuilder>], selects the constructor that takes a string; [int[]::new] creates an array of the size passed as a parameter;
- line 37: the three creation functions are used; [reverse] reverses the contents of [StringBuilder] (line 8 of the results);
- Lines 40–47: A significant difference between [Callable] and [Supplier]. The method [call] is declared as [throws Exception]: a lambda of type [Callable] can therefore throw a checked exception (see Chapter 3), in this case a [IOException]. The method [get] of [Supplier] does not declare any exceptions: the same lambda of type [Supplier<Integer>] would be rejected ([unreported exception IOException; must be caught or declared to be thrown]). No interface of type [java.util.function] allows checked exceptions: a lambda that throws one must catch it itself, or use an interface whose method declares it. This is why [main] is declared as [throws Exception] (line 3): the calls to [callable.call()] (line 11) and [read.call()] (line 44) could theoretically invoke any [Exception], which the [catch] on line 45—limited to [IOException]—does not intercept;
- lines 50–52: two evaluations of the same method reference [name::toUpperCase] produce two different objects, which neither [==] nor [equals] consider to be equal (line 10 of the results). Keep this in mind: it will be important when unsubscribing from an event listener.
A method reference is simply a shorthand notation for a lambda that merely calls a method. We use it when it improves readability: [String::length] rather than [s -> s.length()].
7.3.3. Composing Functions
Predefined functional interfaces have methods [default] and [static] that create new functions from existing ones, such as the [then] method in our [Operation] interface. The program [Lambdas05.java] lists the main ones:
Results:
Comments:
- line 7: [f.andThen(g)] returns the result of applying [f] first, then [g]: (5 + 1) × 2 = 12;
- line 8: [f.compose(g)] first applies [g], then [f]: 5 × 2 + 1 = 11;
- lines 10–15: a dynamically constructed processing chain. We start with the identity function ([Function.identity()], which leaves its parameter unchanged) and add, via [andThen], each step in the list. The resulting function [normalize] removes leading and trailing spaces, converts the text to lowercase, and replaces spaces with dashes. Each call to [andThen] creates a new function: existing functions are never modified. Similarly, the list of steps could be read from a configuration file;
- lines 19–25: the predicates are combined using [and] (and), [or] (or), and [negate] (not). The static method [Predicate.not] (Java 11) returns the negation of a predicate: it allows you to write [Predicate.not(String::isEmpty)], whereas [String::isEmpty.negate()] is rejected (a method reference has no type until it has a target type);
- Lines 28–34: [c1.andThen(c2)] returns a [Consumer], which executes [c1] and then [c2] on the same value. This is the simplest way in Java to execute multiple operations with a single call. Line 30: [log::add] is a reference to the [add] method of the [log] object;
- line 39: [Comparator.comparing(f)] constructs a comparator that compares the values of [f] (here, age); [thenComparing] specifies the tiebreaker to use in case of a tie (here, name). The record [Person] (lines 60–65) provides the accessors [age()] and [name()]; we have redefined its method [toString] to shorten the display output;
- lines 41–42: [comparingInt] avoids converting the age to [Integer], [reversed] reverses the order obtained so far, and the second form of [thenComparing] takes, in addition to the criterion, the comparator to be used for that criterion: [Comparator.reverseOrder()], the reversed natural order;
- lines 45–46: [BinaryOperator.minBy] converts a comparator into a function that returns the smaller of its two parameters;
- lines 50–58: the method [filter] used for these tests. Its parameter is of type [Predicate<? super T>] rather than [Predicate<T>]: it therefore accepts, to filter a list of [String], a [Predicate<String>] as well as a [Predicate<Object>] or a [Predicate<CharSequence>]. This is the rule followed throughout the Java library: a functional parameter that consumes [T] is declared with [? super T], and a parameter that produces them is declared with [? extends T].
These methods allow you to write complex processes by combining simple functions, without writing a single class. We’ll revisit them with the API Stream.
7.3.4. Variable Capture and Closures
A lambda can use the local variables of the method in which it is defined. It is said to capture these variables. Java enforces an important rule in this regard, which distinguishes the language from many others (C#, JavaScript, Python): a captured local variable must be “effectively final,” meaning it is never modified after its initialization, neither in the method nor in the lambda. The program [Lambdas06.java] demonstrates what this rule allows and prohibits:
Results:
Comments:
- Line 9: The lambda uses the local variable [threshold] declared on line 8. [threshold] is not a parameter of the lambda; it is a captured variable. The variable is never modified: it is effectively final, without the need to declare it as [final] (which was required up to Java 7 for anonymous classes);
- Line 11: If we were to add
[threshold = 10;]after the lambda is created, the compiler would reject the lambda on line 9:*[local variables referenced from a lambda expression must be final or effectively final]. To change the threshold, we create another variable and another lambda (lines 12–13). Conversely, the question “Does the lambda see the variable’s value at the time of its creation, or its value at the time of its execution?” does not arise in Java: the two are identical. Specifically, the lambda receives a copy* of the variable’s value when it is created; - line 17: for the same reason, a lambda cannot modify a captured local variable: [n -> total += n] is rejected, with the same message;
- Lines 19–23: The captured variable [total] does not change; it still references the same object, [AtomicInteger]. However, nothing prevents the lambda from modifying the state of this object. The class [AtomicInteger] in the package [java.util.concurrent.atomic] encapsulates a mutable integer ([addAndGet], [incrementAndGet], [get]…), which can be safely used by multiple threads (Chapter 8). We also encounter the technique of a single-element array ([int[] total = {0};] followed by [total[0] += n]), which does not have this latter property;
- lines 4 and 25–28: the rule applies only to local variables. A lambda can read and modify the fields of the current object: it then captures the reference [this], which itself does not change. The field [nbCalls] is a field of the implicit class of the compact source file; since the method [main] is an instance method, the lambda can increment it;
- Lines 58–62: The method
*[createCounter]returns a lambda that captures its local variable*[value]*. Normally, a local variable is destroyed when the method ends. Here, the returned lambda has retained a copy of the reference [value], and the referenced object [AtomicInteger] survives [createCounter]* as long as the lambda exists. The combination of the lambda and the values it captures is called a “closure.” To create it, the virtual machine stores the captured values in fields of the object it creates for the lambda; - lines 31–34: Each call to [createCounter] creates a new [AtomicInteger] object—and thus a new counter—that is independent of the others. The results (lines 6–7) confirm this;
- lines 38–42: In a standard [for] loop, the variable [i] is modified on each iteration ([i++]): it is not effectively final, and the lambda on line 39 would be rejected. In other languages, this capture is allowed and leads to an unexpected result: the three lambdas share the same variable [i] and all print 3, its value, at the end of the loop. Java eliminates this pitfall at compile time. The solution is to declare a variable [copy] within the loop body—a new one for each iteration that is never modified (line 40)—so that each lambda captures its own;
- lines 49–51: with the “for each element” loop [for], the problem does not arise: the variable [j] is a new variable on each iteration, never modified, and therefore effectively final;
- lines 44 and 53: the [forEach] method of the collections expects a [Consumer] and executes it on each element. Here, each element is itself a
*[Runnable]*, and the reference **[Runnable::run]***(any object of type*[Runnable]*, on which*[run]is called) executes it.
The rule regarding effectively final variables may seem restrictive. However, it has a solid justification: a lambda may be executed later, or in another thread (Chapter 8). If it could modify the local variables of a method that may have already finished, or read them while another thread is modifying them, we would encounter behavior that is difficult to predict. By requiring that variables remain unchanged, Java prevents these situations; when a shared, mutable state is truly necessary, it is made explicit using an object ([AtomicInteger], collection…) whose behavior with respect to threads is specified.
7.4. Functional Interfaces or Interfaces?
In many languages, the concept of a “function passed as a parameter” (function pointer in C, delegate in C#) is distinct from the concept of an interface, and one might wonder which to choose. In Java, the question is framed differently, because the two concepts are one and the same: a lambda is an instance of an interface. Let’s take the example [Lambdas07.java], which is similar to [Lambdas02.java]:
Results:
On line 15, the method *[execute] expects a parameter of the type defined by the functional interface *[Operation] on line 34. This allows us to pass different functions to it: static methods from our class [Methods] (lines 5 and 7), a method from the Java library with the correct form (line 9: among the overloads of *[Math.max]*, the compiler chooses ***[int max(int a, int b)]*), and even a lambda (line 11): it was not necessary to write a multiplication method. This polymorphism is that of interfaces, covered in Chapter 4: the program [Lambdas08.java] demonstrates this by implementing the same interface with classes:
Results:
Comments:
- Lines 31–33: The [Operation] interface defines a method [calculate]. It is not annotated [@FunctionalInterface]: The annotation is merely a check, and any interface with a single abstract method is functional;
- lines 41–47 and 50–56: the classes
*[Add]and*[Subtract]implement the interface*[Operation]*; - lines 26–28: the method [execute], whose first parameter is of the type of the interface [Operation]. It receives, in succession, an instance of the [Add] class, an instance of the [Subtract] class (lines 5 and 7), and then a lambda (line 9). For [execute], these three objects are of the same type: objects that implement [Operation];
- Lines 36–38: The [NamedOperation] interface inherits the [calculate] method and adds a [name] method. With two abstract methods, it is no longer functional: it can no longer be implemented using a lambda; a class is required—in this case, an anonymous class (lines 11–21).
These two examples show that in Java, you don’t have to choose between two mechanisms: a method that expects a single-method interface accepts objects from classes written for the occasion, method references, and lambdas alike. The real question, then, is: should the interface be functional, with a single method, or should it have multiple methods?
- A single-method interface—whether predefined ([Function], [Predicate]…) or not—proves to be more flexible to use: any existing method with the correct form will do, and a lambda even allows you to write the function right at the point of the call. There is no class to write. This flexibility explains why modern libraries (the API Stream, the asynchronous programming in Chapter 9, Spring) make extensive use of functional interfaces;
- an interface with multiple methods regains the advantage as soon as the behavior to be implemented involves several interrelated operations ([calculate] and [name] above), or when it requires its own state and identity.
This is the case for the [ITaxDao] interface in the [dao] layer from Chapter 6: although it has only one method, [List<TaxBracket> getTaxBrackets()], its implementations have state (the brackets read, the file name), are created by the dependency injection container, and represent a clearly named role within the application. A [Supplier<List<TaxBracket>>] would technically have sufficed, but it would have been much less descriptive. We therefore reserve functional interfaces for short, one-off behaviors (a criterion, a transformation, an action), and “business” interfaces for the application’s roles, regardless of the number of methods they contain.
7.5. Event Handling
7.5.1. The Listener Model
Functional interfaces are used to set up events. Java does not have a dedicated keyword for events: it uses a set of conventions, known as the “listener model,” established as early as Java 1.1 for graphical components and JavaBeans. A class [C1] that wants to signal an event [Xxx] proceeds as follows:
- A [XxxEvent] class, derived from [java.util.EventObject], encapsulates the event information. [EventObject] stores the object that triggered the event—referred to as the source—which is obtained via [getSource()];
- an interface [XxxListener], which extends the empty interface [java.util.EventListener] (a simple marker), defines the method(s) that will be called when the event occurs. With a single method, it is a functional interface: a listener can be a lambda;
- The [C1] class manages a list of listeners and provides two public methods for subscribing to and unsubscribing from it: [addXxxListener(XxxListener l)] and [removeXxxListener(XxxListener l)];
- When a [c1] instance of [C1] wants to signal the event, it creates a [XxxEvent] object and calls the method of each registered listener, passing this object to it. By convention, this notification is handled by a protected method [fireXxx(XxxEvent e)] (“fire”), which a derived class can override to modify how the event is published;
- if a [c2] object wants to be notified of the [Xxx] event from the [c1] object, it registers itself using [c1.addXxxListener(...)]. It can unsubscribe via [c1.removeXxxListener(...)] when it no longer wishes to be notified. Since [c1] manages a list, different objects can subscribe to be notified of the same event.
In this scenario, we have:
- a class that signals (also called “publishes” or “emits”) an event;
- objects that are notified of this event. They are said to subscribe to or register for the event: these are the listeners, or event handlers;
- a listener interface that defines the signature of the methods that will be notified of the event.
This is the Observer design pattern: the emitter does not know its subscribers; it simply notifies them. This model offers the same protection as a dedicated keyword: the list of listeners is a private field of the emitter. From the outside, one can only subscribe or unsubscribe; one cannot clear the list, replace other listeners, or trigger the event in place of the emitter.
7.5.2. An example: excessively high temperature
Consider the following example:
- A class named [Emitter] encapsulates a temperature. When this temperature exceeds a certain threshold, an event must be triggered. We’ll call this event [TemperatureTooHigh]. Information about this event will be encapsulated in a [TemperatureTooHighEvent] object, and the listeners will implement the [TemperatureTooHighListener] interface;
- a class named [Subscriber] registers as a listener for the aforementioned event. When it is notified of the event, it displays a message on the console. A subscriber unsubscribes automatically after receiving a specified number of alerts;
- A console program creates a publisher and two subscribers. It enters temperatures via the keyboard and stores them in the publisher. If any of them is too high, the publisher signals the [TemperatureTooHigh] event.
All these classes are grouped together in the compact source file [Lambdas09.java], which we present in three parts. First, here are the event class, the listener interface, and the class [Emitter]:
Comments:
- lines 1–28: The class [TemperatureTooHighEvent] derives from [EventObject], as per convention. Its constructor (lines 10–13) passes the event source to the constructor of [EventObject] ([super(source)]) and stores the temperature that triggered the event. We have added the event time, [time] (line 8). The fields are [final], and the class has only read-only accessors: a listener must not be able to modify the event information for subsequent listeners;
- lines 3–4: [EventObject] implements the [Serializable] interface (so that events can be logged or transmitted over a network). The compiler, when using the [-Xlint:all] option, then requires each derived class to declare a version number [serialVersionUID]. The [@Serial] annotation (Java 14) flags this specific field to the compiler;
- lines 16–19: The [getSource] method of [EventObject] returns a [Object]. We redefine it with a more specific return type, [Emitter], which Java allows (covariant return type): listeners will not have to cast the source. We cannot use a
*[record]* here: a record cannot inherit from any class, and therefore cannot inherit from*[EventObject]*; - Lines 30–34: the listener interface. It extends [EventListener] and declares a single method, [temperatureTooHigh], which receives the event object: this is a functional interface;
- line 39: the temperature threshold above which the [TemperatureTooHigh] event is triggered;
- line 41: the emitter has a name for identification;
- line 45: the list of registered listeners. This is a [CopyOnWriteArrayList] from the [java.util.concurrent] package, a list that, whenever it is modified, copies its contents into a new array. A current iteration therefore continues on the old array, even if the list is modified in the meantime. We’ll see later why this is essential here. This list is the standard type for listener lists: the list is traversed for each event, but it is rarely modified;
- lines 59–65: the method [setTemperature] records the new temperature and signals the event if it exceeds the threshold;
- line 63: Publication is handled by the [fireTemperatureTooHigh] method, to which a [TemperatureTooHighEvent] object is passed; this object originates from the transmitter itself ([this]) and contains the temperature that exceeded the threshold;
- lines 68–70: Registering a listener adds it to the list. [Objects.requireNonNull] immediately rejects a listener [null], which would otherwise trigger a [NullPointerException] much later, during notification;
- lines 73–75: Unregistration removes the listener from the list. The method [remove] searches for the object to be removed using [equals]; therefore, it must be passed the same object that was registered;
- lines 78–82: The notification calls the [temperatureTooHigh] method for each registered listener, in the order in which they were registered. If there are no listeners, the loop does nothing: there are no special cases to handle.
Here is the [Subscriber] class:
Comments:
- lines 4–6: Each subscriber is identified by a name and is assigned the maximum number of alerts it accepts ([nbAlertsMax]);
- line 10: the subscriber’s listener is a reference to its private method [evtTemperatureTooHigh]. We saw with [Lambdas04.java] that two evaluations of [this::evtTemperatureTooHigh] produce two different objects. If we were to write [emitter.addTemperatureTooHighListener(this::evtTemperatureTooHigh)] into [subscribe] and then [emitter.removeTemperatureTooHighListener(this::evtTemperatureTooHigh)] into [unsubscribe], unsubscribing would not remove anything, and no error message would be displayed! So we create the listener once and store it in a field;
- lines 18–20: subscription to a publisher. This is the subscriber’s responsibility; the subscriber knows which object to remove to unsubscribe;
- lines 23–25: Unsubscribing removes the same object, [listener]. Each subscriber has their own object, [listener]: [remove] removes only the one belonging to the subscriber in question;
- line 28: the event handler. It has the form specified by the [TemperatureTooHighListener] interface. It is private: only the subscriber can register it with a publisher. Another very common approach is to have the [Subscriber] class itself ([class Subscriber implements TemperatureTooHighListener]) implement the listener interface and register with [this]. The drawback of this approach is that it makes the [temperatureTooHigh] method public, which anyone could then call;
- line 29: thanks to the redefinition of [getSource], the event source is directly of type [Emitter];
- lines 30–31: the handler displays the subscriber’s name, the name of the sender that reported the event, and the temperature that triggered it. The [%.1f] format displays the real number with one decimal place, here with a decimal point;
- Lines 33–37: After the [nbAlertsMax] alerts, the subscriber unsubscribes from the publisher. The subscriber does this during the event notification, from its own handler—that is, while the [fireTemperatureTooHigh] method iterates through the list of listeners. This is safe thanks to [CopyOnWriteArrayList]: [remove] creates a new array, but the current iteration continues on the old one.
Here, finally, is the main program:
Results:
Comments:
- line 5: creation of the sender;
- lines 7–10: creation of two subscribers, [s0] (up to 3 alerts) and [s1] (1 alert), which are registered with the sender;
- lines 12–13: a third listener, a lambda, which acts as a log. The type of the parameter [e] is inferred from that of the parameter [addTemperatureTooHighListener]. The format [%tT] displays a time in the form [HH:mm:ss]. This lambda cannot be unregistered: it would need to have been stored in a variable in order to pass it to [removeTemperatureTooHighListener]. Therefore, lambdas written directly in the registration call are reserved for listeners that remain registered as long as the sender exists;
- lines 16–31: loop for entering temperatures via the keyboard. [IO.readln(prompt)] displays the prompt and reads a line; it returns [null] if standard input is closed, which is treated as an empty entry (line 19);
- line 24: [Double.parseDouble] accepts only the decimal point. To allow a French user to type [22,5], the comma is replaced with a period. If the input is not a number, [parseDouble] triggers a [NumberFormatException], which is intercepted on line 27;
- line 26: if the entered temperature is valid, it is transmitted to the [e1] transmitter, which signals the [TemperatureTooHigh] event if it is greater than 19 °C.
The results show (the standard input was fed by a file, which explains why all hours are identical):
- line 2: 17 °C does not exceed the threshold: no event;
- lines 3–7: 21 °C triggers the event. The three listeners are notified in the order in which they were registered. [s1], which accepts only one alert, unsubscribes immediately, but this does not prevent the log, registered after it, from being notified;
- lines 10–12: [s1] is no longer notified;
- lines 13–16: [s0] receives its third alert and unsubscribes in turn;
- line 18: only the log remains.
What would happen if there were simply a [ArrayList] on line 45 of the first excerpt, in the [Emitter] class? We tested this: when [s1] unsubscribes while the list is being processed, the subsequent items are skipped, and the processing stops immediately. The log is not notified of temperatures 21 and 25, with no error message. In other configurations, the iteration triggers a [ConcurrentModificationException]. A list of listeners must therefore always support modifications while it is being iterated over: the [CopyOnWriteArrayList] is designed for this purpose, and it also supports subscriptions and unsubscriptions made by other threads (Chapter 8).
7.5.3. Property Change Events
The most common type of event occurs when an object’s property changes value: another object (a graphical user interface, a cache, a log) wants to be notified of this change. The Java library provides a ready-to-use class for this purpose, [java.beans.PropertyChangeSupport], which manages the list of listeners for us. Listeners implement the functional interface [PropertyChangeListener], whose method [void propertyChange(PropertyChangeEvent evt)] receives an event containing the source, the property name, and its old and new values. The program [Lambdas10.java] uses it:
Results:
Comments:
- lines 3–4: The package [java.beans] belongs to the module [java.desktop], not to the module [java.base]: it must be explicitly imported, even in a compact source file;
- Line 33: The class [Thermometer] delegates the management of its listeners to an object [PropertyChangeSupport], constructed from the event source ([this]). The methods on lines 62–72 simply pass subscription and unsubscription events to it;
- Lines 45–49 and 55–59: Each modifier stores the old value, records the new one, and calls [firePropertyChange] with the property name and both values. [firePropertyChange] notifies listeners only if the two values are different: line 22 produces no output;
- lines 9–11: a first listener, a lambda stored in a variable so it can be unsubscribed (line 25). It is subscribed for all of the object’s properties. [e.getSource()] returns a [Object], which must be cast. The values are objects: the [double] objects have been converted to [Double], whose [toString] method uses the decimal point;
- lines 13–17: a second sensor, registered solely for the [temperature] property. It triggers an alert when the temperature exceeds 19 °C;
- lines 19–26: property changes. The name change (line 23) is reported only to the log. On line 5 of the results, the log displays the source’s new name, [lounge]: the event is reported after the change;
- lines 26–27: After unsubscribing from the log, only the alert is displayed, and only one listener remains.
Without writing an event class or a listener interface, we thus obtain a complete mechanism. This is the one used by Swing GUI components and JavaBeans; JavaFX has its own, very similar mechanism based on observable properties.
7.6. Lambdas and the API Stream
In Chapter 5, we used the methods of the API Stream ([filter], [map], [sorted]…) by passing lambda expressions to them. We now know what these parameters are: the [filter] method is declared as follows in the [java.util.stream.Stream<T>] interface: [Stream<T> filter(Predicate<? super T> predicate)].
Its parameter is a [Predicate]: any method or lambda that takes one element and returns a boolean will work. Similarly, [map] expects a [Function<? super T, ? extends R>], [sorted] expects a [Comparator], [forEach] expects a [Consumer], [reduce] expects a [BinaryOperator]… To fully understand how the Stream API works, let’s write our own filtering and transformation operations. This is the [Lambdas11.java] program:
Results:
Comments:
- Lines 7–9: The lambda from [filter] is first stored in a variable of type [Predicate<Integer>] to reveal its type. [map], in turn, receives a [Function<Integer, Integer>] that transforms each element: this yields the squares of the even numbers;
- Lines 36–44: our method [filter]. It is generic: it filters a list of elements of any type [T], using a predicate of type [Predicate<? super T>]. It iterates through the list, calls the predicate (line 39) on each element, and returns the list of selected elements;
- Line 13: call to [filter] with a lambda. The type [T] is inferred from the list: [Integer]. Line 14: a method reference is passed to it: [isOdd] (line 46) receives a [int]; the compiler adds the conversion from [Integer];
- Lines 17–20: a verbose predicate that displays each element it tests. The result (lines 5–6) shows that the flow definition does not execute anything: the message on line 21 is displayed before the first test. It is only when the terminal operation [forEach] (line 22) requests the elements that the predicate executes, and the tests alternate with the displays from [forEach]: each element passes through the entire flow before the next one enters it. This is deferred (or lazy) execution of the streams. Our method [filter], on the other hand, constructs the entire result list before returning it;
- lines 51–58: to write our own intermediate operations—which are lazy, like those in the Stream API—we use the intermediate collectors ([Gatherer], Java 24), which are the counterparts for intermediate operations of the collectors ([Collector]) used by [collect]. [Gatherer.of] receives a lambda with three parameters, called for each element in the stream: a state (unnecessary here, hence the anonymous parameter [_]), the element, and the next stage in the flow ([downstream]), to which an element is passed via [downstream.push(...)]. The lambda returns [false] to stop the stream; our functions never decide this on their own: they return the result of [push], which is equal to [false] only if the downstream component no longer wants any elements (or [true] when the filter discards an element). The filter (line 52) transmits only the elements that satisfy the predicate; the transformation (line 57) transmits the result of the function applied to each element;
- line 56: [transform] is doubly generic: [T] is the type of the received elements, and [R] is the type of the produced elements. The parameter [Function<? super T, ? extends R>] follows the rule we’ve already encountered: the function consumes [T] and produces [R];
- lines 27–30: The [gather] stream method inserts our operations into the chain, which is written the same way as for the operations of the Stream API: we keep cities with up to 5 letters, then capitalize them.
The entire API stream is built on this model: operations that receive functional interfaces and produce new streams, whose elements are calculated only upon request by the terminal operation.
7.7. Expression Trees
Some languages, such as C#, allow a lambda to be converted not into executable code, but into a data structure that describes it (an “expression tree”): a program can then parse the lambda [t -> t.getLimit() > 20000] and translate it, for example, into a clause SQL [WHERE limits > 20000] executed by the database server.
Java has no equivalent. A Java lambda is always compiled into executable code: you can call it, but you cannot examine what it does. Database access libraries therefore use other means to express a query. Hibernate, which we will cover in Chapter 11, offers two solutions: a query language similar to SQL but expressed in terms of classes and properties, HQL (["from Bracket t where t.limit > 20000"]), and Jakarta Persistence’s API Criteria, which constructs the query through method calls ([cb.gt(t.get("limit"), 20000)]). In both cases, the query is data (a string or an object tree) that Hibernate translates into SQL, whereas a lambda passed to [filter] is executed within the Java program.
7.8. Conclusion
Key takeaways from this chapter:
- A functional interface is an interface that has only one abstract method. The predefined interfaces in the [java.util.function] package ([Function], [Supplier], [Consumer], [Predicate], their two-parameter variants, and their primitive versions), along with [Runnable], [Callable], and [Comparator], usually eliminate the need to define your own; when one is defined, it is annotated as [@FunctionalInterface];
- A functional interface can be implemented by a class (named or anonymous), by a lambda expression, or by a method reference ([Class::staticMethod], [object::method], [Class::instanceMethod], [Class::new]). A lambda has no type of its own: the target type determines it;
- Lambdas capture the local variables surrounding them, provided that those variables are actually final; mutable state is handled through an object or a field;
- the methods [andThen], [compose], [and], [negate], [comparing], [thenComparing]… build new functions from existing ones;
- In Java, a lambda is an instance of an interface: a functional interface is chosen for short, one-time behavior, while an interface with multiple methods (or a “business” interface) is chosen for an application role;
- An event is set up using the listener pattern: a class [XxxEvent] derived from [EventObject], an interface [XxxListener], methods [addXxxListener] / [removeXxxListener], and a list of listeners ([CopyOnWriteArrayList]) iterated over by a method [fireXxx]. To unsubscribe a listener, you must retain the subscribed object. [PropertyChangeSupport] provides this mechanism out of the box for property changes;
- the API Stream is built on operations that accept functional interfaces; Java does not have expression trees, and Hibernate expresses its queries in HQL or using the API Criteria.
Lambdas will reappear in all subsequent chapters: a thread executes a [Runnable], and a [ExecutorService] executes a [Callable] (Chapter 8), a [CompletableFuture] chains functions (Chapter 9), and Spring Boot web services handle HTTP requests in methods that the framework calls as listeners (Chapter 13).