Write at the beginning: I plan to write a Kotlin series of tutorials, one is to make my own memory and understanding more profound, two is to share with the students who also want to learn Kotlin. The knowledge points in this series of articles will be written in order from the book “Kotlin In Action”. While displaying the knowledge points in the book, I will also add corresponding Java code for comparative learning and better understanding.
Kotlin Tutorial (1) Basic Kotlin Tutorial (2) Functions Kotlin Tutorial (3) Classes, Objects and Interfaces Kotlin Tutorial (4) Nullability Kotlin Tutorial (5) Types Kotlin Tutorial (6) Lambda programming Kotlin Tutorial (7) Operator overloading and Other conventions Higher-order functions Kotlin tutorial (9) Generics
A Lambda expression, or Lambda for short, is essentially a piece of code that can be passed to other functions. With lambda, it is easy to extract common code structures into library functions, and the Kotlin standard library makes extensive use of them.
Lambda expressions and member references
The introduction of lambda into Java 8 is one of the most anticipated changes in the evolution of the Java language. Why is it so important? In this section, you’ll find out why lambda works so well and what Kotlin’s lambda syntax looks like.
Introduction to Lambda: Blocks of code that are arguments to a function
Storing and passing small pieces of behavior in your code is a common task. For example, you often need to express ideas like “run this event handler when a time occurs” or “apply this action to all elements in the data interface.” In older versions of Java, you could do this using anonymous inner classes. This technique works but the syntax is too verbose. Functional programming offers another approach to the problem: treating functions as values. You can pass functions directly without having to declare a class and then pass an instance of that class. With lambda expressions, the code is much more concise. There is no need to declare functions, and you can efficiently pass code blocks directly as function arguments. Let’s look at an example. Suppose you want to define the behavior of clicking a button, add a listener that handles clicking. The listener implements the corresponding interface OnClickListener and one of its methods onClick:
/* Java */
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
});
Copy the code
This is too verbose a way to declare anonymous inner classes. In Kotlin we can use lambda just like in Java 8 to eliminate this redundant code.
/* Kotlin */
button.setOnClickListener{ /* do someting */ }
Copy the code
This code does the same thing as above, but without the verbose anonymous inner class. As mentioned earlier, Kotlin can use the keyword object to create an anonymous inner class, so you can write it the normal way:
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
println("on click")}})Copy the code
Convert to Java code in the following two ways:
button.setOnClickListener((OnClickListener)null.INSTANCE);
button.setOnClickListener((OnClickListener)(new OnClickListener() {
public void onClick(@Nullable View v) {
String var2 = "on click"; System.out.println(var2); }}));Copy the code
Anonymous inner classes are transformed into Java’s anonymous inner classes. But lambda is probably Kotlin’s own special processing and cannot be converted to the corresponding Java code.
Lambda and collections
Let’s start with an example where you’ll use a Person class that contains the name and age of the Person:
data class Person(val name: String, val age: Int)
Copy the code
Suppose you now have a list of people and need to find the oldest person in the list. If you don’t know anything about lambda, you might do something like this:
fun findTheOldest(people: List<Person>) {
var maxAge = 0
var theOldest: Person? = null
for (person in people) {
if (person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> findTheOldest(people)
Person("Alice"29),Copy the code
It does the job, but it’s a little too much code. Kotlin has a better way, using library functions:
>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice"29),Copy the code
The maxBy function can be called on any collection and takes only one argument: a function that specifies which value to compare to find the largest element. The code in curly braces {it.age} is the lambda that implements this logic. It takes an element from a collection as an argument (referring to it using it) and returns a value for comparison. In this case, the collection element is the Person object, which compares ages stored in its Age attribute. If a lambda happens to be a delegate to a function or attribute, it can be replaced with a member reference:
people.maxBy{ Person::age }
Copy the code
Although lambda looks neat, you may not understand exactly how to write lambda and the rules in it. Let’s look at the syntax of lambda expressions.
Syntax for Lambda expressions
A lambda encodes a little behavior that you can pass around as a value. It can be declared independently and stored in a variable. But it’s more common to declare it directly and pass it to a function.
{x: Int, y: Int -> x + y}Copy the code
Kotlin’s lambda expressions are always surrounded by curly braces. -> Separate the argument from the function body, with the argument list on the left and the function body on the right. Note that the arguments are not enclosed in (). Lambda expressions can be stored in a variable that is treated like a normal function (that is, called with the corresponding argument) :
>>> val sum = {x:Int,y:Int -> x + y}
>>> println(sum(1, 2))
3
Copy the code
If you like, you can also call lambda expressions directly:
>>> { println(42) }()
42
Copy the code
But such syntax is unreadable and meaningless (it is equivalent to directly executing the code in the body of a lambda function). If you really need to enclose a small piece of code in a block, you can use the library function run to execute the lambda passing it:
>>> run{ println(42) }
42
Copy the code
In later sections we will see why this invocation is as efficient as the built-in language structure and does not incur additional runtime overhead. Now let’s move on to the “find the oldest on the list” example:
>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice"29),Copy the code
If you were to rewrite this example without any concise syntax, you would get the following code:
people.maxBy({ p: Person -> p.age })
Copy the code
The code is self-explanatory: the snippet in curly braces is a lambda expression, passed as an argument to the function. The lambda takes an argument of type Person and returns its age. But this code is a bit verbose. First, excessive punctuation destroys readability. Second, types can be inferred from context and omitted. Finally, there is no need to assign a name to lambda arguments in this case. So let’s improve on those, and we’ll start with curly braces. Kotlin has a syntactic convention that if a lambda expression is the last argument to a function call, it can be placed outside the parentheses. In this example, lambda is the only argument, so it can be placed after parentheses:
people.maxBy() { p:Person -> p.age }
Copy the code
We can also remove empty parentheses from the calling code when lambda is the only argument to the function:
people.maxBy { p:Person -> p.age }
Copy the code
All three grammatical forms have the same meaning, but the last is the easiest to read. If lambda is the only argument, you’d like to omit these parentheses when writing code. When you have more than one argument, you can either leave the lambda inside parentheses to emphasize that it is an argument, or you can put it outside the parentheses, both options work. If you want to pass two more lambdas, you cannot put more than one lambda outside. Let’s see how these options look in more complex calls. Remember the joinToString function defined in Tutorial 2? It is also defined in the Kotlin library, except that it can accept an additional function argument. This function can convert an element to a string using methods other than toString. The following example shows how you can use it to print only the person’s name:
>>> val names = people.joinToString(separator = "", transform = { p: Person -> p.name })
>>> println(names)
Alice Hubert
Copy the code
This approach uses named arguments to pass the lambda, making it clear where the lambda is applied. The following example shows how the class could rewrite the call to place the lambda outside parentheses:
>>> val names = people.joinToString("") { p: Person -> p.name }
>>> println(names)
Alice Hubert
Copy the code
This approach does not explicitly indicate where the lambda is referenced, so it may be harder for those unfamiliar with the function being called to understand.
In AS or IDEA you can invoke the action using Alt+Enter. Move lambda expression out of parentheses using “Move lambda expression out of parentheses”. Or “Move lambda expression into parentheses” to Move lambda expression into parentheses.
We continue to simplify the syntax by removing the type of the parameter.
People. MaxBy {p:Person -> p.age} people. MaxBy {p -> p.ageCopy the code
As with local variables, if the type of a lambda argument can be derived, you do not need to specify it explicitly. In the case of the maxBy function here, the parameter type is always the same as the element type of the collection. The compiler knows that you are calling maxBy on a collection of Person objects, so it can deduce that lambda arguments will also be of type Person. There are also cases where the compiler cannot infer the type of a lambda argument, but we won’t discuss that here. You can follow a simple rule: do not declare types and wait for the compiler to report an error before specifying them. The final simplification you can make in this example is to use the default parameter name it instead of the named parameter. This name is generated if the current context expects a lambda with only one argument and the type of the argument can be inferred.
People. MaxBy {it. Age} //it is the automatically generated parameter nameCopy the code
The default name is generated only if the argument name is not explicitly specified.
It conventions can greatly shorten your code, but you shouldn’t abuse them. Especially in the case of nested lambdas. It is best to explicitly declare the arguments to each lambda. Fouz, it’s hard to figure out which value it is quoting. An explicit declaration of parameters is also useful if the type or meaning of the parameters in the context is not clear.
If you store lambdas in variables, there is no context in which to infer the type of the argument, so you must explicitly specify the type of the argument:
>>> val getAge = { p:Person -> p.age }
>>> people.maxBy(getAge)
Copy the code
The examples you’ve seen so far have been lambdas of single expressions or statements. But lambda is not limited to such a small size, and it can contain many more statements. In this case, the final expression is the result:
val sum = { x: Int, y: Int ->
println("Computing the sum of $x and $y ...")
x + y
}
>>> println(sum(1, 2))
Computing the sum of 1 and 2 ...
3
Copy the code
Access variables in scope
When you declare an anonymous inner class within a function, you can reference the parameters and local variables of the function within the anonymous inner class. You can do the same thing with lambda. If you use lambda inside a function, you can also access the parameters of that function, as well as local variables defined before lambda. We use the library function forEach to demonstrate this behavior. This function iterates through every element in the collection and calls the given lambda on that element. The forEach function is just a little cleaner than a regular for loop.
fun printMessageWithPrefix(message: Collection<String>, prefix: String) {// Accept lambda as an argument to specify the operation on each element message.forEach {println("$prefix $it"}} >>> Val errors = listOf()"403 Forbidden"."404 Not Found") > > >printMessageWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found
Copy the code
One significant difference between Kotlin and Java here is that final variables are not limited to being accessed in Kotlin; they can also be modified inside a lambda:
fun printProblemCounts(response: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
response.forEach {
if (it.startsWith("4")) {
clientErrors++
} else if (it.startsWith("5")) {
serverErrors++
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
>>> val response = listOf("200 OK"."418 I'm a teapot"."500 Internal Server Error") > > >printProblemCounts(response)
1
Copy the code
Unlike Java, Kotlin allows non-final variables to be accessed and even modified inside a lambda. Access external variables from within the lambda, which we say are captured by the lambda, just like prefix, clientErrors, and serverErrors in this example.
Access non-final variables and even modify their principles
Note that by default, the life of a local variable is limited to the function that declares it, but if it is captured by the lambda, the code that uses the variable can be stored and executed later. You may ask how does this work? When you capture a final variable, its value is stored with the lambda code that uses the value. For non-final variables, the value is wrapped ina special wrapper so you can change the value, and references to the wrapper are stored with the lambda code. This principle is also mentioned in tutorial 3 on anonymous inner classes: accessing variables in functions that create anonymous inner classes is not restricted to final variables, as illustrated in this example:
var clickCount = 0
B().setListener(object : Listener {
override fun onClick() {clickCount++ // modify variable}})Copy the code
And converted to Java code:
final IntRef clickCount = new IntRef();
clickCount.element = 0;
(new B()).setListener((Listener)(new Listener() {
public void onClick() { int var1 = clickCount.element++; }}));Copy the code
You can see that the real clickCount used is an int, but in Java the wrapper class IntRef is used, and the real int becomes clickCount. Element. Any time you capture a final variable (val), its value is copied, just like in Java. When you capture a variable variable (VAR), its value is stored as an instance of the Ref class. The Ref variable is final and can be captured easily, whereas the actual value is stored in its field and can be modified within the lambda.
It is important to note that if the lambda is used as an event handler or in other asynchronous execution cases, changes to local variables will only occur when the lambda is executed. For example, the following code is not the correct way to count button clicks:
fun tryToCountButtonOnClicks(button: Button): Int {
var clicks = 0
button.setOnClickListener { clicks++ }
return clicks
}
Copy the code
This function always returns 0. Although the onClick handler can modify clicks’ value, you will not notice the change because the onClick handler is called after the function returns. Proper implementation of this function requires that the number of clicks be stored outside the function in places that are still accessible — such as class properties — rather than in local variables of the function.
Members of the reference
You’ve already seen how lambda lets you pass blocks of code to functions as arguments. But what if the code you want to pass as an argument is already defined as a function? Of course you could pass a lambda that calls this function, but that would be a bit redundant. Can you pass the function name directly? Kotlin, like Java 8, allows you to pass a function if you convert it to a value. Use the :: operator to convert:
val getAge = Person::age
Copy the code
This expression, called a member reference, provides a concise syntax for creating a function value that calls a single method or accesses a single property. The double colon separates the class name from the name of the member (a method or an attribute) that you are referencing. The same thing with lambda expressions looks like this:
val getAge = { person: Person -> person.age }
Copy the code
Never put parentheses after the name of a member reference, whether you refer to a function or attribute. Member references have the same type as the lambda calling the function, so they can be used interchangeably.
You can also refer to top-level functions (which are not members of the class) :
fun salute() = println("Salute!")
>>> run(::salute)
Salute!
Copy the code
In this case, you omit the class name and start with ::. The member reference ::salute is passed as an argument to the library function run, which calls the corresponding function. If a lambda is to delegate to a function that takes multiple arguments, it would be handy to provide a member reference instead:
val action = { person: Person, message: String -> sendEmail(person, message) // This lambda delegates to the sendEmail function} val nextAction = ::sendEmail // Can be replaced with member referencesCopy the code
Constructor references can be used to store or defer the creation of class instances. Constructor references are in the form of the class name specified after a double colon:
data class Person(val name: String, val age: Int)
>>> val createPerson = ::Person
>>> val p = createPerson("Hubert">>> println(p) Person(name=Hubert, age=26)Copy the code
Extension functions can also be referenced in the same way:
fun Person.isAdult() = age >= 18
val predicate = Person::isAdult
Copy the code
Although isAdult is not a member of the Person class, it can be accessed by reference, just as if it were an instance member: Person.isadult ().
The binding reference
In Kotlin 1.0, when accepting a method or property reference to a class, you always need to provide an instance of that class to invoke the reference. The Kotlin 1.1 initiative supports bound member references, which allow you to capture method references on specific instance objects using the member reference syntax.
>>> val p = Person("Hubert". 26) >>> val personsAgeFunction = Person::age >>> println(personsAgeFunction(p)) 26 >>> val hubertsAgeFunction = p::age //Kotlin 1.1 allows binding member references >>> println(hubertsAgeFunction()) 26Copy the code
Note that personsAgeFunction is a single-argument function (returning the age of a given person), while hubertsAgeFunction is a no-argument function (returning the age of the p object). Before Kotlin 1.1, you needed to explicitly write lambda{p.age} instead of referring to p::age with bound members.
The functional API for collections
The functional programming style offers many advantages when working with collections. Most tasks can be done using library functions to simplify your code.
The filter and map
The filter and map functions form the basis of collection operations, and many collection operations are expressed through them. We’ll give two examples of each function, one using numbers and the other using the familiar Person class:
data class Person(val name: String, val age: Int)
Copy the code
The filter function iterates through the collection and selects those elements that return true when applied to the given lambda:
>>> val list = listOf(1, 2, 3, 4) >>> println(list.filter {it % 2 == 0}) // Leave only even numbers [2, 4]Copy the code
The result above is a new set that contains only those elements in the input set that satisfy the judgment. If you want to keep those over 30, use filter:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.filter { it.age > 30 })
Person(name=Bob, age=31)
Copy the code
The filter function can remove unwanted elements from the collection, but it does not change them. Transformations of elements are where maps come in. The map function applies the given function to each element in the collection and collects the results into a new collection. A list of numbers can be converted to a list of their squares, as in:
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]
Copy the code
The result is a new set containing the same number of elements, but each element has been transformed according to the given judgment. If you want to print only a list of names, rather than a complete list of people, use map to transform the list:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.map { it.name })
[Hubert, Bob]
Copy the code
This example can also be beautifully rewritten with member references:
people.map(Person::name)
Copy the code
Multiple such calls can easily be linked together. For example, print out the names of people over the age of 30:
>>> people.filter { it.age > 30 }.map(Person::name)
[Bob]
Copy the code
Now, if you want the names of all the oldest people in the group, you can find the oldest people in the group first, and then return all the people of that age. You can easily write lambda as follows:
people.filter { it.age == people.maxBy(Person::age).age }
Copy the code
But notice that this code repeats the process of finding the maximum age for everyone, assuming there are 100 people in the set, the process of finding the maximum age is executed 100 times! The following solution is modified to calculate the maximum age only once:
val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }
Copy the code
Don’t double count if you don’t have to! The simplicity of code using lambda expressions sometimes masks the complexity of the underlying operations. Always remember what your code is doing. You can also apply filtering and transformation functions to the map collection:
>>> val numbers = mapOf(0 to "zero", 1 to "one">>> println(numbers. MapValues {it.value.toupperCase ()}) {0=ZERO, 1=ONE}Copy the code
Keys and values are handled by their respective functions. FilterKeys and mapKeys filter and transform keys in the map set, while filterValues and mapValues filter and transform corresponding values.
“All”, “any”, “count”, and “find” : apply judgments to collections
Another common task is to check whether all elements in a collection meet a condition (or a variant of it, whether there are elements that do). In Kotlin, they are expressed through the all and any functions. The count function checks how many elements meet the criteria, and the find function returns the first element that meets the criteria. To demonstrate these functions, let’s first define a judgment to check if a person is under the age of 28:
val canBeInClub27 = { p:Person -> p.age <= 27 }
Copy the code
If you are interested in whether all elements satisfy the judgment, you should use the all function:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false
Copy the code
If you need to check if there is at least one matching element in the collection, use any:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.any(canBeInClub27))
true
Copy the code
Note that! All (not all) plus some condition can be replaced by any plus the inverse of that condition, and vice versa. To make your code easier to understand, select functions that do not require a negation sign in front of them:
>>> val list = listOf(1, 2, 3) >>> println(! list.all { it == 3 }) //! The negation is not obvious and it is best to use any in this casetrue>>> println(list.any { it ! = 3}) // The conditions in the lambda argument should be reversedtrue
Copy the code
The first line check is to make sure that not all elements are equal to 3. This is the same thing as at least one element that is not 3, which is exactly what you did in the second line with any. If you want to know how many elements satisfy the predicate, use count:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1
Copy the code
Use the right function to get the job done: count vs. size
The count method is easily forgotten, and is then implemented by filtering the collection and then resizing it:
>>> println(people.filter(canBeInClub27).size)
1
Copy the code
In this case, an intermediate set is created and used to store all the elements that satisfy the predicate. The count method, on the other hand, just tracks the number of matched elements and doesn’t care about the elements themselves, so it’s more efficient.
To find an element that satisfies a judgment, use the find function:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Hubert, age=26)
Copy the code
If there are more than one matching element, the first one is returned; Or return null if none of the elements satisfy the predicate. Find also has a synonym for firstOrNull, which you can use to make your intentions clearer.
GroupBy: Converts lists into grouped maps
Suppose you need to divide all the elements into different groups based on different characteristics. For example, if you want to group people by age, people of the same age are in a group. It is convenient to pass this feature directly as a parameter. The groupBy function can do this for you:
>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31), Person("Carol", 31))
>>> println(people.groupBy { it.age })
Copy the code
The result of this operation is a map of elements grouped by keys (age in this case) and elements grouped by persons:
{26=[Person(name=Hubert, age=26)], 31=[Person(name=Bob, age=31), Person(name=Carol, age=31)]}
Copy the code
Each group is stored in a List of types Map
>. This map can be further modified using functions such as mapKeys and mapValues. Let’s look at another example of grouping strings by the first letter using member references:
>>> val list = listOf("a"."ab"."b")
>>> println(list.groupBy(String::first))
{a=[a, ab], b=[b]}
Copy the code
Here first is not a member of the String class, but an extension that can also be accessed as a member reference.
Flatmap and Flatten: Handles elements in nested collections
If you have a bunch of books, use the Book class to represent:
data class Book(val title: String, val authors: List<String>)
Copy the code
Each book may have one or more authors, and a set of all authors in the library can be counted:
books,flatMap { it.authors }.toSet()
Copy the code
The flatMap function does two things: it first transforms (or maps) each element in the collection based on the function given as an argument, and then merges (or tiles) multiple lists into a single list. The following string example nicely illustrates this concept:
>>> val strings = listOf("abc"."def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]
Copy the code
The toList function on the string converts it to a list of strings. If you use the map function with toList, you get a list of character lists, as shown in the second line below. The flapMap function also performs the following steps and returns a list of all the elements.
Going back to the example of book authors, where each number can have multiple authors, the property book.authors stores a collection of authors for each book, and the flatMap function merges all book authors into a flat list. The toSet call removes all repeating elements from the result set. You might think of flapMap when you’re stuck in a set of elements and have to merge one. If you don’t need to do any transformations and just need to tile a collection, you can use the flatten function: listOfLists.flatten().
Lazy set operations: sequences
In the previous section, you saw many examples of chaining function calls, such as map and filter. These functions create intermediate collections early, meaning that the intermediate results of each step are stored in a temporary list. Sequences give you another way to perform these operations without creating these temporary intermediate objects. Let’s start with an example:
people.map(Person::name).filter { it.startsWith("A")}Copy the code
As noted in the Kotlin Library reference, both filter and Map return a list. This means that the chain call in the above example creates two lists: one to hold the results of the filter function and the other to hold the results of the map function. If the original list had only two elements, this would not be a problem, but if there were a million elements, chain calls would become inefficient. For efficiency, you can change the operation to use sequences rather than collections directly:
People.assequence () // Convert the initial set to a sequence.map(Person::name).filter {it.startswith ()"A"}.tolist () // Converts the resulting sequence back to the listCopy the code
The result is exactly the same as in the previous example: A list of names starting with the letter A. The second example, however, does not create any intermediate collections to store elements, so performance improves significantly with a large number of elements. The entry point for Kotlin’s lazy collection operations is the Sequence interface. This interface represents a sequence of elements that can be enumerated one by one. Sequence provides only one method: iterator to retrieve values from the Sequence. The power of the Sequence interface lies in the way its operations are implemented. Evaluation of elements in a sequence is lazy. Thus, sequences can be used to perform chaining operations on collection elements more efficiently, without the need to thread through additional collections to hold intermediate results produced during the process. You can call the extension function asSequence to convert any set to a sequence and toList to do the reverse conversion.
Perform sequence operations: intermediate and terminal operations
Sequence operations fall into two categories: intermediate and terminal. An intermediate operation returns another sequence that knows how to transform the elements in the original sequence. An end operation returns a result, which can be a set, an element, a number, or any other object retrieved from the sequence of transformations of the original set.
People.assequence ().map{.. }.filter {.. }.toList()Copy the code
Intermediate operations are always lazy. Let’s take a look at the following example without the terminal operation:
>>> listOf(1, 2, 3, 4).asSequence()
.map { print("map($it)"); it *it }
.filter { print("filter($it)"); it % 2 == 0 }Copy the code
Executing this code does not output anything on the console. This means that the map and filter transforms are deferred, and are only applied when the result is retrieved (i.e. when the end operation is called) :
>>> listOf(1, 2, 3, 4).asSequence()
.map { print("map($it)"); it *it }
.filter { print("filter($it)"); it % 2 == 0 } .toList() map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)Copy the code
The end operation triggers the execution of all deferred calculations. The other notable thing about this example is the order of execution. A clumsy approach would be to now call the map function on each element and then call the filter function on each element of the result sequence. Map and filter do this for collections, but sequences are different. For sequences, all operations are applied sequentially to each element, processing the first element (mapping first to filter), then the second element, and so on. This approach means that some elements do not undergo any transformation at all if the result is achieved before their turn. Let’s look at an example of map and find. First map a number to its square and then find the first entry that is greater than the number 3:
>>> println(listOf(1, 2, 3, 4)
.map { print("map($it); "); it * it }
.find { print("$it > 3 ?; "); it > 3 })
>>> println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -")
>>> println(listOf(1, 2, 3, 4).asSequence()
.map { print("map($it); "); it * it }
.find { print("$it > 3 ?; "); It > 3}) map(1); The map (2); The map (3); The map (4); 1 > 3? ; 4 > 3? ; 4 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the map (1); 1 > 3? ; The map (2); 4 > 3? ; 4Copy the code
In the first case, when you use a set, the list is transformed into another LIEB, so the map transformation applies to each element, including the numbers 3 and 4. Then the first element that satisfies the predicate is found: the number 2 squared. In the second case, the find call begins by processing the elements one by one. Take a number from the original sequence, map it, and then check that it meets the criteria passed to find. When we get to the number 2, we return that its square is already greater than 3, and we return it as the result of the find operation. It is no longer necessary to continue checking numbers 3 and 4, because you have already found the result. The order in which operations are performed on a collection also affects performance. Suppose you have a collection and want to print the names of people in the collection whose length is less than a certain limit. You need to do two things: map each person to their name and filter out any names that aren’t short enough. In this case, the Map and filter operations can be applied in any order. The two sequences get the same result, but the total number of changes they should perform is different:
>>> val people = listOf(Person("Hubert", 26), Person("Alice", 29),
Person("Bob", 31), Person("Dan", 21))
>>> println(people.asSequence()
.map { print("map(${it.name}); "); it.name }
.filter { print("filter($it); "); it.length < 4 } .toList()) >>> println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -")
>>> println(people.asSequence()
.filter { print("filter(${it.name}); "); it.name.length < 4 } .map {print("map($it); "); It.name}. ToList ()) map(Hubert); The filter (Hubert); The map (Alice); The filter (Alice); The map (Bob); The filter (Bob); The map (Dan); The filter (Dan); [Bob, Dan] ---------------- filter(Hubert); The filter (Alice); The filter (Bob); The map (Bob); The filter (Dan); The map (Dan); [Bob, Dan]Copy the code
As you can see, if map is in front, each element is transformed. If filter is first, inappropriate elements are filtered out as soon as possible without transformation.
If you’re familiar with the concept of flow in Java 8, you’ll see that sequences are a clone of it. Kotlin provides his own version of the concept because Java 8 streams do not support platforms based on older Versions of Java, such as Android. If your target version is Java 8, streams provide an important feature that Kotlin collections and sequences are not currently implemented: the ability to perform stream operations (such as map and filter) on multiple cpus in parallel. You can choose between streams and sequences depending on the target version of Java and your specific requirements.
Create a sequence
The previous lists all use the same method to create sequences: call asSquence() on the collection. Another possibility is to use the generateSequence function. Given the previous element in the sequence, this function evaluates the next element. Here is an example of how to use generateSequence to calculate the sum of all natural numbers up to 100.
>>> val naturalNumbers = generateSequence(0) { it + 1 }
>>> val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100.sum())
5050
Copy the code
NaturalNumbers and numbersTo100 in this example are both sequences with deferred operations. The actual numbers in these sequences are not evaluated until you call the end operation (in this case, sum). Another common use case is parent sequences. If an element’s parent is of the same type (such as a human or a Java file), you might be interested in the nature of the sequence of all its ancestors. The following example queries whether a file is in a hidden directory by creating a sequence of its parent directories and examining the properties of each directory.
fun File.isInsideHiddenDirectory() =
generateSequence(this) { it.parentFile }.any{ it.isHidden}
>>> val file = File("/Users/svtk/.HiddenDir/a.txt")
true
Copy the code
You generate a sequence by supplying the first element and retrieving each subsequent element. If you replace any with find, you can also get the desired directory (object). Note that using sequences allows you to stop traversing directories as soon as you find the one you need.
Use Java functional interfaces
Kotlin’s lambda also interoperates seamlessly with the Java API. At the beginning of the article, we pass lambda to an example of a Java method:
/* Kotlin */
button.setOnClickListener{ /* do someting */ }
Copy the code
The Button class sets a new listener for the Button via the setOnClickListener method that receives an argument of type OnClickListner. In Java (pre-8) we had to create an anonymous class to pass as an argument:
/* Java */
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
});
Copy the code
In Kotlin, you can pass a lambda instead of this example:
/* Kotlin */
button.setOnClickListener{ view -> ... }
Copy the code
This lambda is used to implement OnClickListener, which takes an argument of type View, the same as the onclick method. 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. Java apis are littered with functional interfaces like Runnable and Callable, and the methods that support them. Kotlin allows you to use lambdas when calling methods that take functional interfaces as arguments to keep your Kotlin code clean and consistent.
Unlike Java, Kotlin has full function types. Because of this, Kotlin functions that need to accept lambdas as arguments should use function types, rather than functional interface types, as the types of those arguments. Kotlin does not support automatic conversion of lambdas to Kotlin interface objects. We will discuss the use of declared function types in a later section.
Curious as to what happens when you pass lambda to Java and how it fits together?
Pass lambda as an argument to the Java method
A method that can pass a lambda to any expected functional interface. For example, this method takes a parameter of type Runnable:
/* Java */
void postponeComputation(int delay, Runnable computation);
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 we say an instance of Runnable, we mean an instance of an anonymous inner class that implements the Runnable interface. The compiler creates it for you and uses lambda as the body of a single abstract method. The same effect can be achieved by explicitly creating an anonymous object that implements Runnable:
postponeComputation(1000, object: Runnable {
override fun run() {
println(42)
}
})
Copy the code
But here’s a little bit different. When you explicitly declare an object, each call creates a new instance. The case with lambda is different: if the lambda does not access any variables from the function that defines it, the corresponding anonymous class instance can be reused between multiple calls. So a perfectly equivalent implementation would be to display the object declaration in the following code, which stores the Runnable instance in a variable and uses that variable every time it is called:
val runnable = Runnable { println(42) }
fun handleComputation() {
postponeComputation(1000, runnable)
}
Copy the code
If lambda captures variables from the enclosing scope, it is no longer possible to reuse the same instance with each call. In this case, the compiler creates a new object with the value of the captured variable at each call.
Funhandlecomputation (id: String) {// Lambda will capture the id variable postponeComputation(1000) {println(id)} // create a new Runnable instance each time}Copy the code
Implementation details of Lambda
Since Kotlin 1.0, every lambda expression is compiled into an anonymous class, unless it is an inline lambda. Subsequent releases are planned to support Java 8 bytecode generation. Once implemented, the compiler can avoid generating a separate.class file for each lambda expression. If lambda captures variables, each captured variable will have a corresponding field in an anonymous class, and a new instance of that anonymous class will be created with each call. Otherwise, a singleton is created. Class names are derived from the name of the function in which the lambda declaration resides by suffixing it: the above example is HandleComputation$1. If you decompile the code for the previous lambda expression, you’ll see:
class HandleComputationThe $1(val id: String) : Runnable {
override fun run() {
println(42)
}
}
fun handleComputation(id: String) {
postponeComputation(100, HandleComputationThe $1(id))
}
Copy the code
As you can see, the compiler generates a field and constructor parameter for each captured variable.
SAM constructor: Explicitly converts lambda to a functional interface
SAM constructors are compiler-generated functions that let you perform an explicit conversion from a lambda to a functional interface instance. It can be used in contexts where the compiler does not automatically apply the transformation. For example, if you have a method that returns an instance of a functional interface, instead of returning a lambda directly, use the SAM constructor to wrap it around:
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All Done!") }
}
>>> createAllDoneRunnable().run()
All Done!
Copy the code
The name of the SAM constructor is the same as the name of the underlying functional interface. The SAM constructor takes a single argument — a lambda used as the single abstract method body of a functional interface — and returns an instance of the class that implements the interface. In addition to the return value, the SAM constructor can be used when you need to store the instance of a functional interface from the lambda province in a variable. Suppose you want to reuse the same listener on multiple buttons, as in the following code:
val listener = OnClickListener { view ->
val text = when(view.id) {
R.id.button1 -> "First Button"
R.id.button2 -> "Second Button"
else -> "Unknown Button"
}
toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
Copy the code
The listener checks which button is the source of the click event and acts accordingly. You can define listeners using an object declaration that implements OnClickListener, but the SAM constructor gives you a cleaner choice.
Lambda and add/remove listeners
Note that there is no such thing as an anonymous object inside a lambda: there is no way to refer to the anonymous class instance to which the lambda is converted, and from the compiler’s point of view, a lambda is a block of code, not an object, and it cannot be treated as an object reference. The this in Lambda refers to the class that surrounds it. If your event listener also needs to cancel itself while handling the event, lambda cannot do so. This case uses an anonymous object that implements the interface, where the this keyword points to an instance of the object that can be passed to the listener removal API.
Lambda with receiver: with and apply
With the function
Many grammars have statements that allow you to perform multiple operations on the same object without having to write out the object’s name repeatedly. Kotlin is no exception, but it provides a library function called with rather than some special language construct. To understand this usage, let’s first look at the following example, which you will later reconstruct with:
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'.'Z') {
result.append(letter)
}
result.append("\nNow I know the alphabet!")
return result.toString()
}
>>> println(alphabet)
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!
Copy the code
In the example above, you call several different methods on the Result instance and repeat the name result each time. This isn’t too bad, but what if you use longer expressions or repeat them more often? Let’s look at rewriting this code using the with function:
fun alphabet(): String {
val stringBuilder = StringBuilder()
returnWith (stringBuilder) {// Specify the value of the receiver whose method you will callfor (letter in 'A'.'Z') {this.append(letter) // Call the receiver worthy method by explicitly calling this} append("\nNow I know the alphabet!") //this can omit this.tostring () // return}} from lambda.Copy the code
The with structure looks like a special syntax construct, but it’s actually a function that takes two arguments: in this case, a stringBuilder and a lambda. This takes advantage of the convention to place the lambda outside parentheses so that the entire call looks like a built-in language feature. You can also write it as with(stringBuilder, {… }). The with function converts its first argument to the second argument passed to the recipient of its lambda. The receiver can be explicitly accessed through this reference. Alternatively, you can omit the this reference and access the value method and property directly without any qualifiers. In this example this points to stringBuilder, which is the first argument passed to with.
Lambda with receiver and extension function
You may recall seeing a similar concept before, where this refers to the receiver of the function. Inside the extension function body, this refers to an instance of that type of the function, and can also be omitted, giving you direct access to the receiver’s members. An extension function is in a sense a function with a receiver.
Let’s further refactor the original Alphabet function to remove the additional stringBuilder variables:
fun alphabet(): String = with(StringBuilder()) {
for (letter in 'A'.'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
Copy the code
This function now returns only one expression, so it is overwritten using expression function body syntax. You can create a new Instance of StringBuilder and pass it directly to the function as an argument, and then reference it in the lambda without explicitly referring to this.
Method name conflict
What if the object you pass to with as an argument already has a method with the same name as the method in the class you’re using? In this case, you can explicitly label this reference to indicate which method you want to call. Assume that the function’s alphabet is a method of class OuterClass. If you want to refer to the toString method that defines an external class instead of StringBuilder, you can use statements like this:
[email protected]()
Copy the code
The value returned by with is the result of executing the lambda code, which is the last expression in the lambda. But sometimes you want to return the recipient object rather than the result of executing a lambda. This is where the Apply library function comes in.
The apply function
The Apply function is almost identical to the with function, except that apply always returns the object passed to it as an argument (the receiver object). Let’s refactor the Alphabet function again, this time using apply:
fun alphabet(): String = StringBuilder().apply {
for (letter in 'A'.'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
Copy the code
Apply is declared as an extension function whose receiver programs the receiver of the lambda as an argument. The result of applying is StringBuilder, so you can then call toString to convert it toString. There are many situations where Apply works, one of which is when creating an object instance requires that some of its properties be initialized in the right way. In Java, this is usually done through a separate Builder object, whereas in Kotlin, you can use Apply on any object without any special support from the library that defines it. Let’s use Apply to demonstrate an example of creating a TextView instance in Android:
fun createViewWithCustomAttr(context: Context) =
TextView(context).apply {
text = "Sample Text"TextSize = 20.0 fsetPadding(10, 0, 0, 0)
}
Copy the code
The Apply function allows you to use a compact expression style. The new TextView instance is passed to Apply immediately after it is created. In the lambda passed to Apply, the TextView instance becomes the receiver, and you can call its methods and set its properties. After Lambda is executed, apply returns the initialized recipient instance, which becomes the result of the createViewWithCustomAttr function. The with and apply functions are the most basic and general examples of using a lambda with a receiver. More specific function functions can also use this pattern. For example, you can further simplify the Alphabet function by using the standard library function buildString, which creates StringBuilder and calls toString:
fun alphabet(): String = buildString {
for (letter in 'A'.'Z') {
append(letter)
}
append("\nNow I know the alphabet!")}Copy the code