Lambda expressions and member references

1) Introduction to Lambda: blocks of code that are arguments to a function

Lambda expressions are used in scenarios such as “Run the event handler when an event occurs” or “apply the action to all elements of the data structure.” Lambda expressions efficiently pass code blocks directly as function arguments. Comparing the following anonymous inner class with the listener of the lambda implementation, you can see that the lambda implementation is more concise,

/ * * / Java anonymous inner class way button. SetOnClickListener (new an OnClickListener () {@ Override public void onClick (View View) {/ * Click the action */}});Copy the code
/ * Kotlin Lambda method * / button. SetOnClickListener {/ * click execute action after * /}Copy the code

2) Lambda and sets

Search through the collection using lambda, as shown in the following example

val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age })
Copy the code

The maxBy function can be called on any collection and requires only one argument: a function. The code in curly braces {it.age} is a lambda that implements this logic, taking an element from a collection as an argument (referencing it with it) and returning the value for comparison.

If a lambda happens to be a delegate to a function or attribute, it can be replaced with a member reference, as in

people.maxBy { Person::age }
Copy the code

3) Syntax of Lambda expressions

A lambda encodes a piece of behavior, can pass it around as a value, can be declared independently and stored in a variable, and can be declared directly and passed to a function.

Lambda expressions are defined in curly braces, including arguments and the function body, with arrows separating the argument list from the lambda function body, as follows:

Lambda expressions can be stored in a variable that is treated as a normal function, such as

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
Copy the code

Lambda expressions can be called directly, as in

{ println(42) }()
Copy the code

If you need to enclose a small piece of code in a block, you can use the library function run to execute the lambda passed to it. Run runs the code in the closure and returns a value such as

run { println(42) }
Copy the code

People. MaxBy {it. Age}

A) If you don’t use any simple grammar, write as follows

people.maxBy({ p: Person -> p.age })
Copy the code

This is a bit wordy. For one thing, excessive punctuation destroys readability. Second, types can be inferred from context and omitted. Again, there is no need to assign a name to lambda’s arguments. Finally, if the lambda expression is the last argument to the function call, it can be placed outside the parentheses.

B) If the lambda expression is the last argument to the function call, it can be placed outside the parentheses as follows

people.maxBy() { p: Person -> p.age }
Copy the code

When there are multiple arguments, the lambda can be left inside or outside parentheses. If you want to pass two or more lambdas, you cannot put more than one out.

C) When lambda is the only argument to a function, we can also remove empty parenthesis pairs from the calling code:

people.maxBy { p: Person -> p.age }
Copy the code

D) omit lambda parameter types:

people.maxBy { p -> p.age }
Copy the code

E) Use the default parameter name it instead of the named parameter when the argument name is not explicitly specified and there is only one argument whose type can be inferred:

people.maxBy { it.age }
Copy the code

In the case of nested lambda, it is recommended to explicitly declare the arguments to each lambda.

If lambda is stored in variables, there is no context in which the parameter type can be inferred, and the parameter type must be specified explicitly, for example,

val getAge = { p: Person -> p.age }
people.maxBy(getAge)
Copy the code

When a lambda contains more statements, the last expression is the result of the lambda, as in

val sum = { x: Int, y: Int -> println("Computing the sum of $x and $y..." ) x + y } println(sum(1, 2))Copy the code

4) Access variables in scope

Using lambda inside a function gives access to the function’s arguments, as well as local variables defined before lambda. The following

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}
Copy the code

The function takes lambda as an argument, and the “prefix” argument can be accessed from lambda.

Final variables can be accessed inside lambda in Kotlin, and non-final variables can be modified. As follows,

fun printProblemCounts(responses: Collection<String>) {var clientErrors = 0 var serverErrors = 0 responses. ForEach {if (it. StartsWith (" 4 ")) { clientErrors++ } else if (it.startsWith("5")) { serverErrors++ } } }Copy the code

By default, the life of a local variable is limited to the function that declares it, but if it is captured by a lambda, code that uses the variable can be stored and executed later. When a final variable is captured, its value is stored with the lambda code that uses the value. For non-final variables, the value is wrapped ina special wrapper that can be changed, and references to this wrapper are stored with the lambda code.

If lambda is used as an event handler or in other asynchronous execution cases, changes to local variables will only occur while the lambda is executing.

5) Member references

To convert a function to a value that can be passed as an argument to another function, use :: to convert, as in

val getAge = Person::age
Copy the code

This expression, called a member reference, is used to create a function value that calls a single method or accesses a single property. A double colon separates the class name from the name of the member to be referenced.

You can also refer to top-level functions, such as

fun salute() = println("Salute")
run(::salute)
Copy the code

Constructor references can be used to store or defer the action of creating class instances. Constructor references are in the form of specifying the class name after a double colon. The action of creating an instance of “Person” as follows is saved as a value.

data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
Copy the code

Extension functions can also be referenced in the same way,

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
Copy the code

The following two ways are equivalent,

val p = Person("Dmitry", Person::age personsAgeFunction(p) val dmitryAgeFunction = p::age dmitryAgeFunction()Copy the code

Functional APIS for collections

1) Basic: Filter and Map

The filter function iterates through the collection and selects those elements that will return true when applying the given lambda, such as

val list = listOf(1, 2, 3, 4)
list.filter { it % 2 == 0 }
Copy the code

The map function applies the given function to each element in the collection and collects the result into a new collection, as in

val list = listOf(1, 2, 3, 4)
list.map { it * it }
Copy the code

2) “all”, “any”, “count” and “find” : apply a judgment to the set

All: indicates whether all elements satisfy the statement. The result is false

val canBeInClub27 = { p: Person -> p.age <= 27}
val people = listOf(Person("Alice", 27), Person("Bob", 31))
people.all(canBeInClub27)
Copy the code

Any: checks whether there is at least one matching element in the set. The result is true

people.any(canBeInClub27)
Copy the code

Count: Checks how many elements satisfy the criterion. The result is 1

people.count(canBeInClub27)
Copy the code

Size creates an intermediate set of all elements that meet the criteria. Count keeps track of the number of elements that match.

Person(name=Alice, age=27)

people.find(canBeInClub27)
Copy the code

3) groupBy: Converts lists into grouped maps

The following example

val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
println(people.groupBy { it.age })
Copy the code

The output is

{29=[Person(name=Bob, age=29)], 
 31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}
Copy the code

Each group is stored in a List of type Map<Int, List>.

4) flatMap and Flatten: Handle elements in nested collections

The flatMap first transforms each element in the collection based on the function given as an argument, and then merges multiple lists into a single list. [A, B, C, D, e, f]

val strings = listOf("abc", "def")
strings.flatMap { it.toList() }
Copy the code

Lazy set operation: sequence

Filters and maps create intermediate collections, and the intermediate results of each step are stored in a temporary list, which is poor performance when the data is large. To improve efficiency, turn the operation into a use sequence, as follows

people.asSequence()
    .map(Person::name)
    .filter { it.startWith("A") }
    .toList()
Copy the code

The initial collection is converted to a sequence using asSequence, and the resulting sequence is converted back to the list using toList. If you only need to iterate over elements in a sequence, you can use sequences directly. If you want to use other API methods, such as accessing elements with subscripts, you need to turn the sequence into a list.

The entry point for Kotlin’s lazy collection operations is the asSequence interface, which represents a sequence of elements that can be enumerated one by one. Sequence provides only one method, iterator, to get a value from the Sequence.

1) Perform sequential operations: intermediate and terminal operations

Sequence operations fall into two categories: intermediate and terminal. An intermediate operation returns another sequence, and an end operation returns a result. Nothing is printed on the console without the end operation, which triggers the execution of all deferred calculations. All operations are applied to each element in sequence.

Use Java functional interfaces

Kotlin’s lambda interoperates seamlessly with the Java API.

So let’s look at a click-listening example,

button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ... }}Copy the code

In Kotlin, you can pass a lambda in place of this instance, as follows

button.setOnClickListener { view -> ... }Copy the code

The reason this works is that the OnClickListener interface has only one abstract method. This interface is called a functional interface, or SAM interface, which stands for single abstract method. Kotlin allows you to use lambdas when calling methods that take functional interfaces as arguments, keeping Kotlin code clean and custom compliant.

1) Pass lambda as an argument to a Java method

A method that can pass a lambda to any expected functional interface. For example, the following method takes a parameter of type Runnable:

/* Java */
void postponeComputat on(int delay, Runnable computation);
Copy the code

Explicitly create an anonymous object that implements Runnable to pass the parameter as follows

postponeComputation(1000, object : Runnable {
    override fun run() {
        println(42)
    }
})
Copy the code

In Kotlin, you can call it and pass it a lambda as an argument. The compiler automatically converts it to an instance of Runnable:

PostponeComputation (1000) {println(42)}Copy the code

When an object is explicitly declared in approach one, a new instance is created for each call. Lambda is different: if the lambda doesn’t access any variables from the function that defines it, the corresponding anonymous class instance can be reused between calls, and the entire program creates only one instance of Runnable.

To allow the method to create one instance at a time, we can store Runnable instances in a variable that is used each time:

val runnable = Runnable { println(42) }
fun handleComputation() {
    postponeComputation(1000, runnable)
}
Copy the code

Runnable stores a unique instance of Runnable in the program, and the same object is used each time postponeComputation is called.

If a lambda captures a variable from the enclosing scope, each time it is called, the compiler creates a new object that stores the value of the captured variable. Here is an example,

fun handleComputation(id: String) {
    postponeComputation(1000) { println(id) }
}
Copy the code

At the bottom, the above code is compiled into the following form:

class HandleComputation$1(val id: String) : Runnable {
    override fun run() {
        println(id)
    }
}

fun handleComputation(id: String) {
    postponeComputation(1000, HandleComputation$1(id))
}
Copy the code

2) SAM constructor: explicitly convert lambda to a functional interface

SAM: SAM: SAM: SAM: SAM: SAM: SAM: SAM

A lambda is a block of code, not an object, and cannot be treated as an object reference. The this reference in lambda refers to the class that surrounds it. In contrast to anonymous objects, the this keyword refers to the object instance.

When lambda is passed as an argument to an overloaded method, there are also cases where the compiler cannot choose the correct overload, and using the SAM constructor shown is a good way to resolve the compiler’s error.

5, “with”, “let”, “run”, “also”, “apply”, “use”

All of the following functions can run the code in the closure and return results.

1) The “with” function

The following is an example of the “with” function,

fun alphabet() = with(StringBuilder()) { for (letter in 'A'.. 'Z') { append(letter) } append("\nNow I know the alphabet!" ) toString() }Copy the code

The with function is a function that takes two arguments: in this case, a StringBuilder object created and a lambda. I’m going to put lambda out of parentheses here.

The with function converts its first argument to the receiver of the lambda passed to it as the second argument, and the StringBuilder acts as the receiver of the lambda. The recipient can be explicitly accessed through this reference, or the this reference can be omitted.

The value returned by with is the result of executing the lambda code, which is the value of the last expression in the lambda.

2) “let” function

The let function turns an object that calls it into an argument to a lambda expression, as shown in the figure,

The safe call to “let” only executes a lambda if the expression is not null, as shown in the following example

email? .let { sendEmailTo(it) }Copy the code

The let function is called only if the value of email is non-empty, returns the last line in the function, or specifies return. When multiple values need to be checked for null, nested let calls can be used to handle this. But this code can be harder to understand, and it might be simpler to use if.

3) “run” function

The run function combines the advantages of the let and with functions: non-null checks can be performed before expressions are executed; If we pass this, we can call the properties and functions of this directly. The value returned is the result of executing the lambda code, which is the value of the last expression in the lambda.

user? .run { println("$name, $age") this }Copy the code

4) the “also” function

The “also” function can perform a non-null check before an expression is executed; The passed argument is it; The return value is fixed to this itself.

user? .also { println("${it.name}, ${it.age}") }Copy the code

5) “apply” function

The “apply” function can perform a non-null check before an expression is executed. The argument passed in is this; The return value is fixed to this itself.

user? .apply { println("$name, $age") }Copy the code

6) “use” function

When InputStream, OutputStream, etc., opens something that needs to be turned off, use executes lambda code through the use function. After execution, use helps to turn off something that needs to be turned off, such as

File(pathName).inputStream().reader().buffered()
    .use {
        it.readLine()
    }
Copy the code

About it and this

  • This is used for function types with receivers, representing receivers.
  • It is used in function types: a function takes only one argument. It represents the parameter object