7. Functional Types, Lambdas, and Events
In many programs, we need to pass not data but behavior to a function: the sorting criterion, the filtering condition, or the action to be performed when something happens. Kotlin addresses this need by treating functions as values just like any other: a function has a type—the functional type, denoted, for example, as [(Int, Int) -> Int]; it can be stored in a variable, passed as a parameter, returned as the result of another function, and executed whenever desired. A function that takes or returns a function is called a higher-order function.
Java, as a reminder, does not have functional types: it relies on functional interfaces—interfaces with a single abstract method ([Runnable], [Comparator], [Function<T, R>]…), and it wasn’t until Java 8 (2014) that it allowed them to be implemented using lambda expressions and method references. Kotlin has supported lambdas since its very first version, and it can also work with Java’s functional interfaces, since it directly uses Java libraries. We’ve already used lambdas extensively without going into detail, in the collection operations covered in Chapter 5: in [persons.filter { it.age >= 18 }], we pass a lambda to the function [filter]. This chapter explains what lies behind this syntax. It concludes with events, which Kotlin—like Java—handles using “listeners”: functions, most often lambdas, that register with an emitter to be notified of what is happening there. Graphical user interfaces (Swing, JavaFX, Compose) and web frameworks alike rely on this mechanism: a button click or the arrival of a HTTP request is signaled to registered listeners. This course does not cover graphical user interfaces; all examples are console programs.
The programs in this chapter are organized in the Gradle project [07-lambdas], with one package per program: [src/main/kotlin/lambdas01/Lambdas01.kt], [src/main/kotlin/lambdas02/Lambdas02.kt]… They are run using [gradlew -q run -Pmain=lambdas01.Lambdas01Kt] or by clicking the green triangle in IntelliJ or IDEA. The project requires no libraries other than the standard Kotlin library: functional types, lambdas, and scope functions are included, and the interfaces in the [java.util.function] package are the same as those in JDK.
7.1. Functional types and predefined functional interfaces
7.1.1. The Concept of a Functional Type
A functional type describes a model (also called a signature or a form) of a function: the types of its parameters, enclosed in parentheses, followed by an arrow, and then the type of its return value. A variable of a functional type can refer to any function of this form and execute it. Here are a few examples:
- [() -> Unit]: a function with no parameters that returns no useful result ([Unit] plays the role of Java’s [void]);
- [() -> String]: a function with no parameters that returns a string;
- [(Int) -> Boolean]: a function that takes an integer and returns a boolean, in other words, a predicate;
- [(Int, Int) -> Int]: a function that takes two integers and returns an integer;
- [(String) -> (Int) -> Int]: a function that takes a string and returns… a function of type [(Int) -> Int] (the arrow is read from right to left);
- [(() -> Unit)?]: a function that may be absent; the parentheses are necessary because [() -> Unit?] would otherwise denote a function returning a [Unit?]. Here we see the safety property with respect to [null] from Chapter 3: such a function is denoted by [f?.invoke()];
- [String.(Int) -> String]: a function with a receiver; it is applied to a string—which its code denotes as [this]—as an extension function (Chapter 4). It can be called as an extension (["ab".f(3)]) or as an ordinary function whose receiver is the first parameter ([f("ab", 3)]).
Parameters can be named to document the type: [(n1: Int, n2: Int) -> Int] is the same type as [(Int, Int) -> Int].
There are three ways to obtain a value of a function type:
- write a reference to an existing function that has the correct form: [val task: () -> Unit = ::display];
- Write a lambda expression: [val task: () -> Unit = { println("task") }];
- write an anonymous function: [val task = fun() { println("task") }], or an object of a class that implements the functional type (a functional type is an interface, as we’ll see). These two notations are less common.
The function is then executed like an ordinary function: [task()]. This is a shorthand notation for calling the method [invoke] of the function object: [task.invoke()]. This is the first noticeable difference from Java, where you must call the interface’s method by name ([run], [apply], [test]…); Kotlin behaves here like C#, where a delegate is called like a method.
7.1.2. Kotlin Functional Types and java.util.function Interfaces
Before Java 8, each Java library defined its own single-method interfaces ([Runnable], [Comparator], [ActionListener]…). Java 8 introduced the [java.util.function] package, which provides 43 generic functional interfaces. Kotlin does not need them: a functional type is written directly. However, these interfaces can still be used from Kotlin, and you’ll encounter them whenever you call a Java library. The following table maps the main Java interfaces to Kotlin’s functional types:
Java Interface | Kotlin Type | Functions Represented |
[Runnable]: [void run()] | [() -> Unit] | No parameters, no results |
[Supplier<T>] : [T get()] | [() -> T] | No parameters, one result: one “supplier” |
[Consumer<T>]: [void accept(T t)] | [(T) -> Unit] | one parameter, no result: a “consumer” |
[BiConsumer<T, U>] | [(T, U) -> Unit] | two parameters, no results |
[Function<T, R>]: [R apply(T t)] | [(T) -> R] | one parameter, one result |
[BiFunction<T, U, R>] | [(T, U) -> R] | two parameters, one result |
[UnaryOperator<T>] | [(T) -> T] | Parameter and result of the same type |
[BinaryOperator<T>] | [(T, T) -> T] | Two parameters and one result of the same type |
[Predicate<T>]: [boolean test(T t)] | [(T) -> Boolean] | one parameter, a Boolean result: a “predicate” |
[BiPredicate<T, U>] | [(T, U) -> Boolean] | two parameters, a Boolean result |
[Comparator<T>]: [int compare(T o1, T o2)] | [(T, T) -> Int] | two parameters of the same type, an integer result |
[Callable<V>] : [V call()] | [() -> V] | such as [Supplier<V>]; in Kotlin, no checked exception needs to be declared |
[IntBinaryOperator], [IntPredicate]… | [(Int, Int) -> Int], [(Int) -> Boolean]… | The Java versions for primitive types |
In Kotlin, there is no practical limit to the number of parameters a function type can have: [(Int, Int, Int) -> Int] is written just as simply as the others, whereas in Java, since there is no predefined interface with three parameters, you have to define your own.
How does the Java Virtual Machine, which only recognizes classes and interfaces, represent a functional type? Through generic interfaces from the Kotlin library: [kotlin.jvm.functions.Function0<R>], [Function1<P1, R>], [Function2<P1, P2, R>]… all the way up to [Function22], followed by an interface [FunctionN] beyond that. Each has a single method, [invoke]: [(Int, Int) -> Int] is compiled into [Function2<Integer, Integer, Integer>], whose method is [operator fun invoke(p1: Int, p2: Int): Int]. It is the modifier [operator] of [invoke] (see operator overloading in Chapter 4) that allows the notation [f(2, 3)]. A functional type is therefore, for JVM, a functional interface like those in Java. Since Java generics apply only to object types, [Int] values passed to a [(Int, Int) -> Int] are converted to [Integer] and vice versa (boxing and unboxing). Java avoids these conversions with its specialized interfaces ([IntBinaryOperator]…); Kotlin avoids them in a different way, using the [inline] functions, which we’ll discuss later.
The program [Lambdas01.kt] uses these types with function references:
Results:
Comments:
- line 11: the variable [task] is of type [() -> Unit]. It is assigned a function reference: [::display] refers to the top-level function [display] declared later in the file (line 79). The compiler verifies that it has the required form (no parameters, no return value) and creates the function object that calls it. In Java, an auxiliary class would have been needed to contain these methods, since the implicit class of a compact source file cannot be named; in Kotlin, top-level functions are referenced directly;
- line 13: the function is executed by calling it as an ordinary function. The function [display] is executed (line 1 of the results). Line 15: the same result is obtained by explicitly calling the method [invoke] (line 2 of the results);
- Line 18: [(Int, Int) -> Int] refers to a function with two parameters, [Int], and returning an [Int]. The function [add] (line 83) is appropriate. The function is called via [operation(2, 3)] (line 19);
- line 21: a parameterless function returning a string, the equivalent of Java’s [Supplier<String>]. Note that the local variable [greet] has the same name as the function [greet]: in [::greet], this refers to the function, and in [greet()], it refers to the local variable, which masks the function;
- Line 25: a function with one parameter, [String], that returns nothing—the equivalent of Java’s [Consumer<String>]. The standard library function [println] is overloaded ([println(Int)], [println(Any?)]…): it is the expected type, [(String) -> Unit], that allows the compiler to choose the correct overload;
- Line 29: a predicate, equivalent to [Predicate<Integer>]. Line 33: the Kotlin library’s extension function [removeAll] specifically expects a parameter of type [(T) -> Boolean]: it removes from the mutable list all elements for which the predicate evaluates to [true], and returns [true] if it has removed at least one element (line 7 of the results);
- Line 38: The function [sortWith] expects a [Comparator<T>]. In Kotlin, [Comparator] is an alias for the Java interface [java.util.Comparator]. [Comparator(::compareLengths)] creates a [Comparator<String>] from the function [compareLengths]: this is a SAM conversion (“Single Abstract Method”), which creates the object implementing the Java interface from a Kotlin function of the same form. The function [compareLengths] (line 91) uses [compareTo], which compares two [Int] objects without the risk of capacity overflow (which a simple subtraction does not guarantee);
- lines 42–44: the interfaces of
*[java.util.function]can be used as-is. They are constructed in the same way, via an explicitSAMconversion: the interface name followed by a function reference or a lambda. They are then executed via their Java method ([get], [applyAsInt]*, [test]…), just as in Java; - lines 49–52: [removeIf] is a Java method of the [Collection] interface; it expects a [Predicate]. You can pass it a
*[Predicate](line 49), but also directly a value of type*[(Int) -> Boolean](line 51) or a lambda (line 52): When calling a Java method that expects a functional interface, Kotlin automatically performs the SAM conversion. This is what makes Java libraries just as pleasant to use from Kotlin as they are from Java. The initial list (10, 3, 8, 7, 2, 1) successively removes elements greater than 4, even elements, and then the element 1 (line 11 of the results); - lines 56–57 and 72–75: the function
*[execute]is passed a function that may be absent, of type*[(() -> Unit)?]. The safe call [callback?.invoke()] does nothing if [callback] is equal to [null]. You cannot write [callback?()]: the safe call to a nullable function goes through [invoke]. This will be the standard notation for optional listeners; - Line 60: a functional type with a receiver. The reference [String::repeat] refers to the method [repeat] of the class [String], which applies to a string and takes an integer. Line 61 is referred to in two ways: as an extension, ["ab".times(3)], or as a function with two parameters, [times("cd", 2)]. We’ll see functions with receivers again in the scope functions [apply] and [with];
- lines 64–68: a function is an object. The class of the function reference [operation] (line 16 of the output) is a class generated by the Kotlin compiler, named after the location where the reference appears; the class of the lambda [sum] (line 18 of the results) is generated at runtime by the virtual machine, just like Java lambdas: its name contains [$$Lambda] followed by an address that changes from one execution to the next. In both cases, the class implements the [kotlin.jvm.functions.Function2] interface (lines 17 and 19 of the results). A Kotlin function 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 Types
Functional types are often sufficient, but you may want to give a name to a function model. Kotlin offers two tools for this purpose, which are very different in nature.
The first is a type alias, declared using the keyword [typealias]:
- [typealias BinaryOperation = (n1: Int, n2: Int) -> Int]
An alias does not create a new type: [BinaryOperation] is another name for [(Int, Int) -> Int], and the two notations are interchangeable. The alias makes the code more readable when the functional type is long or appears frequently.
The second is the functional interface, declared by [fun interface]:
- [fun interface Operation { fun calculate(n1: Int, n2: Int): Int }]
This is the exact equivalent of a functional Java interface: an interface with only one abstract method (also known as a SAM, or “Single Abstract Method” interface). It may also have methods with bodies and non-abstract properties; these do not count. The keyword [fun] preceding [interface] serves the same purpose as the Java annotation [@FunctionalInterface], with one important difference: it is not optional. It is this keyword that enables the SAM conversion—that is, the creation of an [Operation] object from a lambda or a function reference: [Operation { n1, n2 -> n1 + n2 }]. Without the keyword [fun], a Kotlin interface—even one reduced to a single method—can only be implemented by a class or an anonymous object ([object : Operation { … }]).
Compared to the type [(Int, Int) -> Int], the type [Operation] is a truly new type, distinct from all others: it has a name that describes its purpose, its method has a descriptive name ([calculate] rather than [invoke]), it can have additional methods, and it can be implemented by a named class. Conversely, it is executed by its method name ([op.calculate(2, 3)]), not by [op(2, 3)].
A variable named [op] of type [Operation], like a variable of type [(Int, Int) -> Int], references only one function at a time. Unlike C#, which offers “multicast delegates,” neither Java nor Kotlin provides a way to store multiple functions in a single variable and execute them all with a single call: when needed, we store the functions in a list and execute them one by one. This is exactly what we’ll do with event listeners.
Consider the example [Lambdas02.kt]:
Results:
Comments:
- lines 60–64: definition of the functional interface [Operation]. In Kotlin, type declarations can be placed anywhere in the file, before or after the function [main]; we place them after it;
- Line 64: A functional interface can have methods with a body (the equivalent of Java’s [default] methods). The method [then] constructs a new [Operation]: this object performs the current operation, then applies the function [next] to its result. It returns a lambda converted to [Operation], which we will examine in the next section. Note that the [next] parameter does not require an interface like the [IntUnaryOperator] in the Java version: the functional type [(Int) -> Int] is sufficient, and it is executed via [next(…)];
- lines 76–89: the class [Calculations] has two instance methods, [add] and [subtract], and a method of its companion object, [increase] (the equivalent of a static method, see Chapter 4), all three of which have the same form as the method [calculate];
- line 9: the variable [op] is of type [Operation]. It is assigned the object obtained by converting the reference [Calculations::increase] to SAM, which refers to the method [increase] of the companion object of the class [Calculations]. The conversion must be written as follows: [Operation(Calculations::increase)] resembles a constructor call, but an interface does not have a constructor; the compiler creates the object that implements [Operation];
- line 11: the operation is performed by the [calculate] method. The [increase] method is the one that executes, with the parameters passed to [calculate];
- Line 15: An instance of class [Calculations] with the identifier [c1] is created;
- Line 17: The reference [c1::add] refers to the method [add] of the object [c1]: this is called a “bound reference.” [increase] was a method of the companion object, 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 21: the same is done with the instance method [subtract];
- lines 26–32: To execute multiple operations, they are placed in a list of type [List<Operation>] (inferred type), which is then traversed. References and lambdas can be mixed (line 29). Each result is retrieved (lines 7–11 of the results);
- line 36: the method [then] is applied to the operation [Operation(c1::add)]. The lambda [{ x -> x * x }] is passed outside the parentheses because it is the last (and in this case, the only) parameter of [then]: we will explain this syntax in more detail in the next section. [squaredSum.calculate(1, 2)] calculates (1+2)², which is 9 (lines 12–13 of the results). In Java, the conversion [((Operation) c1::add).then(…)] had to be written explicitly; in Kotlin, the explicit conversion SAM serves this purpose;
- Line 40: [Operation] and the Java interface [IntBinaryOperator] have exactly the same form, but they are two different types with no inheritance relationship. The assignment [val other: IntBinaryOperator = op] would be rejected ([initializer type mismatch: expected 'IntBinaryOperator', actual 'Operation']). In contrast, the reference [op::calculate] is a function of type [(Int, Int) -> Int]: the conversion SAM turns it into an [IntBinaryOperator], which calls the [calculate] method of the [op] object ([c1::subtract] at this stage of the program);
- lines 68–69: A functional interface can be generic. [Transformation<T, R>] represents a function that takes a [T] and returns an [R]: this is equivalent to [(T) -> R], with names chosen by us;
- Line 44: [String::length] refers to the property [length] of the class [String]: a reference can point to a property as well as a function. It is used as a function of type [(String) -> Int]: the property will be read from the string passed as a parameter. The types [T = String] and [R = Int] of [Transformation] are inferred from the reference. Line 45: ["*"::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 references (unbound, bound), which we will discuss in detail in the following section;
- line 73: a type alias. The parameters named [n1] and [n2] are for documentation purposes only;
- lines 50–54: the lambda [f] of type [BinaryOperation] is assigned without conversion to the variable [g] of type [(Int, Int) -> Int]: they are the same type. To convert it to an [Operation], however, a conversion to SAM is required (line 54).
These rules can be verified by triggering errors. If you write ***[var op: Operation = Calculations::increase]*** without an explicit conversion, the compiler will reject it:
The message also reveals the type of a function reference: [KFunction2<Int, Int, Int>], a Kotlin reflection type ([kotlin.reflect]) that is also a [(Int, Int) -> Int]. The implicit SAM conversion occurs only for the arguments of a function call (for example, [values.removeIf(isEven)]), not for an assignment.
If a second abstract method, such as [fun invert(n: Int): Int], is added by mistake to the interface [Operation], the compiler flags the declaration itself and each of the SAM conversions that use the interface (abbreviated output):
These messages are from the [kotlinc] compiler; Gradle displays them preceded by [e:] and the full file path. Like those from [javac], they are in English.
7.3. Anonymous objects, lambda expressions, and references
7.3.1. From anonymous objects to lambdas
In the previous examples, each function is a named function defined elsewhere in the program. For short functions used only once, this is cumbersome. Java originally offered the anonymous class: the class that implements the interface is defined right where the object is created. Kotlin has an equivalent—the object expression [object : Type { … }], introduced in Chapter 4—but it rarely uses it to represent a function: from the outset, Kotlin has supported lambda expressions, which offer a much more concise syntax. The program [Lambdas03.kt] demonstrates the different forms:
Results:
Comments:
- lines 7–12: an anonymous object. The keyword [object] is followed by a colon and the type to be implemented—in this case, the functional type [(Int, Int) -> Int] (this is an interface, as we have seen), and the body of a class that implements this type by redefining its method [invoke]. The compiler generates an unnamed class and creates an instance of it. Six lines to express [n1 + n2]. Yet it is executed like a function: [add(2, 3)];
- line 15: the same thing with a lambda expression. A lambda is written between curly braces: the list of parameters, the [->] operator (an arrow, which is read as “gives”), then the body. The type name and method name have disappeared. Since the parameters are typed, the compiler can determine the lambda’s type on its own: [(Int, Int) -> Int]. This differs from Java, where a lambda never has a type of its own;
- line 19: the parameter types have been omitted. The compiler infers them from the expected type declared for the variable: [n1] and [n2] are of type [Int]. This is the most common form. In Kotlin, there is no distinction between an “expression body” and a “block body” as there is in Java: the body of a lambda is a sequence of statements, and the value of the last expression is the result of the lambda;
- Line 23: With a single parameter, you don’t need to declare it at all, nor the arrow; it is then implicitly named [it] (“it,” “that”). [{ it * it }] reads as “its square.” Line 24: The single parameter is named [s]. This is done when [it] would make the code less readable, particularly when lambdas are nested (each would have its own [it]);
- Lines 28–29: Without a parameter, there is no arrow. The type of
*[greet]*is inferred as*[() -> String]*; the type of*[beep]is*[() -> Unit]*, since*[println]yields*[Unit]*; - Lines 34–39: A lambda can contain any instructions: declarations, loops, tests, etc. The value of the last expression, [buffer.toString()], is the result. You cannot write [return buffer.toString()]: in a lambda, an unlabeled [return] does not exit the lambda (see below);
- line 44: the underscore character [_] denotes an unused parameter. It is used when the type signature requires parameters that the lambda does not need, which is common for event listeners. You can include multiple such parameters in the same lambda (Java adopted this syntax in version 22);
- Lines 48–49: The most common use of lambdas is to pass them directly as parameters to a higher-order function. When the lambda is the last parameter, it is written outside the parentheses; if there are no other parameters, the parentheses are omitted: [numbers.filter { it % 2 == 0 }] rather than [numbers.filter({ it % 2 == 0 })]. It is this rule that gives Kotlin code its distinctive look and allows you to write functions that resemble language statements ([repeat], [use], [thread], [synchronized]…). Compare this with [Lambdas01.kt]: it is no longer necessary to write a function [isEven];
- line 51: a lambda that reverses the usual comparison: the list is sorted in descending order. [sortWith] expects a [Comparator] (a Java interface): the conversion to SAM is done automatically;
- line 54: the lambda remains outside the parentheses when the function has other parameters: [fold(0) { acc, n -> acc + n }]. The function [fold] (Chapter 5) starts with the initial value 0 and adds the elements one by one;
- lines 58–62: within a lambda, the statement [return@forEach] exits the lambda (and thus moves on to the next element), just like a [continue] in a loop. The label [@forEach] is implicit: it is the name of the function to which the lambda is passed. An unlabeled [return] would exit the entire function [main]: this is called a non-local return, and it is only allowed in a lambda passed to a function like [inline] or [forEach] (we’ll see why);
- lines 64–68: You can also choose the label by placing it before the opening curly brace: [odd@{ … }], then [return@odd];
- lines 70–74: an anonymous function. It is written like a regular function, but without a name. Its type is inferred: [(Int) -> Boolean]. In an anonymous function, [return] exits the anonymous function itself. This is used when the body has multiple exit points, or when you want to explicitly specify the type of the result;
- Line 78: The parameters of a lambda can be destructured (Chapter 3). The lambda passed to [forEach] receives an entry from the associative array ([Map.Entry<String, Int>]); The notation [(name, age)], enclosed in parentheses, decomposes it into its key and value. Note: [{ name, age -> … }] would declare two parameters, while [{ (name, age) -> … }] would declare a single decomposed parameter.
Let’s review the forms of a lambda expression:
Form | Example |
Typed parameters | [{ a: Int, b: Int -> a + b }] |
Untyped parameters (expected type known) | [{ a, b -> a + b }] |
a single parameter, implicit | [{ it * it }] |
no parameters | [{ "Hello" }] |
unused parameters | [{ _, _ -> println("received") }] |
destructured parameter | [{ (key, value) -> "$key=$value" }] |
multiple statements | [{ x -> println(x) ; x * 2 }] |
lambda as the last parameter | [list.filter { it > 0 }] |
anonymous function | [fun(x: Int): Int { return x * 2 }] |
A lambda is not quite a shorthand for an anonymous object. In an anonymous object, [this] refers to the anonymous object itself; in a lambda, [this] refers to the object of the method that encloses the lambda (or the receiver, for a lambda with a receiver). Furthermore, an anonymous object can implement an interface with multiple methods, multiple interfaces at once, or inherit from a class—which is impossible with a lambda. In this course, we will use lambdas whenever possible.
7.3.2. Additional Information: Type Inference and References
The program [Lambdas04.kt] explains how the type of a lambda is determined and describes the different types of references:
Results:
Comments:
- line 11: the lambda [{ x: Int -> x * 2 }] has its own type, [(Int) -> Int], calculated from the type of its parameter and that of its body. The declaration [val double = { x -> x * 2 }], on the other hand, is rejected: the compiler cannot infer the type of [x] ([an explicit type is required on a value parameter]). In Java, [var twice = x -> x * 2;] is rejected for a more fundamental reason: a Java lambda cannot exist without a target type, which specifies the interface to be implemented;
- lines 14–16: the same lambda expression, [{ "Hello" }], yields [() -> String] here, [Supplier<String>] there, and [Callable<String>] elsewhere. In the last two cases, it is the name of the Java interface—placed before the lambda—that triggers the conversion to SAM. These are three objects of different classes, which share only the form of their method;
- lines 19–41: the types of references. Kotlin calls them “callable references”: they refer to functions, but also to properties and constructors:
- line 21: the reference to a top-level function, [::function]. [::triple] is equivalent to the lambda [{ n -> triple(n) }]. It replaces the reference to a static method in Java ([Class::staticMethod]). For a function of a companion object, we write [Class::function], as in [Lambdas02.kt];
- lines 25–26: the reference to a method or property of a given object, [object::method], called a bound reference. [name::uppercase] is equivalent to [{ name.uppercase() }]: the object [name] is fixed once and for all. [name::length] references the property [length] of the string [name];
- Lines 30–31: a reference to a method of any object of a class, [Class::method], known as an unbound reference. The object on which the method will be called is not yet known: it becomes the first parameter of the function. [String::uppercase] is equivalent to [{ s: String -> s.uppercase() }]. For [ignoreCase], a lambda was preferred, which allows the argument named [ignoreCase = true] to be passed to [compareTo];
- line 35: an extension function is referenced like a method: [String::toInt] is equivalent to [{ s: String -> s.toInt() }];
- Lines 38–41: The reference to a constructor, [::Class] (in Java: [Class::new]). [::ArrayList] is equivalent to [{ ArrayList() }]; [::StringBuilder], with the expected type [(String) -> StringBuilder], selects the constructor that takes a string; [::IntArray] creates an array of integers of the size passed as a parameter; [::Person] refers to the primary constructor of the [Person] data class (line 65);
- line 44: the creation functions are used; [reverse] reverses the contents of [StringBuilder] (line 8 of the results);
- Lines 47–51: Kotlin does not support checked exceptions (Chapter 3). A lambda of type [() -> Int] can therefore throw an [IOException] without declaring anything. In Java, this was a real challenge: no [java.util.function] interface allows checked exceptions, and a lambda of type [Supplier<Integer>] that throws an [IOException] is rejected; so we had to use [Callable], whose method is declared as [throws Exception]. Note also that the lambda [{ throw IOException("file missing") }] is accepted as a [() -> Int]: the expression [throw] is of type [Nothing], which is compatible with all types;
- lines 55–59: two evaluations of the same reference
***[name::uppercase]produce two different objects ([r1 === r2]is equal to*[false]*), but, unlike Java, Kotlin considers them *equal in the sense of [equals] ([r1 == r2] is equal to [true]): two references are equal if they refer to the same function, associated with the same object. Two lambdas, even with identical text, are never equal (line 10 of the results). Keep this in mind: it will be important when unregistering an event listener. Finally, note that the type of*[r1]must have been written: the function [uppercase] is overloaded ([uppercase()] and [uppercase(locale: Locale)]), and without an expected type, the compiler refuses to choose ([overload resolution ambiguity between candidates]*).
A reference is simply a shorthand notation for a lambda that merely calls a function. It is used when it improves readability: [String::length] rather than [{ it.length }]. In practice, with [it], Kotlin lambdas are so short that references are used slightly less often than in Java.
7.3.3. Composing Functions
Java’s functional interfaces have methods [default] and [static] that create new functions from existing ones: [andThen], [compose], [and], [or], [negate]… Kotlin’s functional types do not have these: the standard library does not provide a function [andThen] for a [(A) -> B]. But it is very easy to write them yourself, as extension functions on functional types (Chapter 4), since a function can return a function. The program [Lambdas05.kt] demonstrates this, then introduces the functions from the Kotlin library that build comparators:
Results:
Comments:
- Line 60: The extension function [then] is applied to a function of type [(A) -> B] (the parentheses around the function type are required) and takes a function [next] of type [(B) -> C]. It returns a new function of type [(A) -> C]—the lambda [{ x -> next(this(x)) }]—which first applies the function [this] (the extension’s receiver), then [next] to its result. This is the [andThen] method of the Java interface [Function]. The three parameters of type [A], [B], and [C] are generic: [then] composes any single-parameter functions, provided that the result of the first function is suitable for the second. The modifier [infix] allows you to write [f then g] instead of [f.then(g)];
- line 63: the inverse composition, equivalent to the Java method [compose]: [f after g] first applies [g], then [f];
- line 11: [plusOne then timesTwo] returns the function that first applies [plusOne], then [timesTwo] to the result: (5 + 1) × 2 = 12. The parentheses around the composition are necessary to be able to call the resulting function;
- Line 12: [plusOne after timesTwo] first applies [timesTwo], then [plusOne]: 5 × 2 + 1 = 11;
- line 14: if you prefer the Java interface methods, simply create a [Function] by converting SAM: [andThen] is then available, and you can pass it a Kotlin lambda, which is in turn converted. The result is a
*[Function], which is executed by*[apply]; - lines 17–18 and 76: [adder] is a function that returns a function: [adder(10)] returns the function that adds 10 to its parameter. The returned function can be stored in a variable ([plusTen]) or called immediately: [adder(3)(5)]. This process, which involves fixing one parameter to obtain a function of the remaining parameters, is called partial application;
- lines 20–21: a dynamically constructed processing chain. We start with the identity function ([{ s: String -> s }], which returns its parameter unchanged) and, using [then], add each step in the list to it, thanks to the function [fold] (Chapter 5): The accumulated value, [function], is a function here. The resulting function [normalize] removes leading and trailing spaces ([trim]), converts the text to lowercase, and replaces spaces with dashes. Each call to [then] creates a new function: existing functions are never modified. Similarly, the list of steps could be read from a configuration file;
- Lines 66–70: The predicates combine with our extensions [and], [or], and [not]. The latter is declared as [operator]: this is the function called by the operator [!] (see operator overloading in Chapter 4). Thus, [!short] is written for the negation of the predicate [short] (line 30). In Java, [isShort.negate()] is written, and [Predicate.not(String::isEmpty)] for a method reference;
- lines 32–33: a predicate of type [(CharSequence) -> Boolean] is accepted by [filterElements] to filter a list of [String], whereas the parameter is declared as [(T) -> Boolean] with [T = String]. This is because Kotlin’s functional types are contravariant in their parameters and covariant in their results (Chapter 4): [(T) -> R] is actually [Function1<in T, out R>]. A function that can process any [CharSequence] can also process a [String]. In Java, you must declare the parameter [Predicate<? super T>] to achieve this result; in Kotlin, it happens automatically;
- lines 36–42 and 73: the [andAlso] extension returns a function that performs two successive operations on the same value, like the [andThen] method of the Java interface [Consumer]. This is the simplest way to execute multiple operations with a single call. Note that the lambda [{ this(it); other(it) }] has two statements on the same line, separated by a semicolon;
- line 47: the [compareBy(f)] function from the Kotlin library constructs a comparator that compares the values of [f] (here, age); [thenBy] specifies the tiebreaker to use in case of a tie (here, name). These are the equivalents of [Comparator.comparing] and [thenComparing] in Java. The references [Person::age] and [Person::name] refer to the properties of the data class [Person] (lines 89–90), for which we have redefined the method [toString] to shorten the output;
- line 49: [reversed] reverses the order obtained so far, and [thenByDescending] adds a criterion in descending order;
- line 52: [compareBy] also accepts multiple criteria, in the form of lambda expressions, applied in order: here, the length of the name, followed by the name. These are two lambdas passed as arguments, since [compareBy] receives them in a parameter named [vararg];
- lines 55–56: The function [minOf] returns the smaller of its first two parameters based on the comparator passed as the third parameter (in Java: [BinaryOperator.minBy]);
- lines 79–86: the function [filterElements] used for these tests. This is a higher-order function that does what the library function [filter] does: it applies the predicate [predicate] to each element—calling it as an ordinary function—and retains the elements for which it returns [true].
These functions allow you to write complex processing by combining simple functions, without writing a single class. We’ll revisit them when discussing collections and sequences.
7.3.4. Variable capture and closures
A lambda can use the local variables of the function in which it is written. It is said to capture these variables. In this regard, Kotlin differs significantly from Java. Java requires that a captured local variable be “effectively final”: it is never modified after its initialization, neither in the method nor in the lambda. Kotlin, like C#, JavaScript, or Python, imposes no such restriction: a lambda can capture a variable [var], read it even after it has changed, and even modify it. The program [Lambdas06.kt] demonstrates what this freedom allows, how it is implemented, and the pitfall it entails:
Results:
Comments:
- Line 11: The lambda uses the local variable [threshold] declared on line 10. [threshold] is not a parameter of the lambda; it is a captured variable. Since it is a [val], the lambda simply receives a copy of its value when it is created, exactly as in Java;
- lines 15–19: this time, the lambda captures a
*[var]*, which Java would reject (*[local variables referenced from a lambda expression must be final or effectively final]). It does not receive a copy of the value, but shares the variable with the function [main]: when [variableThreshold] changes to 10 (line 19), the same lambda returns a different result (lines 2–3 of the results). The lambda function therefore sees the value of the variable at the moment it executes*, not at the moment it was created. This is the issue that Java avoids by prohibiting these captures; - Lines 23–27: The lambda [accumulate] modifies the local variable [total]. In Java, you have to use a mutable object ([AtomicInteger], or the workaround of a single-element array); in Kotlin, you write [total += n] directly;
- line 29: how is this possible, since the virtual machine itself only allows values to be captured? The Kotlin compiler does the work that a Java developer must do manually: a captured and modified local variable [var] is no longer stored on the stack, but in a small object of class [kotlin.jvm.internal.Ref.IntRef] (there is one for each primitive type, and
*[Ref.ObjectRef]for references), which has a single mutable field,*[element]. The function [main] and the lambda share this object: every read or write to [total], in either one, becomes a read or write to [total.element]. We can verify this through reflection: the lambda object’s single field is of type [Ref$IntRef] (line 5 of the results). This is exactly what the C# compiler does with its captured variables; - lines 31–34: a lambda can also read and modify a top-level variable (line 6) or a property of the current object. There is nothing to capture here: the top-level variable is a static field of the [Lambdas06Kt] class; for a property, the lambda captures the reference [this];
- lines 64–67: The function [createCounter] returns a lambda that captures and modifies its local variable [value]. Normally, a local variable disappears when the function ends. Here, the variable is stored in an object [Ref.IntRef] referenced by the lambda: it survives [createCounter] as long as the lambda exists. The combination of the lambda and the variables it captures is called a “closure”;
- lines 37–40: Each call to
*[createCounter]creates a new variable[value]*, and thus a new counter, independent of the others. The results (lines 7–8) confirm this; - lines 44–48: In a Kotlin loop [for], the variable [i] is a [val], which is new on each iteration: each lambda captures its own, and the three lambdas print 0, 1, and 2. Note that a list of functions is executed via [actions.forEach { it() }]: [it] is a function that is called;
- Lines 53–59: This is the downside of Kotlin’s flexibility. The loop [while] uses a single variable, [k], which is modified on each iteration. The three lambdas capture the same variable and print its value when they execute—that is, after the loop, when it is equal to 3: they all print 3 (line 10 of the results). Java eliminates this pitfall at compile time; in Kotlin, it’s up to the developer to handle it. The solution is the same as in Java: declare a new [val] in the loop body for each iteration ([val copy = k]) and capture it, or, better yet, use a loop [for].
The Java rule has a solid justification: a lambda may be executed later, or in another thread (Chapter 8). If it can modify a function’s local variables, or read them while another thread is modifying them, this can lead to unpredictable behavior. Kotlin takes a different, more practical approach: capturing [var] is allowed, but [Ref] objects are not protected against concurrent access. So let’s remember that capturing a [var] is safe in a single-threaded program and in lambdas that are executed immediately ([forEach], [filter]…), but that as soon as a lambda can execute in another thread, the shared mutable state must be made explicit using an object whose threading behavior is explicitly chosen ([AtomicInteger], concurrent collection…). We will return to this topic in Chapters 8 and 9. The functions [inline], presented later, also avoid the creation of [Ref] objects when the lambda is copied into the calling code.
7.4. Inline Functions and Scope Functions
7.4.1. Inline functions
A lambda is an object. Passing a lambda to a higher-order function therefore incurs a cost: the creation of the object (except for a lambda that captures nothing, which the virtual machine can reuse), the call to its [invoke] method, the [Int] ↔ [Integer] conversions imposed by the generic interfaces [Function1], [Function2]…, and the [Ref] objects of the captured variables. For a function like [filter], which is called on every element of collections that can sometimes be very large, this cost matters. Kotlin eliminates this with the [inline] modifier: when a function is declared [inline fun], the compiler does not generate a call to that function; instead, it copies its code to every location where it is called, and it also copies the code of the lambdas passed to it. As a result, there are no longer any lambda objects, no [invoke] calls, and no conversions. The standard library functions that accept lambdas ([forEach], [filter], [map], [fold], [let], [apply], [repeat], [use], [synchronized]…) are almost all [inline]. Java has no equivalent: it relies on the virtual machine’s on-the-fly compiler to produce a result comparable to runtime execution.
The insertion of the code has a second, more noticeable consequence: since the lambda’s code is copied into the calling function, an unlabeled [return] instruction can exit the calling function from there. This is the non-local return encountered with *[forEach] in *[Lambdas03.kt]. Two modifiers complement *[inline]*:
- [noinline], applied to a function parameter of a [inline] function, specifies that the corresponding lambda should not be copied, but passed as a regular object. This is necessary when the function wants to retain the lambda (store it in a list, return it as a result, etc.): copied code cannot be stored in a list;
- [crossinline] requires that the lambda be copied, but prohibits non-local returns. This is necessary when the function [inline] does not execute the lambda itself, but executes it in another context—for example, within a [Runnable] object that it creates: When [Runnable] executes, the calling function may have long since finished, and a [return] would no longer be able to exit it.
The program [Lambdas07.kt] illustrates these concepts (this program has no equivalent in the Java course):
Results:
Comments:
- Line 28: The function [currentMethod] returns the name of the method that called it. To do this, it creates a [Throwable] object, which saves the execution stack at the time of its creation, and reads the second element of that stack (the first being [currentMethod] itself). We use this to determine where a lambda’s code is executed;
- lines 31–36: two identical functions, one ordinary and the other declared as [inline];
- line 7: the lambda is passed to the regular function [execute]. The compiler turns this into the body of a private method of the [Lambdas07Kt] class—named [main$lambda$0] here—and creates an object of type [() -> Unit] that calls it. It is indeed [main$lambda$0] that executes (line 1 of the results);
- line 8: the lambda is passed to the function [inline] [executeInline]. The compiler copied the body of [executeInline] and that of the lambda into [main]: the lambda’s [println] executes directly within the [main] method (line 2 of the results). No function object was created;
- lines 41–50: [forEachElement] is our own version of [forEach], [inline], just like the original. In [firstEven], the [return it] statement in the lambda is a non-local return: it exits the [firstEven] function by returning the first even number found (line 3 of the results), and the loop stops there. With an ordinary function like [execute], the compiler would reject such a [return] (['return' is prohibited here]): the lambda would be a separate object, which could be executed after [firstEven] has finished;
- lines 54–60: a function [inline] that takes two lambdas: it executes the first one and stores the second in a list for later execution. The second parameter must be declared as [noinline]: without this modifier, the compiler refuses to store the lambda in the list ([illegal usage of inline parameter 'stored: () -> Unit'. Add 'noinline' modifier to the parameter declaration.]). Lines 16–18 use it (lines 4–6 of the output);
- line 64: the function [asTask] creates a [Runnable] object (conversion SAM) that will execute the lambda [action]. The parameter must be declared as [crossinline]: without this modifier, the compiler rejects it ([cannot inline 'action: () -> Unit' here: it might contain non-local returns. Add 'crossinline' modifier to parameter declaration 'action: () -> Unit'.]). Lines 21–24 execute the [Runnable] obtained in a thread named [worker] (threads are covered in Chapter 8), as confirmed by line 7 of the results.
You shouldn’t use [inline] on just any function: the modifier is only useful for functions that accept functional parameters, and it increases the size of the generated code (the compiler, in fact, issues a warning for a [inline] function without functional parameters). Finally, it has a third use, discussed in Chapter 4: only [inline] functions can have parameters of type [reified] ([inline fun <reified T> …]), whose type remains known at runtime, since the code is copied to the location where that type is known.
7.4.2. Scope functions
The Kotlin standard library provides five small, widely used functions—[inline]—known as "scope functions": [let], [run], [with], [apply], and [also]. They do almost nothing: each one executes a lambda on an object. They differ in two ways:
- the way the lambda refers to the object: either as its parameter [it] (the lambda is of type [(T) -> R]), or as its receiver [this], which can then be omitted (the lambda is a receiver lambda, of type [T.() -> R]; see [Lambdas01.kt]);
- which is what the function returns: either the result of the lambda or the object itself.
Function | Object designated by | Returns | Typical usage |
[let] | [it] | the result of the lambda | transform a value; with [?.let], process a non-null value |
[run] | [this] | the result of the lambda | Calculate a result based on the object's properties |
[with(object)] | [this] | the result of the lambda | such as [run], where the object is passed as a parameter |
[apply] | [this] | the object | Configure a newly created object |
[also] | [it] | the object | Additional processing (logging, caching, etc.) without modifying the call chain |
The program [Lambdas08.kt] uses them (it has no equivalent in the Java course):
Results:
Comments:
- lines 45–48: a regular [Person] class with mutable properties;
- lines 7–9: [apply] executes the lambda with the object as the receiver: within the lambda, [age = 32] refers to [this.age = 32]. It then returns the object, which is assigned to [paul]. This creates and configures an object in a single expression. Its signature illustrates the mechanism: [inline fun <T> T.apply(block: T.() -> Unit): T];
- line 15: [also] executes the lambda with the object as the parameter [it], then returns the object. This is used to achieve a side effect (in this case, adding to a list) without having to introduce an intermediate variable;
- line 19: [let] executes the lambda with the object as the parameter [it] and returns the result of the lambda: this is a transformation;
- Lines 21–23: The most common use of [let], as seen in Chapter 3: [city?.let { … }] executes the lambda only if [city] is not [null], and, within the lambda, [it] is of type [String] and not [String?]. If [city] equals [null], the expression equals [null], and the Elvis operator [?:] provides the default value;
- Line 27: [run] executes the lambda with the object as the receiver and returns the result of the lambda: it directly uses the properties [name], [age], and [city] of [paul];
- Lines 31–34: [with] does the same thing as [run], but the object is passed to it as the first parameter, and the lambda comes second, outside the parentheses. All the methods called within the lambda ([append], [toString]) are those of [StringBuilder]. The function [buildString] from Chapter 5 is built on this model;
- lines 39–41: [takeIf] is not a scoped function, but it is used in the same way: it returns the object if it satisfies the predicate, and [null] otherwise. It combines naturally with [?:], [?.let]… ([takeUnless] does the opposite).
These functions make the code more compact, but they shouldn’t be overused: multiple nested scope functions, each with its own [it] or [this], quickly make the code unreadable. A well-named local variable is often preferable.
7.5. Functional types, 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, since the two concepts are one and the same: a lambda is an instance of an interface. Kotlin falls somewhere in between: it has functional types, which are distinct from interfaces in syntax but are interfaces to the virtual machine; and it has functional interfaces ([fun interface]) that can be implemented using lambdas. Let’s take the example [Lambdas09.kt], which is similar to [Lambdas02.kt]:
Results:
On line 18, the function [execute] expects a parameter of type [(Int, Int) -> Int]. This allows different functions to be passed to it: our high-level functions [add] and [subtract] (lines 7 and 9), and a method from the Java library with the correct form (line 11: among the overloads of [Math.max]*, the compiler chooses [int max(int a, int b)]***, which Kotlin views as ***[max(a: Int, b: Int): Int]***), and even a lambda (line 13): it wasn’t necessary to write a multiplication function. The function parameter was placed last** so that the lambda could be written outside the parentheses: this is the convention throughout the Kotlin library, and it’s a good idea to follow it in your own functions.
With [fun interface], we see the interface polymorphism covered in Chapter 4: the program [Lambdas10.kt] demonstrates this by implementing the same interface with classes:
Results:
Comments:
- lines 31–32: The functional interface [Operation] defines a method [calculate];
- lines 41–49: The classes [Add] and [Subtract] implement the interface [Operation];
- line 25: the function [execute], whose first parameter is of the type defined by the interface [Operation]. It receives, in succession, an instance of the class [Add], an instance of the class [Subtract] (lines 7 and 9), and then a lambda (line 11). For the lambda, the conversion from
SAMto*[Operation]is performed automatically, since it is a call argument. For [execute], these three objects are of the same type: objects that implement [Operation]*; - Lines 36–37: The [NamedOperation] interface inherits the [calculate] method and adds an abstract property [name]. It is not functional (in fact, it cannot be declared as
[fun interface]): it can no longer be implemented using a lambda; a class is required—in this case, an anonymous object (lines 13–16); - lines 57–58: the converse is also true: a class can implement a functional type, since it is an interface. Its instances are then called like functions ([modulo(7, 2)]) and passed wherever a function is expected ([applyFunction(modulo, 7, 3)], line 21).
These two examples show that in Kotlin, just as in Java, a function that expects a behavior accepts objects from classes written for that purpose, references, and lambdas. Two questions remain. The first is specific to Kotlin: for a functional parameter, is a functional type required, or is a [fun interface] sufficient?
- The functional type is the default choice. It requires no declaration, is called like a function, directly accepts lambdas and references, and benefits from [inline] functions. A [typealias] gives it a name when it is long;
- the “fun” interface is justified when you want a truly distinct type (one that cannot be confused with another function of the same form), a method with a descriptive name, additional methods with a body, or a API also intended for Java callers: for Java, a [fun interface] is an ordinary functional interface, usable with its lambdas, whereas a Kotlin functional type appears to it as a [Function2<Integer, Integer, Integer>] whose [invoke] method must return an object ([Unit] for a “returnless” function).
The second question is the same as in Java: should the interface have a single method, or multiple ones?
- A function, functional type, or single-method interface is more flexible to use: any existing function with the correct form will work, and a lambda even allows you to write the function right where it’s called. There is no class to write. This flexibility explains why modern libraries (collections, the coroutines in Chapter 9, Spring) make extensive use of functional parameters;
- an interface with multiple members 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 member—the [val taxBrackets: List<TaxBracket>] property—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 [() -> List<TaxBracket>] would technically have sufficed, but it would have been much less descriptive. We therefore reserve functional types for short, one-off behaviors (a criterion, a transformation, an action), and “business” interfaces for the application’s roles, regardless of the number of their members.
7.6. Event Handling
7.6.1. The Observer Pattern
Functions passed as parameters are used to set up events. Like Java, Kotlin has no dedicated keyword for events (C# has its own keyword, [event]): it uses the conventions established by Java as early as version 1.1 for graphical components and JavaBeans, known as the “listener model.” A [C1] class that wants to signal an [Xxx] event proceeds as follows:
- A [XxxEvent] class encapsulates information about the event, including the object that triggered the event, known as the source. In Java, the convention is for it to derive from [java.util.EventObject], which stores the source; in Kotlin, a regular class with read-only properties is sufficient, and that is what we will do;
- A listener type defines the form of the function that will be called when the event occurs. In Java, this is an interface named [XxxListener]; in Kotlin, it is most often a simple functional type, [(XxxEvent) -> Unit], named by a [typealias]: a listener will be a lambda or a function reference;
- the [C1] class manages a list of functions and provides two public methods for registering and unregistering with it: [addXxxListener(listener)] and [removeXxxListener(listener)];
- When a [c1] instance of [C1] wants to signal the event, it creates an [XxxEvent] object and calls each of the registered listeners, passing this object to them. By convention, this notification is handled by a protected method [fireXxx(e: XxxEvent)] (“fire”), which a derived class can override to modify how the event is published;
- If an object [c2] wants to be notified of the event [Xxx] from the object [c1], it subscribes 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 type of listener that defines the form of the functions 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 property 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.6.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 be functions of type [TemperatureTooHighListener];
- a class named [Subscriber] registers as a listener for the previous event. When it is notified of the event, it displays a message on the console. A listener unsubscribes automatically after receiving a specified number of alerts;
- A console program creates an emitter and two subscribers. It enters temperatures via the keyboard and records them in the emitter. If any of them is too high, the emitter signals the [TemperatureTooHigh] event.
This program is divided into three files in the [lambdas11] package. First is the file [Emitter.kt], which contains the event class, the listener type, and the class [Emitter]:
Comments:
- Lines 7–14: The [TemperatureTooHighEvent] class stores the event source, of type [Emitter], and the temperature that triggered the event. We added the event time, [time], which is initialized when the object is created. All properties are of type [val]: a listener must not be able to modify the event information for subsequent listeners. We did not derive the class from [EventObject] as in Java: that would serve no purpose here, and would require us—in order for the source to be of type [Emitter] rather than [Any]—to redefine the [getSource] method;
- Line 18: the type of the listener. A listener is a function that takes the event object as an argument and returns nothing. Where Java requires an interface
*[TemperatureTooHighListener]* derived from*[EventListener]*, Kotlin simply uses a functional type alias. We could just as easily have declared a***[fun interface TemperatureTooHighListener]***: that would be the right choice if the listeners were to be written in Java; - line 24: the temperature threshold above which the [TemperatureTooHigh] event is triggered, a constant of the companion object;
- line 21: the emitter has a name for identification. The class is declared as [open], so that a derived class can redefine the [fireTemperatureTooHigh] method;
- line 28: the list of registered listeners, a list of functions. It is a [CopyOnWriteArrayList] from the [java.util.concurrent] package, a list that, whenever it is modified, copies its contents into a new array. An ongoing 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 31–36: The property [temperature] has a custom accessor [set] (Chapter 4): it stores the new value in the hidden field [field], then signals the event if it exceeds the threshold. Publishing is handled by the [fireTemperatureTooHigh] method, to which a [TemperatureTooHighEvent] object is passed; this object originates from the emitter itself ([this]) and contains the temperature that exceeded the threshold. Where Java requires a method named [setTemperature], the class user simply writes [e1.temperature = 21.0];
- lines 41–42: Registering a listener adds it to the list. The parameter type is not nullable: the compiler rejects any listener of type [null], which Java must reject at runtime via [Objects.requireNonNull];
- lines 46–47: Unregistering removes the listener from the list. The method [remove] searches for the object to be removed using [equals]: therefore, it must be passed an object identical to the one that was registered;
- lines 51–53: The notification calls each registered listener, in the order they were registered, as a function: [listener(e)]. If there are no listeners, the loop does nothing: there are no special cases to handle.
Here is the file [Subscriber.kt]:
Comments:
- lines 4–11: Each subscriber is identified by a name and is assigned the maximum number of alerts they accept ([nbMaxAlerts]). It counts the alerts received ([nbAlerts]);
- lines 14–15: registration with an emitter. The listener is a bound reference, [::onTemperatureTooHigh], that is, [this::onTemperatureTooHigh]: this subscriber’s method, [onTemperatureTooHigh];
- lines 19–20: Unsubscribing passes a new reference, [::onTemperatureTooHigh], to [removeTemperatureTooHighListener]. In Java, this would be a classic error: two evaluations of
[this::onTemperatureTooHigh]produce two objects that[equals]*considers different, and the unsubscription would not remove anything, without any error message; the listener must be created only once and stored in a field. We saw with [Lambdas04.kt] that in Kotlin, two references to the same function of the same object are equal: [remove] therefore finds the registered listener—and only that listener—since the references of the other subscribers are bound to other objects. This would not be the case with a lambda: [{ e -> onTemperatureTooHigh(e) }]*, written twice, would result in two different objects; - Line 24: the event handler. It has the form dictated by the type [TemperatureTooHighListener]. It is private: only the subscriber can register it with an emitter. Another approach is to have the listener type implemented by the [Subscriber] class itself ([class Subscriber : TemperatureTooHighListener], with a method [invoke]) and register with [this]. This approach has the drawback of making the method [invoke] public, which anyone could then call;
- line 25: the event source is directly of type [Emitter];
- lines 26–28: the handler displays the subscriber’s name, the name of the emitter that reported the event, and the temperature that triggered it. The [%.1f] format displays the real number with one decimal place, according to the machine’s local conventions (the Kotlin function [format] uses them; here, a decimal point). The string is written in two parts concatenated by [+] so that the line fits on the page;
- lines 30–33: After [nbMaxAlerts] alerts, the subscriber unsubscribes from the emitter. It does so 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.
Finally, here is the main program, [Lambdas11.kt]:
Results (the standard input was supplied via redirection; we have reinserted the inputs after the prompts, where the program reads them):
Comments:
- line 7: creation of the emitter;
- lines 9–11: creation of two subscribers, [s0] (up to 3 alerts) and [s1] (1 alert), which are registered with the emitter;
- lines 14–15: a third listener, a lambda passed outside the parentheses, which acts as a log. The type of the parameter [e] is inferred from that of the parameter of [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 the same object to [removeTemperatureTooHighListener]. Therefore, lambdas written directly in the registration call are reserved for listeners that remain registered as long as the emitter exists;
- lines 19–33: loop for entering temperatures via the keyboard. [readlnOrNull()] reads a line; it returns [null] if standard input is closed, a case that is treated as an empty entry using [isNullOrBlank] (line 23). After this test, the compiler knows that [input] is not [null] (intelligent type conversion);
- line 27: [toDoubleOrNull] 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, [toDoubleOrNull] returns [null]: there is no need to catch an exception as the Java version does with [Double.parseDouble];
- Line 33: If the entered temperature is correct, it is assigned to the property [temperature] of the emitter [e1], whose accessor [set] signals the event [TemperatureTooHigh] if it is greater than 19 °C.
The results show (times vary from one run to another):
- line 1: 17 °C does not exceed the threshold: no event;
- lines 2–6: 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 9–11: [s1] is no longer notified: unsubscription via a new reference worked as intended;
- lines 12–15: [s0] receives its third alert and unsubscribes in turn;
- lines 16–17: Only the log remains.
What would happen if there were simply an [ArrayList] instead of the [CopyOnWriteArrayList] 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 loop throws a [ConcurrentModificationException]. A listener list must therefore always support changes 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.6.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. Kotlin and the Java library provide two complementary tools for this scenario:
- Kotlin's delegated properties. A property declared as [var p: T by delegate] delegates the reading and writing of its value to another object, the delegate. The standard library provides several of these, such as [by lazy { … }], which calculates the property’s value on the first access. The object [kotlin.properties.Delegates] provides two delegates that accept a lambda: [Delegates.observable(initialValue) { property, old, new -> … }] executes the lambda after each write to the property; [Delegates.vetoable(initialValue) { property, old, new -> … }] executes it beforehand and accepts the new value only if the lambda returns [true]. The parameter [property] is a reference to the property ([KProperty]), whose name is obtained via [property.name];
- the Java class [java.beans.PropertyChangeSupport], which manages the list of listeners for us. The 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 delegate [observable] knows when a property changes, and [PropertyChangeSupport] knows who to notify: the program [Lambdas12.kt] links them together:
Results:
Comments:
- Lines 3–4: The [java.beans] package belongs to the [java.desktop] module of JDK, not to the [java.base] module. Since our program is not a Java module, all classes in JDK are accessible to it: we simply need to import them;
- Line 45: The [Thermometer] class delegates the management of its listeners to a [PropertyChangeSupport] object, constructed using the event source ([this]). The methods on lines 60–69 simply pass subscription and unsubscription events to it, and the [nbListeners] property (lines 72–73) returns the number of registered listeners;
- lines 48–50: a small generic function that creates a [Delegates.observable] delegate whose lambda passes each change to [firePropertyChange], along with the property name ([property.name]), the old value, and the new value. It returns a
*[ReadWriteProperty<Any?, T>]*, the interface for delegates of mutable properties. Like a method, a lambda written within a class can access the object’s private properties, in this case[changes]; - lines 53–54: the class’s two observable properties. The keyword [by] delegates their management to a delegate created by our function [observable]. Compared to the Java version, where each setter must store the old value, save the new one, and call
*[firePropertyChange]with the property name written as a string, the Kotlin version fits into a single line per property, and the property name cannot be misspelled. The delegate executes the lambda on every write, even if the value doesn’t change; however, [firePropertyChange]* only notifies the listeners if the two values are different: the write on line 28, which doesn’t change the value, therefore produces no output; - lines 13–17: a first observer, a lambda converted to [PropertyChangeListener] and stored in a variable so it can be unsubscribed (line 31). It is subscribed to all of the object’s properties. [e.source] (i.e., [e.getSource()], viewed from Kotlin as a property) is of type [Any], which must be cast to [as]. The values are objects: [Double] objects are displayed using their [toString] method, which uses the decimal point;
- Lines 19–21: A second listener, registered solely for the property [temperature]. The lambda is moved out of the parentheses and converted to [PropertyChangeListener] (conversion SAM). It triggers an alert when the temperature exceeds 19 °C. [e.newValue as Double] converts the new value to the type [Any?];
- Lines 25–33: property changes are made via simple assignments. The name change (line 29) is reported only to the log. On line 4 of the results, the log displays the new source name, [lounge]: the event is reported after the change. After unsubscribing from the log, only the alert is displayed, and only one listener remains;
- line 57: the property [setpoint] uses the delegate [vetoable]. Its lambda, called before each write operation, accepts the new value only if it is between 5 and 30 °C ([new in 5.0..30.0]); its first two parameters, which are unnecessary here, are denoted as [_]. Lines 36–38: the value 21 is accepted, while the value 45 is rejected; the property retains its previous value (lines 7–8 of the results). The JavaBeans instances have an equivalent mechanism, with [VetoableChangeSupport] and listeners that throw a [PropertyVetoException].
Without writing an event class or a listener type, we thus obtain a complete mechanism. [PropertyChangeSupport] is the one used by Swing GUI components and JavaBeans; JavaFX has its own, very similar mechanism for observable properties. The delegates [observable] and [vetoable], on the other hand, are sufficient on their own when there is only one observer, known at the time the class is written. Finally, note that for asynchronous event streams, Kotlin provides the [Flow] from the kotlinx.coroutines library, which we’ll cover in Chapter 9 ([StateFlow], in particular, acts as an observable property).
7.7. Lambdas, Collections, and Sequences
In Chapter 5, we used the functional operations of collections ([filter], [map], [sortedBy]…) and sequences by passing lambdas to them. We now know what these parameters are: the function [filter] is declared as follows in the standard library: [inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>].
It is an extension function, [inline], whose parameter is a functional type: any function or lambda that takes an element and returns a Boolean is suitable. Similarly, [map] expects a [(T) -> R], [sortedBy] expects a [(T) -> R?], and [R] is comparable, [forEach] corresponds to a [(T) -> Unit], [reduce] to a [(S, T) -> S]… To fully understand how they work, let’s write our own filtering and transformation operations. Here is the program [Lambdas13.kt]:
Results:
Comments:
- Lines 9–11: The lambdas from [filter] and [map] are first assigned to variables to reveal their types. [map] transforms each element: this yields the squares of even numbers;
- line 13: the same calculation using the Java API stream, which can be used as-is from Kotlin: the Java method [filter] expects a [Predicate], [map] expects a [Function], and Kotlin lambdas are automatically converted (SAM conversion). It is rarely used in Kotlin for sequential computations, since collection and sequence functions are simpler; it remains useful for the parallel streams in Chapter 8, or when a Java library returns a [Stream];
- Lines 51–58: Our function [filterBy], written as an extension function of [List<T>] and declared as [inline], just like [filter]. It is generic: it filters a list of elements of any type [T]. It iterates over the list ([this]), calls the predicate on each element (line 54), and returns the list of selected elements;
- Line 16: call to [filterBy] with a lambda, outside the parentheses. The type [T] is inferred from the list: [Int]. On line 17, a function reference is passed to it: [isOdd] (line 61) has the form [(Int) -> Boolean];
- Lines 20–25: a verbose predicate, which displays each element it tests, applied to a list. Collection operations are immediate: [filter] tests all elements and constructs the complete result list before returning it (lines 5–6 of the results). In a chain [list.filter { … }.map { … }], each step thus creates an intermediate list;
- lines 29–34: the same predicate applied to a sequence ([asSequence()]). The result (lines 7–8) shows that the sequence definition does not execute anything: the message on line 33 is displayed before the first test. It is only when the terminal operation [forEach] requests the elements that the predicate executes, and the tests alternate with the displays from [forEach]: each element passes through the entire chain before the next one enters it. This is the deferred (or lazy) execution of sequences, the same as that of Java streams. The intermediate operations of the sequences are not [inline]: they must retain the lambdas in order to execute them later;
- lines 64–73: To write our own intermediate operations—which are lazy, like those in the library—we use the sequence constructor [sequence]. It takes a lambda in which the function [yield] produces an element of the sequence: for each element requested by the downstream, the lambda resumes execution where it left off, until the next [yield] (this is a coroutine, a concept we will study in Chapter 9). The label of
[this@keep]designates the receiver of the extension function[keep], that is, the original sequence: without a label, [this] would denote the receiver of the lambda in [sequence], since that lambda is a lambda with a receiver. The function [keep] passes only those elements that satisfy the predicate; the function [transform], which is doubly generic ([T] is the type of the received elements, [R] is the type of the produced elements), returns the result of the function applied to each element. Java 24 introduced intermediate collectors ([Gatherer]) for the same purpose, which have a significantly more complex syntax; - lines 39–42: Our extension functions are inserted into the string just like library operations: we keep cities with at most 5 letters, then convert them to uppercase. The terminal operation [toList] triggers the calculation;
- lines 45–47: [generateSequence(1) { it * 2 }] defines an infinite sequence: 1, followed by each element obtained by doubling the previous one. It is usable only thanks to lazy evaluation: [take(5)] requests only the first five elements, and only those are computed. The lambda passed to [transform] displays each power in the form [2^n=value] ([countTrailingZeroBits] counts the rightmost zeros in the binary representation, i.e., the exponent).
All operations on collections and sequences are built on this model: higher-order functions that take functional types. For collections, these functions are [inline] and immediate; for sequences, they preserve the lambdas and compute the elements only when requested by the terminal operation.
7.8. 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.Limit > 20000] and translate it, for example, into a clause SQL [WHERE limits > 20000] executed by the database server.
Kotlin, like Java, has no equivalent: a Kotlin lambda is always compiled into executable code; it can be called, but its internal workings cannot be examined. Several tools come close, however:
- Property references: [Person::age] is an object ([KProperty1<Person, Int>]) that knows the property name ([Person::age.name]) and its type. A library can use this to construct a query without writing a string: this is what Spring Data MongoDB does, for example, with its Kotlin extensions, which allow you to write a criterion such as [Person::age gt 18];
- Internal DSL (“Domain-Specific Languages”): thanks to lambdas with receivers, [infix] functions, and operator overloading, a Kotlin library can offer a syntax similar to SQL, which the program executes to construct a data structure describing the query. This is the principle behind the Exposed library, from JetBrains: [Brackets.selectAll().where { Brackets.limit greater 20000 }];
- compiler plugins: the Kotlin compiler can be extended with plugins that transform the code. This is how kotlinx.serialization works, the [all-open] and [no-arg] plugins used with Spring and Hibernate, or the [power-assert] plugin, which analyzes an assertion’s expression to display its details in case of failure. However, these are development tools, not built-in features of the program.
Hibernate, which we will cover in Chapter 11, offers the same solutions as in Java: 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’ 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 Kotlin program.
7.9. Conclusion
Key takeaways from this chapter:
- In Kotlin, functions are values: they have a functional type ([(Int, Int) -> Int], [() -> Unit], [(T) -> Boolean], [(() -> Unit)?], [T.() -> R]…), can be stored in variables, passed as parameters, returned as results, and called like ordinary functions. To the virtual machine, they are objects that implement the interfaces [Function0], [Function1]…;
- the interfaces of [java.util.function] ([Function], [Supplier], [Consumer], [Predicate]…), along with [Runnable], [Callable], and [Comparator], remain usable: when a Java method expects a functional interface, Kotlin automatically converts a lambda or a reference to it (conversion SAM); elsewhere, the conversion is written explicitly: [Comparator { … }];
- to name a function type, we have the alias [typealias] (simply an alias) and [fun interface] (a true new type, implementable via a lambda);
- A function can be obtained via a lambda expression ([{ a, b -> a + b }], [{ it * it }]—written outside parentheses when it is the last parameter, and closed with [return@label]), via an anonymous function, via an anonymous object, or via a reference ([::function], [object::method], [Class::method], [::Class]). Two references to the same function of the same object are equal; two lambdas are never equal;
- lambdas capture the variables surrounding them, including the [var], which they can modify (the compiler stores them in [Ref] objects); they then see the value of the variable at the time of their execution, which requires caution in loops and between threads;
- Functions that return functions and extensions on functional types ([then], [and], [or]…) compose new functions; [compareBy], [thenBy], and [reversed] construct comparators;
- the functions [inline] copy their code and that of the lambdas to the call site, without an object or conversion, and allow non-local returns; [noinline] and [crossinline] handle special cases. The scope functions [let], [run], [with], [apply], and [also] are the most common applications of this;
- a functional type is chosen for short-term, one-off behavior; a [fun interface] for a distinct type; or a API that can be used from Java; a multi-member interface (or a “business” interface) for an application role;
- an event is set up using the listener pattern: an [XxxEvent] class, a listener type (often a functional-type [typealias]), [addXxxListener] / [removeXxxListener], and a list of functions ([CopyOnWriteArrayList]) iterated over by a [fireXxx] method. The delegated properties [Delegates.observable] and [Delegates.vetoable] signal or filter changes to a property, and [PropertyChangeSupport] manages the list of listeners for them;
- collection operations (immediate, [inline]) and sequence operations (lazy) are higher-order functions; Kotlin does not have expression trees, and Hibernate expresses its queries using HQL or the Criteria API.
Lambdas will reappear in all subsequent chapters: a thread executes a lambda ([thread { … }]), and an [ExecutorService] executes a [Callable] (Chapter 8); a coroutine is launched by [launch { … }] or [async { … }] (Chapter 9), extension functions of [ResultSet] will receive, in the form of a lambda, the transformation of a line from the result into an object (Chapter 10), and Spring Boot web services process HTTP requests in methods that the framework calls as listeners (Chapter 13).