Lambda programming
1. Lambda expressions and member references
Introduction to Lambda: code blocks as function parameters
// lambda expression presentation listener button.setOnClickListener { /* ... */ }
Lambda and sets
data class Person( val name: String, val age: Int ) >>> val list = listOf(Person("kerwin", 12), Person("Bob", 23)) // Use lambda to search the collection and compare the age to find the largest element >>> // fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? // Receive an element in a collection as an argument (use it to refer to it) and return the value used for comparison, concise writing // If there is only one parameter lambda, and the type of this parameter can be derived, the default parameter name it will be generated >>> println(list.maxBy { it.age }) // The code snippet in curly braces is a lambda expression, which is passed to this function as an argument // This lambda takes a parameter of type Person and returns its age >>> list.maxBy { person -> person.age } // If a lambda happens to be a delegate of a function or property, you can replace it with a member reference >>> println(list.maxBy(Person::age))
Syntax of Lambda expressions
A lambda encodes a behavior, passes it around as a value, and can be declared independently and stored in a variable.
// Syntax of lambda expression // Parameter - > function body { x: Int, y: Int -> x + y } // kotlin's lambda expression is always surrounded by curly braces ({}). // Arguments are not enclosed in parentheses. Arrows separate the argument list from the function body of the lambda
You can store lambda expressions in a variable and treat this variable as a normal function
// Use variable to store lambda, parameter type cannot be derived, parameter type must be specified explicitly val sum = { x: Int, y: Int -> x + y } println(sum(1, 2)) // Call lambda saved in variable
In kotlin, if the lambda expression is the last argument of a function call, it can be placed outside the bracket
list.maxBy() { person: Person -> person.age } // When lambda is the only argument of a function, empty parentheses in the calling code can be removed // Write out parameter types explicitly list.maxBy { person: Person -> person.age } // The parameter type can not be written, and will be deduced according to the context list.maxBy { person -> person.age }
Use named arguments to pass lambda
val list = listOf(Person("kerwin", 12), Person("Bob", 23)) // Passing lambda as a named argument val names = list.joinToString(separator = " ", transform = { person: Person -> person.name }) println(names) // kerwin Bob // You can send lambda out of parentheses val names = list.joinToString(separator = " ") { person: Person -> person.name }
Accessing variables in scope
You can use lambda inside a function, access its parameters, and define local variables before lambda
// Using function parameters in lambda fun printMessageWithPrefix(messages: Collection<String>, prefix: String) { messages.forEach { // Accessing the prefix parameter in lambda println("$prefix $it") } } val messages = listOf("404", "403") printMessageWithPrefix(messages, "error: ")
kotlin allows the lambda to access non final variables internally or even modify them, and access external variables from within the lambda, which is said to be captured by the lambda.
Member reference
// Member reference syntax, using:: operator // Class: Members Person::age
: operator can convert a function to a value, such as: val getAge = Person::age, which is called member reference
You can also reference top-level functions (not members of a class)
fun test() = println("test") // Reference to the top-level function, omitting the name of the class, directly starting with:: // Member reference:: test is passed to the library function as an argument, which calls the corresponding function run(::test)
If a lambda is to delegate to a function that takes multiple parameters, it is very convenient to replace it with a member reference
// This lambda is delegated to the sendEmail function val action = { person: Person, message: String -> sendEmail(person, message) } // Member references can be used instead val nextAction= ::sendEmail >>> action(Person("kerwin",12), "Having dinner") >>> nextAction(Person("bob",34), "Having dinner")
You can use constructor references to store or defer the action of creating class instances. A constructor reference takes the form of specifying a class name after a double colon (::)
data class Person( val name: String, val age: Int ) // Construct method reference, the action of creating Person instance is saved as value val createPerson = ::Person val person = createPerson("kerwin", 23) println(person)
Extension functions can also be referenced
// Extension function of Person class fun Person.isAdult() = this.age >= 21 val person = Person("kerwin", 22) // Although isAdult is not a member of the Person class, it can also be accessed by reference val predicate = Person::isAdult println(predicate(person))
2. Functional API of set
Foundation: filter and map
Bottom source code of filter function:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { return filterTo(ArrayList<T>(), predicate) } public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C { for (element in this) if (predicate(element)) destination.add(element) return destination }
From the source code of the filter function, it can be seen that the filter function traverses the set and selects the elements that meet the given lambda and return true. These elements that meet the conditions are stored in the new set.
val list = listOf(1, 2, 3, 4) // Filter out even elements println(list.filter { it % 2 == 0 }) // [2, 4] val list = listOf(Person("kerwin", 23), Person("Bob", 12)) // Filter out people over 18 println(list.filter { it.age >= 18 }) // [Person(name=kerwin, age=23)]
map function source code:
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> { return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform) } public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C { for (item in this) destination.add(transform(item)) return destination }
It can be seen from the source code of map function that map function applies the given lambda to each element in the collection and stores the result in a new collection. The number of elements in the new set remains the same, but each element is transformed according to the given lambda.
val list = listOf(1, 2, 3, 4) println(list.map { it * it }) // [1, 4, 9, 16] val list = listOf(Person("kerwin", 23), Person("Bob", 12)) // Just a list of names, you can use map transformation println(list.map { it.name }) // kerwin, Bob] // You can use member references list.map(Person::name) // filter and map can be combined // Output names older than 18 println(list.filter { it.age >= 18 }.map { it.name }) // [kerwin] // Find out the oldest person: first find the oldest person in the set, and then filter the oldest person val maxAge = list.maxBy(Person::age)?.age println(list.filter { it.age == maxAge })
You can also apply filtering and transformation functions to Map sets:
val map = mapOf(1 to "one", 2 to "two") println(map.mapValues { it.value.toUpperCase() }) // {1=ONE, 2=TWO} // Keys and values are handled by their respective functions. // filterKeys and mapKeys filter and transform Map keys // filterValues and mapValues filter and transform Map values
all, any, count, find: apply judgment formula to set
All and any functions check whether all elements in the collection meet a certain condition (or its variant, whether there are elements that meet);
The count function checks how many elements satisfy the judgment formula;
The find function returns the first eligible element.
Bottom source code of all function:
/** * Returns `true` if all elements match the given [predicate]. */ public inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean { if (this is Collection && isEmpty()) return true for (element in this) if (!predicate(element)) return false return true }
From the source code of all function, it can be seen that only when all elements in the collection meet the conditions can true be returned, otherwise false will be returned.
val list = listOf(Person("kerwin", 12), Person("bob", 19)) val result = list.all { person: Person -> person.age >= 18 } println(result) false
any function source code:
public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean { if (this is Collection && isEmpty()) return false for (element in this) if (predicate(element)) return true return false }
From any function, we can see that as long as one element in the collection meets the condition, it will return true, otherwise it will return false
val list = listOf(Person("kerwin", 12), Person("bob", 19)) val result = list.any { person: Person -> person.age >= 18 } println(result) true
Bottom source code of count function:
public inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int { if (this is Collection && isEmpty()) return 0 var count = 0 for (element in this) if (predicate(element)) checkCountOverflow(++count) return count }
From the count function, we can know: the number of elements in the set that satisfy the condition
val result = list.count { person: Person -> person.age >= 10 } println(result) 2
Bottom source code of find function:
public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? { return firstOrNull(predicate) } public inline fun <T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean): T? { for (element in this) if (predicate(element)) return element return null }
It can be seen from the find function that: find the first element in the collection that meets the condition, find and return the element, otherwise return null
val result = list.find { person: Person -> person.age >= 18 } println(result) Person(name=bob, age=19)
groupBy: map that converts a list into a group
Bottom source code of groupBy function:
public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> { return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector) } public inline fun <T, K, M : MutableMap<in K, MutableList<T>>> Iterable<T>.groupByTo(destination: M, keySelector: (T) -> K): M { for (element in this) { val key = keySelector(element) val list = destination.getOrPut(key) { ArrayList<T>() } list.add(element) } return destination } public inline fun <K, V> MutableMap<K, V>.getOrPut(key: K, defaultValue: () -> V): V { val value = get(key) return if (value == null) { val answer = defaultValue() put(key, answer) answer } else { value } }
According to the groupBy function, all elements in the set are divided into different groups according to different characteristics, and the Map set is returned. key: grouping condition. value: List collection, each group is stored in a List.
val list = listOf( Person("kerwin", 12), Person("bob", 19), Person("Alice", 12) ) // Group people of the same age into groups val groupList = list.groupBy { person: Person -> person.age } println(groupList) {12=[Person(name=kerwin, age=12), Person(name=Alice, age=12)], 19=[Person(name=bob, age=19)]}
flatMap and flatten: handling elements in nested collections
The underlying source code of flatMap function:
public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> { return flatMapTo(ArrayList<R>(), transform) } public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C { for (element in this) { val list = transform(element) destination.addAll(list) } return destination }
From the flatMap function, we can see that each element is transformed (or mapped) according to the expression given by lambda (to return the iteratable subclass), and then multiple lists are merged (or tiled) into a list.
val books = listOf( Book("java", listOf("abc", "bcd")), Book("kotlin", listOf("wer")) ) // The authors of each book in the statistical collection merge into a flat list val bookAllAuthors = books.flatMap { book: Book -> book.authors } println(bookAllAuthors) [abc, bcd, wer]
3. Lazy set operation: sequence
When many chained set functions are called, such as map and filter, these functions will create intermediate sets as early as possible, that is to say, the intermediate results of each step are stored in a temporary list. Sequences avoid creating these temporary intermediate objects.
// This creates temporary intermediate objects // Features: first call the map function on each element, then call the filter function on each element in the result list list.map { println("map: $it") it.name } .filter { println("filter: $it") it.startsWith("k") } .toList() // In order to improve efficiency, you can first turn operations into sequences rather than directly using sets // Features: all operations are to finish processing the first element (mapping and filtering first), then the second element, and so on list.asSequence() // Convert initial set to sequence .map { println("map: $it") it.name } // Sequence supports the same API as collection .filter { println("filter: $it") it.startsWith("k") } .toList() // Convert result sequence back to list, reverse conversion
The entry of kotlin lazy collection operation is: Sequence interface, which represents a Sequence of elements that can be enumerated. It provides only one method, iterator, to get the value from the Sequence. The evaluation of elements in a Sequence is lazy, so using Sequence can perform chain operation on collection elements more efficiently.
public interface Sequence<out T> { /** * Returns an [Iterator] that returns the values from the sequence. * * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time. */ public operator fun iterator(): Iterator<T> }
Performing sequence operations: middle and end operations
Sequence operations fall into two categories: intermediate and terminal. An intermediate operation returns another sequence, a new sequence knows how to transform the elements in the original sequence; an end operation returns a result, which may be a set, element, number or any object obtained from the transformation sequence of the initial set.
list.asSequence() .map(Person::name).filter { it.startsWith("k") } // Intermediate operation, always inert .toList() // Terminal operation
Create sequence
In addition to calling asSequence() on a collection to create a sequence, you can use the generateSequence function.
// Calculate the sum of all natural numbers within 100 val naturalNumbers = generateSequence(0) { it + 1 } val numbersTo100 = naturalNumbers.takeWhile { it <= 100 } // When the result sum is obtained, all delayed operations are performed println(numbersTo100.sum()) // 5050
The takeWhile function is in the lower source code of the collection:
public inline fun <T> Iterable<T>.takeWhile(predicate: (T) -> Boolean): List<T> { val list = ArrayList<T>() for (item in this) { if (!predicate(item)) break list.add(item) } return list }
4. Using java functional interface
kotlin's lambda can seamlessly interoperate with Java APIs.
Pass lambda as a parameter to java method
You can pass a lambda to any method that expects a functional interface.
//java void postponeComputation(int delay, Runnable computation) //In kotlin, you can pass lambda as an argument to it, and the compiler will automatically convert it to a Runnable instance // "A Runnable instance": refers to an instance of an anonymous class that implements the Runnable interface // Only one instance of Runnable will be created in the whole program postponeComputation(1000) { println("kotlin") } // Object expressions can also be passed as implementation of functional interfaces // This method creates a new instance every time it is called postponeComputation(1000,object : Runnable { override fun run() { } }) // It is programmed as a global variable. There is only one instance in the program, and it is the same object every time it is called val runnable = Runnable { println("kotlin") } fun handleComputation() { postponeComputation(1000, runnable) }
lambda captures variables from the scope surrounding it, and it is no longer possible to reuse the same instance every time it is called
fun handleComputation(id: String) { // lambda will capture the id variable // A new instance of Runnable is created each time handleComputation is called postponeComputation(1000) { println(id) } }
Construction method of SAM: explicitly converting lambda into functional interface
The SAM constructor is a compiler generated function that lets you perform an explicit conversion from a lambda to a functional interface instance.
An interface with a single abstract method, called the SAM interface
If a method returns an instance of a functional interface, instead of returning a lambda directly, you need to wrap it with a SAM construction method
// Return value using SAM construction method // The SAM constructor has the same name as the underlying functional interface // The SAM constructor takes only one parameter (a lambda used as a single abstract method body of a functional interface) and returns an instance of the class that implements the interface fun callAllDoneRunnable() : Runnable { return Runnable { println("All Done.") } } callAllDoneRunnable().run()
SAM construction method can also be used to store the functional interface instance generated from lambda in a variable
// Using SAM construction method to reuse listener instance val listener = OnClickListener { view -> // Use view.id to determine which button is clicked val text = when(view.id) { R.id.button1 -> "First Button" R.id.button1 -> "Second Button" else -> "Unknown Button" } toast(text) } button1.setOnClickListener(listener) button2.setOnClickListener(listener)
5. lambda with receiver: with and apply
A method of a different object can be called in the body of a lambda function without any additional qualifiers. Such a lambda is called a lambda with a receiver
with function
with function: it can be used to perform multiple operations on the same object without repeatedly writing out the name of the object.
// This code calls several different methods on the result instance, and the result name is repeated every time fun alphabet(): String { val result = StringBuilder() for (letter in 'A'..'Z') { result.append(letter) } result.append("\nNow I known the alphabet") return result.toString() }
Use the with function to rewrite the previous code, and the with function takes two parameters.
// fun <T, R> with(receiver: T, block: T.() -> R): R // Using with construction fun alphabet(): String { val stringBuilder = StringBuilder() // Specifies the value of the recipient return with(stringBuilder) { for (letter in 'A'..'Z') { // Method to call receiver value through explicit this this.append(letter) } // Omit this to call methods append("\nNow I known the alphabet") // Return value from lambda this.toString() } }
Using with and an expression function body to build the alphabet
// Use expression body syntax fun alphabet() = with(StringBuilder()) { for (letter in 'A'..'Z') { append(letter) } append("\nNow I known the alphabet") toString() }
apply function
The apply function always returns the object passed to it as an argument (in other words, the recipient object)
// Its receiver becomes the lambda receiver as an argument, and the result of apply ing is StringBuilder fun alphabet() = StringBuilder().apply { for (letter in 'A'..'Z') { append(letter) } append("\nNow I known the alphabet") }.toString()
In java, it is usually completed by a separate Builder object; in kotlin, you can use the apply function on any object without any custom object.
// Initializing a TextView with apply fun createViewWithCustomAttributes(context: Context) = TextView(context).apply { text = "Sample Text" textSize = 20.0 setPading(10, 0, 0, 0) }
You can use the kotlin standard library function buildString, which is responsible for creating StringBuilder and calling toString
fun alphabet() = buildString { for (letter in 'A'..'Z') { append(letter) } append("\nNow I known the alphabet") }