Skip to content

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:

// Java's predefined functional interfaces

void main() {
    // 1 - Runnable: no parameter, no result: void run()
    Runnable task = new Runnable() {
        @Override
        public void run() {
            Functions.display();
        }
    };
    // short form: a method reference
    task = Functions::display;
    // execution: we call the single abstract method of the interface
    task.run();

    // 2 - BiFunction<T, U, R>: two parameters, one result
    BiFunction<Integer, Integer, Integer> operation = Functions::add;
    IO.println("operation.apply(2, 3)=" + operation.apply(2, 3));
    // Supplier<T>: no parameter, one result
    Supplier<String> greet = Functions::greet;
    IO.println("greet.get()=" + greet.get());
    // primitive version: IntBinaryOperator, int applyAsInt(int, int), without boxing
    IntBinaryOperator multiply = Functions::multiply;
    IO.println("multiply.applyAsInt(2, 3)=" + multiply.applyAsInt(2, 3));

    // 3 - Consumer<T>: one parameter, no result
    Consumer<String> write = IO::println;
    write.accept("Consumer<String> executed IO.println");

    // 4 - Predicate<T>: one parameter, a boolean result
    Predicate<Integer> isEven = Functions::isEven;
    IO.println("isEven.test(8)=" + isEven.test(8));
    List<Integer> numbers = new ArrayList<>(List.of(3, 8, 1, 6, 5, 4));
    // Collection's removeIf expects a Predicate
    boolean modified = numbers.removeIf(isEven);
    IO.println("even numbers removed: " + modified + ", remaining: " + numbers);

    // 5 - Comparator<T>: two T parameters, an int result
    List<String> names = new ArrayList<>(List.of("Paul", "Mélanie", "Jo", "Sébastien"));
    names.sort(Functions::compareLengths);
    IO.println("sorted by length: " + names);

    // 6 - a lambda is an object: the instance of a generated class that implements the interface
    IO.println("class: " + operation.getClass().getName());
    IO.println("interfaces: " + Arrays.toString(operation.getClass().getInterfaces()));
}

// the referenced methods, grouped in a class
// (the implicit class of a compact source file cannot be named)
class Functions {

    static void display() {
        IO.println("display() executed by Runnable.run()");
    }

    static int add(int n1, int n2) {
        return n1 + n2;
    }

    static int multiply(int n1, int n2) {
        return n1 * n2;
    }

    static String greet() {
        return "Hello";
    }

    static boolean isEven(int n) {
        return n % 2 == 0;
    }

    static int compareLengths(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
}

Results:

> java Lambdas01.java
display() executed by Runnable.run()
operation.apply(2, 3)=5
greet.get()=Hello
multiply.applyAsInt(2, 3)=6
Consumer<String> executed IO.println
isEven.test(8)=true
even numbers removed: true, remaining: [3, 1, 5]
sorted by length: [Jo, Paul, Mélanie, Sébastien]
class: Lambdas01$$Lambda/0x0000000020165a28
interfaces: [interface java.util.function.BiFunction]

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]:

// defining your own functional interfaces

void main() {
    // a variable of type Operation receives the static method increase
    Operation op = Calculations::increase;
    // we execute the operation
    int n = op.calculate(4, 7);
    IO.println("n=" + n);

    // creation of an object c1 of type Calculations
    var c1 = new Calculations();
    // we store in op the add method of the object c1
    op = c1::add;
    n = op.calculate(2, 3);
    IO.println("n=" + n);
    // then the subtract method of c1
    op = c1::subtract;
    n = op.calculate(2, 3);
    IO.println("n=" + n);

    // a variable references only one function: to execute several, we use a list
    List<Operation> operations = new ArrayList<>();
    operations.add(c1::add);
    operations.add(c1::subtract);
    operations.add((n1, n2) -> n1 * n2);
    for (Operation operation : operations) {
        IO.println("-> " + operation.calculate(10, 4));
    }

    // a default method of the interface builds a new operation
    Operation squaredSum = ((Operation) c1::add).then(x -> x * x);
    IO.println("squaredSum.calculate(1, 2)=" + squaredSum.calculate(1, 2));

    // Operation and IntBinaryOperator have the same shape but are two different types
    IntBinaryOperator other = op::calculate;
    IO.println("other.applyAsInt(5, 1)=" + other.applyAsInt(5, 1));

    // a generic functional interface
    Transformation<String, Integer> length = String::length;
    Transformation<Integer, String> stars = "*"::repeat;
    IO.println("length(\"Hello\")=" + length.transform("Hello"));
    IO.println("stars(5)=" + stars.transform(5));
}

// definition of a functional interface: a single abstract method
// accepting two integers as parameters and returning an integer
@FunctionalInterface
interface Operation {
    int calculate(int n1, int n2);

    // a default method does not count: the interface remains functional
    default Operation then(IntUnaryOperator next) {
        return (n1, n2) -> next.applyAsInt(calculate(n1, n2));
    }
}

// a generic functional interface: T -> R
@FunctionalInterface
interface Transformation<T, R> {
    R transform(T value);
}

// a class whose methods have the shape of the Operation interface
class Calculations {
    // two instance methods
    int add(int n1, int n2) {
        IO.println("add(" + n1 + "," + n2 + ")");
        return n1 + n2;
    }

    int subtract(int n1, int n2) {
        IO.println("subtract(" + n1 + "," + n2 + ")");
        return n1 - n2;
    }

    // a static method
    static int increase(int n1, int n2) {
        IO.println("increase(" + n1 + "," + n2 + ")");
        return n1 + 2 * n2;
    }
}

Results:

> java Lambdas02.java
increase(4,7)
n=18
add(2,3)
n=5
subtract(2,3)
n=-1
add(10,4)
-> 14
subtract(10,4)
-> 6
-> 40
add(1,2)
squaredSum.calculate(1, 2)=9
subtract(5,1)
other.applyAsInt(5, 1)=4
length("Hello")=5
stars(5)=*****

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):

> java Lambdas02.java
Lambdas02.java:5: error: incompatible types: Operation is not a functional interface
    Operation op = Calculations::increase;
                   ^
    multiple non-overriding abstract methods found in interface Operation
...
Lambdas02.java:47: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Operation is not a functional interface
    multiple non-overriding abstract methods found in interface Operation
9 errors
error: compilation failed

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:

// from anonymous classes to lambda expressions

void main() {
    // 1 - an anonymous class: the class implementing the interface is written in place
    IntBinaryOperator add = new IntBinaryOperator() {
        @Override
        public int applyAsInt(int n1, int n2) {
            return n1 + n2;
        }
    };
    IO.println("anonymous class: add(2, 3)=" + add.applyAsInt(2, 3));

    // 2 - the same thing with a lambda expression (Java 8): parameters -> body
    IntBinaryOperator subtract = (int n1, int n2) -> {
        return n1 - n2;
    };
    IO.println("block-body lambda: subtract(2, 3)=" + subtract.applyAsInt(2, 3));

    // 3 - the parameter types are inferred from the target interface
    IntBinaryOperator multiply = (n1, n2) -> n1 * n2;
    IO.println("expression-body lambda: multiply(2, 3)=" + multiply.applyAsInt(2, 3));

    // 4 - a single parameter: the parentheses are optional
    IntUnaryOperator square = x -> x * x;
    Predicate<String> isEmpty = s -> s.isEmpty();
    IO.println("square(7)=" + square.applyAsInt(7) + ", isEmpty(\"\")=" + isEmpty.test(""));

    // 5 - no parameter: empty parentheses
    Supplier<String> greet = () -> "Hello";
    Runnable beep = () -> IO.println("beep!");
    IO.println(greet.get());
    beep.run();

    // 6 - a block body with several statements
    BiConsumer<String, Integer> repeat = (text, times) -> {
        for (int i = 0; i < times; i++) {
            IO.print(text);
        }
        IO.println();
    };
    repeat.accept("ab", 3);

    // 7 - unnamed parameters (Java 22): we do not need their values
    BiConsumer<Object, String> listener = (_, _) -> IO.println("event received");
    listener.accept(null, "click");

    // 8 - a lambda passed directly as a method parameter
    List<Integer> numbers = new ArrayList<>(List.of(3, 8, 1, 6, 5, 4));
    List<Integer> evens = new ArrayList<>(numbers);
    evens.removeIf(n -> n % 2 != 0);
    IO.println("evens: " + evens);
    numbers.sort((n1, n2) -> Integer.compare(n2, n1));
    IO.println("descending order: " + numbers);
}

Results:

> java Lambdas03.java
anonymous class: add(2, 3)=5
block-body lambda: subtract(2, 3)=-1
expression-body lambda: multiply(2, 3)=6
square(7)=49, isEmpty("")=true
Hello
beep!
ababab
event received
evens: [8, 6, 4]
descending order: [8, 6, 5, 4, 3, 1]

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:

// more on lambdas and method references

void main() throws Exception {
    // 1 - a lambda has no type by itself: the context determines it
    // var twice = x -> x * 2;  // rejected: the lambda needs a target type
    var twice = (IntUnaryOperator) x -> x * 2;
    IO.println("twice(21)=" + twice.applyAsInt(21));
    // the same lambda text can implement two different interfaces
    Supplier<String> supplier = () -> "Hello";
    Callable<String> callable = () -> "Hello";
    IO.println(supplier.get() + " / " + callable.call());

    // 2 - parameters declared with var (Java 11): allows annotating them or making them final
    BinaryOperator<String> concatenate = (final var s1, final var s2) -> s1 + s2;
    IO.println(concatenate.apply("lam", "bda"));

    // 3 - the four kinds of method references
    // a) static method: Class::staticMethod
    Function<String, Integer> convert = Integer::parseInt;
    IO.println("convert(\"42\") + 1 = " + (convert.apply("42") + 1));
    // b) instance method of a given object: object::method
    String name = "Pierre";
    Supplier<String> inUpperCase = name::toUpperCase;
    IO.println("inUpperCase.get()=" + inUpperCase.get());
    // c) instance method of an arbitrary object: Class::instanceMethod
    //    the object on which the method is called becomes the first parameter
    UnaryOperator<String> upperCase = String::toUpperCase;
    Comparator<String> ignoreCase = String::compareToIgnoreCase;
    IO.println("upperCase(\"paul\")=" + upperCase.apply("paul")
            + ", ignoreCase(\"abc\", \"ABC\")=" + ignoreCase.compare("abc", "ABC"));
    // d) constructor: Class::new
    Supplier<List<String>> createList = ArrayList::new;
    Function<String, StringBuilder> createBuffer = StringBuilder::new;
    IntFunction<int[]> createArray = int[]::new;
    List<String> list = createList.get();
    list.add("element");
    IO.println(list + " " + createBuffer.apply("abc").reverse() + " " + createArray.apply(3).length);

    // 4 - checked exceptions: Callable.call can throw them, Supplier.get cannot
    Callable<Integer> read = () -> {
        throw new IOException("missing file");
    };
    try {
        read.call();
    } catch (IOException ex) {
        IO.println("exception received: " + ex.getMessage());
    }

    // 5 - each evaluation of a lambda or a method reference may create a new object
    Supplier<String> r1 = name::toUpperCase;
    Supplier<String> r2 = name::toUpperCase;
    IO.println("r1 == r2: " + (r1 == r2) + ", r1.equals(r2): " + r1.equals(r2));
}

Results:

> java Lambdas04.java
twice(21)=42
Hello / Hello
lambda
convert("42") + 1 = 43
inUpperCase.get()=PIERRE
upperCase("paul")=PAUL, ignoreCase("abc", "ABC")=0
[element] cba 3
exception received: missing file
r1 == r2: false, r1.equals(r2): false

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:

// composing functions: andThen, compose, and, or, negate, comparing, thenComparing

void main() {
    // 1 - Function: andThen (first this, then the other) and compose (first the other)
    Function<Integer, Integer> plusOne = x -> x + 1;
    Function<Integer, Integer> timesTwo = x -> x * 2;
    IO.println("plusOne.andThen(timesTwo).apply(5)=" + plusOne.andThen(timesTwo).apply(5));
    IO.println("plusOne.compose(timesTwo).apply(5)=" + plusOne.compose(timesTwo).apply(5));
    // a processing chain built from a list of functions
    List<UnaryOperator<String>> steps =
            List.of(String::strip, String::toLowerCase, s -> s.replace(' ', '-'));
    Function<String, String> normalize = Function.identity();
    for (UnaryOperator<String> step : steps) {
        normalize = normalize.andThen(step);
    }
    IO.println("normalize(\"  My Nice Title \")=" + normalize.apply("  My Nice Title "));

    // 2 - Predicate: and, or, negate, Predicate.not
    Predicate<String> isShort = s -> s.length() <= 4;
    Predicate<String> startsWithP = s -> s.startsWith("P");
    List<String> firstNames = List.of("Paul", "Mélanie", "Jo", "Sébastien", "Pierre");
    IO.println("short and P: " + filter(firstNames, isShort.and(startsWithP)));
    IO.println("short or P: " + filter(firstNames, isShort.or(startsWithP)));
    IO.println("not short: " + filter(firstNames, isShort.negate()));
    IO.println("not empty: " + filter(List.of("a", "", "b"), Predicate.not(String::isEmpty)));

    // 3 - Consumer: andThen chains two processings on the same value
    List<String> log = new ArrayList<>();
    Consumer<String> display = s -> IO.println("display: " + s);
    Consumer<String> remember = log::add;
    Consumer<String> both = display.andThen(remember);
    both.accept("first");
    both.accept("second");
    IO.println("log: " + log);

    // 4 - Comparator: comparing, thenComparing, reversed
    List<Person> persons = new ArrayList<>(List.of(new Person("Paul", 32),
            new Person("Mélanie", 25), new Person("Jo", 32), new Person("Sébastien", 25)));
    persons.sort(Comparator.comparing(Person::age).thenComparing(Person::name));
    IO.println("by age then name: " + persons);
    persons.sort(Comparator.comparingInt(Person::age).reversed()
            .thenComparing(Person::name, Comparator.reverseOrder()));
    IO.println("by descending age then descending name: " + persons);
    // BinaryOperator.minBy builds a function from a comparator
    BinaryOperator<Person> youngest = BinaryOperator.minBy(Comparator.comparingInt(Person::age));
    IO.println("youngest(Paul, Mélanie)=" + youngest.apply(persons.get(0), persons.get(3)));
}

// keeps the elements that satisfy the predicate
static <T> List<T> filter(List<T> elements, Predicate<? super T> predicate) {
    List<T> result = new ArrayList<>();
    for (T element : elements) {
        if (predicate.test(element)) {
            result.add(element);
        }
    }
    return result;
}

record Person(String name, int age) {
    @Override
    public String toString() {
        return name + "(" + age + ")";
    }
}

Results:

> java Lambdas05.java
plusOne.andThen(timesTwo).apply(5)=12
plusOne.compose(timesTwo).apply(5)=11
normalize("  My Nice Title ")=my-nice-title
short and P: [Paul]
short or P: [Paul, Jo, Pierre]
not short: [Mélanie, Sébastien, Pierre]
not empty: [a, b]
display: first
display: second
log: [first, second]
by age then name: [Mélanie(25), Sébastien(25), Jo(32), Paul(32)]
by descending age then descending name: [Paul(32), Jo(32), Sébastien(25), Mélanie(25)]
youngest(Paul, Mélanie)=Mélanie(25)

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:

// variable capture and closures

// a field of the implicit class: lambdas can modify it
int nbCalls;

void main() {
    // 1 - a lambda can use the local variables that surround it...
    int threshold = 5;
    Predicate<Integer> exceeds = n -> n > threshold;
    IO.println("threshold=" + threshold + " : exceeds(7)=" + exceeds.test(7));
    // ... provided they are effectively final: threshold = 10; would be rejected
    int otherThreshold = 10;
    Predicate<Integer> exceedsOther = n -> n > otherThreshold;
    IO.println("threshold=" + otherThreshold + " : exceeds(7)=" + exceedsOther.test(7));

    // 2 - a lambda cannot modify a captured local variable...
    // int total = 0; IntConsumer accumulate = n -> total += n;  // rejected
    // ... but it can modify the object it references
    AtomicInteger total = new AtomicInteger();
    IntConsumer accumulate = n -> total.addAndGet(n);
    accumulate.accept(3);
    accumulate.accept(4);
    IO.println("total=" + total.get());
    // ... or a field of the current object (the lambda captures this)
    Runnable count = () -> nbCalls++;
    count.run();
    count.run();
    IO.println("nbCalls=" + nbCalls);

    // 3 - a closure: the captured object outlives the method that created it
    IntSupplier counterA = createCounter();
    IntSupplier counterB = createCounter();
    IO.println("A : " + counterA.getAsInt() + " " + counterA.getAsInt() + " " + counterA.getAsInt());
    IO.println("B : " + counterB.getAsInt() + " " + counterB.getAsInt());

    // 4 - the for loop: the variable i changes, it cannot be captured
    List<Runnable> actions = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        // actions.add(() -> IO.print(i + " "));  // rejected: i is not effectively final
        int copy = i;
        actions.add(() -> IO.print(copy + " "));
    }
    IO.print("for, capture of a copy: ");
    actions.forEach(Runnable::run);
    IO.println();

    // the for-each loop declares a new variable at each iteration: it is effectively final
    actions.clear();
    for (int j : new int[] { 0, 1, 2 }) {
        actions.add(() -> IO.print(j + " "));
    }
    IO.print("for-each: ");
    actions.forEach(Runnable::run);
    IO.println();
}

// a method that returns a function
static IntSupplier createCounter() {
    // local object captured by the returned lambda
    AtomicInteger value = new AtomicInteger();
    return () -> value.incrementAndGet();
}

Results:

1
2
3
4
5
6
7
8
9
> java Lambdas06.java
threshold=5 : exceeds(7)=true
threshold=10 : exceeds(7)=false
total=7
nbCalls=2
A : 1 2 3
B : 1 2
for, capture of a copy: 0 1 2 
for-each: 0 1 2 

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]:

// a parameter of a functional interface type

void main() {
    // execution of the add method
    IO.println(execute(Methods::add, 2, 3));
    // execution of the subtract method
    IO.println(execute(Methods::subtract, 2, 3));
    // execution of a Java library method having the right shape
    IO.println(execute(Math::max, 2, 3));
    // execution of a lambda: no named method is needed
    IO.println(execute((n1, n2) -> n1 * n2, 2, 3));
}

// execution of an operation received as a parameter
static int execute(Operation op, int n1, int n2) {
    return op.calculate(n1, n2);
}

// two methods having the shape of the Operation interface
class Methods {
    static int add(int n1, int n2) {
        IO.println("add(" + n1 + "," + n2 + ")");
        return n1 + n2;
    }

    static int subtract(int n1, int n2) {
        IO.println("subtract(" + n1 + "," + n2 + ")");
        return n1 - n2;
    }
}

// the functional interface
@FunctionalInterface
interface Operation {
    int calculate(int n1, int n2);
}

Results:

1
2
3
4
5
6
7
> java Lambdas07.java
add(2,3)
5
subtract(2,3)
-1
3
6

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:

// classes that implement the functional interface

void main() {
    // execution of the Add operation
    IO.println(execute(new Add(), 2, 3));
    // execution of the Subtract operation
    IO.println(execute(new Subtract(), 2, 3));
    // the same execute method still accepts lambdas
    IO.println(execute((n1, n2) -> n1 * n2, 2, 3));
    // an interface with two abstract methods no longer accepts a lambda: a class is needed
    NamedOperation division = new NamedOperation() {
        @Override
        public int calculate(int n1, int n2) {
            return n1 / n2;
        }

        @Override
        public String name() {
            return "integer division";
        }
    };
    IO.println(division.name() + " : " + division.calculate(7, 2));
}

// execution of the single method of the Operation interface
static int execute(Operation op, int n1, int n2) {
    return op.calculate(n1, n2);
}

// interface Operation
interface Operation {
    int calculate(int n1, int n2);
}

// an interface that is not functional: two abstract methods
interface NamedOperation extends Operation {
    String name();
}

// class Add
class Add implements Operation {
    @Override
    public int calculate(int n1, int n2) {
        IO.println("Add(" + n1 + "," + n2 + ")");
        return n1 + n2;
    }
}

// class Subtract
class Subtract implements Operation {
    @Override
    public int calculate(int n1, int n2) {
        IO.println("Subtract(" + n1 + "," + n2 + ")");
        return n1 - n2;
    }
}

Results:

1
2
3
4
5
6
7
> java Lambdas08.java
Add(2,3)
5
Subtract(2,3)
-1
6
integer division : 3

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]:

// the information carried by the event
class TemperatureTooHighEvent extends EventObject {
    @Serial
    private static final long serialVersionUID = 1L;
    // temperature at the time of the event
    private final double temperature;
    // time of the event
    private final LocalTime time = LocalTime.now();

    TemperatureTooHighEvent(Emitter source, double temperature) {
        super(source);
        this.temperature = temperature;
    }

    // the source is necessarily a Emitter: we override getSource with a more precise type
    @Override
    public Emitter getSource() {
        return (Emitter) super.getSource();
    }

    double getTemperature() {
        return temperature;
    }

    LocalTime getTime() {
        return time;
    }
}

// the interface that the event listeners must implement
@FunctionalInterface
interface TemperatureTooHighListener extends EventListener {
    void temperatureTooHigh(TemperatureTooHighEvent e);
}

// the class that signals the event
class Emitter {
    // threshold that triggers the event
    static final double THRESHOLD = 19;
    // name of the emitter
    private final String name;
    // observed temperature
    private double temperature;
    // the registered listeners
    private final List<TemperatureTooHighListener> listeners = new CopyOnWriteArrayList<>();

    Emitter(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }

    double getTemperature() {
        return temperature;
    }

    void setTemperature(double temperature) {
        this.temperature = temperature;
        if (temperature > THRESHOLD) {
            // we signal the event to the listeners
            fireTemperatureTooHigh(new TemperatureTooHighEvent(this, temperature));
        }
    }

    // registration of a listener
    void addTemperatureTooHighListener(TemperatureTooHighListener listener) {
        listeners.add(Objects.requireNonNull(listener));
    }

    // unregistration of a listener
    void removeTemperatureTooHighListener(TemperatureTooHighListener listener) {
        listeners.remove(listener);
    }

    // notification of the event to all the registered listeners
    protected void fireTemperatureTooHigh(TemperatureTooHighEvent e) {
        for (TemperatureTooHighListener listener : listeners) {
            listener.temperatureTooHigh(e);
        }
    }
}

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:

// a class that registers as a listener of the event
class Subscriber {
    // name of the subscriber
    private final String name;
    // maximum number of alerts accepted
    private final int nbAlertsMax;
    // number of alerts received
    private int nbAlerts;
    // the listener, created only once so that it can be unregistered
    private final TemperatureTooHighListener listener = this::evtTemperatureTooHigh;

    Subscriber(String name, int nbAlertsMax) {
        this.name = name;
        this.nbAlertsMax = nbAlertsMax;
    }

    // registration with an emitter
    void subscribe(Emitter emitter) {
        emitter.addTemperatureTooHighListener(listener);
    }

    // unregistration
    void unsubscribe(Emitter emitter) {
        emitter.removeTemperatureTooHighListener(listener);
    }

    // handler of the TemperatureTooHigh event
    private void evtTemperatureTooHigh(TemperatureTooHighEvent e) {
        Emitter emitter = e.getSource();
        IO.println("Subscriber [%s]: the source [%s] reported a temperature too high: [%.1f]"
                .formatted(name, emitter.getName(), e.getTemperature()));
        // beyond nbAlertsMax alerts, the subscriber unsubscribes
        nbAlerts++;
        if (nbAlerts == nbAlertsMax) {
            unsubscribe(emitter);
            IO.println("Subscriber [%s]: unsubscribed after %d alert(s)".formatted(name, nbAlerts));
        }
    }
}

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:

// event handling: an emitter and listeners

void main() {
    // creation of an event emitter
    var e1 = new Emitter("e");
    // creation of two subscribers that register with e1
    List<Subscriber> subscribers = List.of(new Subscriber("s0", 3), new Subscriber("s1", 1));
    for (Subscriber subscriber : subscribers) {
        subscriber.subscribe(e1);
    }
    // a third listener: a lambda
    e1.addTemperatureTooHighListener(
            e -> IO.println("Log: %.1f °C at %tT".formatted(e.getTemperature(), e.getTime())));

    // we read the temperatures from the keyboard
    while (true) {
        String input = IO.readln("Temperature (nothing to stop): ");
        // end of input? (readln returns null if the standard input is closed)
        if (input == null || input.isBlank()) {
            break;
        }
        try {
            // we accept the decimal comma
            double temperature = Double.parseDouble(input.strip().replace(',', '.'));
            // correct temperature - we record it in the emitter
            e1.setTemperature(temperature);
        } catch (NumberFormatException ex) {
            // we report the error
            IO.println("Incorrect temperature");
        }
    }
}

Results:

> java Lambdas09.java
Temperature (nothing to stop): 17
Temperature (nothing to stop): 21
Subscriber [s0]: the source [e] reported a temperature too high: [21.0]
Subscriber [s1]: the source [e] reported a temperature too high: [21.0]
Subscriber [s1]: unsubscribed after 1 alert(s)
Log: 21.0 °C at 18:58:41
Temperature (nothing to stop): abc
Incorrect temperature
Temperature (nothing to stop): 22,5
Subscriber [s0]: the source [e] reported a temperature too high: [22.5]
Log: 22.5 °C at 18:58:41
Temperature (nothing to stop): 25
Subscriber [s0]: the source [e] reported a temperature too high: [25.0]
Subscriber [s0]: unsubscribed after 3 alert(s)
Log: 25.0 °C at 18:58:41
Temperature (nothing to stop): 30
Log: 30.0 °C at 18:58:41
Temperature (nothing to stop): 

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:

// property change events: java.beans.PropertyChangeSupport

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

void main() {
    var thermometer = new Thermometer("living room");
    // a listener of all the properties
    PropertyChangeListener log = e -> IO.println("Log: %s.%s : %s -> %s".formatted(
            ((Thermometer) e.getSource()).getName(), e.getPropertyName(), e.getOldValue(), e.getNewValue()));
    thermometer.addPropertyChangeListener(log);
    // a listener of the temperature property only
    thermometer.addPropertyChangeListener("temperature", e -> {
        if ((double) e.getNewValue() > 19) {
            IO.println("Alert: temperature too high");
        }
    });

    thermometer.setTemperature(18);
    thermometer.setTemperature(21.5);
    // same value: no event
    thermometer.setTemperature(21.5);
    thermometer.setName("lounge");
    // unregistration of the log
    thermometer.removePropertyChangeListener(log);
    thermometer.setTemperature(22);
    IO.println("remaining listeners: " + thermometer.getNbListeners());
}

// a class whose properties signal their changes
class Thermometer {
    // listener management is delegated to a PropertyChangeSupport object
    private final PropertyChangeSupport changes = new PropertyChangeSupport(this);
    private String name;
    private double temperature;

    Thermometer(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }

    void setName(String name) {
        String old = this.name;
        this.name = name;
        changes.firePropertyChange("name", old, name);
    }

    double getTemperature() {
        return temperature;
    }

    void setTemperature(double temperature) {
        double old = this.temperature;
        this.temperature = temperature;
        changes.firePropertyChange("temperature", old, temperature);
    }

    // registration and unregistration of the listeners
    void addPropertyChangeListener(PropertyChangeListener listener) {
        changes.addPropertyChangeListener(listener);
    }

    void addPropertyChangeListener(String property, PropertyChangeListener listener) {
        changes.addPropertyChangeListener(property, listener);
    }

    void removePropertyChangeListener(PropertyChangeListener listener) {
        changes.removePropertyChangeListener(listener);
    }

    int getNbListeners() {
        return changes.getPropertyChangeListeners().length;
    }
}

Results:

1
2
3
4
5
6
7
> java Lambdas10.java
Log: living room.temperature : 0.0 -> 18.0
Log: living room.temperature : 18.0 -> 21.5
Alert: temperature too high
Log: lounge.name : living room -> lounge
Alert: temperature too high
remaining listeners: 1

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:

// lambdas and the Stream API

void main() {
    List<Integer> numbers = List.of(3, 8, 1, 6, 5, 4, 9, 2);

    // 1 - filter and map expect a Predicate<T> and a Function<T, R>
    Predicate<Integer> isEven = n -> n % 2 == 0;
    Function<Integer, Integer> square = n -> n * n;
    List<Integer> evenSquares = numbers.stream().filter(isEven).map(square).toList();
    IO.println("Stream: " + evenSquares);

    // 2 - our own filtering method, called with a lambda then with a method reference
    IO.println("filter(n > 4): " + Tools.filter(numbers, n -> n > 4));
    IO.println("filter(isOdd): " + Tools.filter(numbers, Tools::isOdd));

    // 3 - execution is deferred: the predicate is only called by the terminal operation
    Stream<Integer> large = numbers.stream().filter(n -> {
        IO.print("[test " + n + "] ");
        return n > 7;
    });
    IO.println("the stream is defined, nothing has been executed yet");
    large.forEach(n -> IO.print("-> " + n + " "));
    IO.println();

    // 4 - our own intermediate operations with gatherers (Gatherer, Java 24)
    List<String> cities = List.of("Angers", "Paris", "Lyon", "Nantes", "Aix");
    List<String> result = cities.stream()
            .gather(Tools.filter(v -> v.length() <= 5))
            .gather(Tools.transform(String::toUpperCase))
            .toList();
    IO.println("gatherers: " + result);
}

class Tools {
    // filters a list with a predicate: the work that filter does
    static <T> List<T> filter(List<T> source, Predicate<? super T> predicate) {
        List<T> result = new ArrayList<>();
        for (T element : source) {
            if (predicate.test(element)) {
                result.add(element);
            }
        }
        return result;
    }

    static boolean isOdd(int n) {
        return n % 2 != 0;
    }

    // an intermediate stream operation that only lets through the elements satisfying the predicate
    static <T> Gatherer<T, ?, T> filter(Predicate<? super T> predicate) {
        return Gatherer.of((_, element, downstream) -> !predicate.test(element) || downstream.push(element));
    }

    // an intermediate operation that transforms each element: the equivalent of map
    static <T, R> Gatherer<T, ?, R> transform(Function<? super T, ? extends R> transformation) {
        return Gatherer.of((_, element, downstream) -> downstream.push(transformation.apply(element)));
    }
}

Results:

1
2
3
4
5
6
7
> java Lambdas11.java
Stream: [64, 36, 16, 4]
filter(n > 4): [8, 6, 5, 9]
filter(isOdd): [3, 1, 5, 9]
the stream is defined, nothing has been executed yet
[test 3] [test 8] -> 8 [test 1] [test 6] [test 5] [test 4] [test 9] -> 9 [test 2] 
gatherers: [PARIS, LYON, AIX]

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).