Kotlin基础 -- 2

五、Lambda编程

1.Lambda表达式和成员引用

Lambda简介:作为函数参数的代码块。可以理解为简化表达后的匿名函数,实质上它就是一种语法糖。

用匿名内部类实现监听器:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.d("TAG", "zwm, test java")
    }
});

用lambda实现监听器:

button.setOnClickListener{ Log.d(TAG, "zwm, test lambda") }

Lambda和集合

手动在集合中搜索:

class Person(val name: String, val age: Int)

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

日志打印:
2020-08-19 15:39:44.995 15807-15807/? D/TAG: zwm, theOldest: Java 20

用lambda在集合中搜索:

class Person(val name: String, val age: Int)

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy { it.age }
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

日志打印:
2020-08-19 15:41:39.258 16117-16117/com.tomorrow.kotlindemo D/TAG: zwm, theOldest: Java 20

maxBy函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值来找到最大元素。

如果lambda刚好是函数或者属性的委托,可以用成员引用替换:

class Person(val name: String, val age: Int)

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy(Person::age )
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

日志打印:
2020-08-19 15:47:17.127 16493-16493/com.tomorrow.kotlindemo D/TAG: zwm, theOldest: Java 20

Lambda表达式的语法

{ x: Int, y: Int -> x + y } //花括号,参数 -> 函数体

可以把lambda表达式存储在一个变量中,把这个变量当做普通函数对待(即通过相应实参调用它):

fun method() {
    val sum = { x: Int, y: Int -> x + y }
    Log.d("TAG", "zwm, sum: ${sum(1, 2)}")
}

日志打印:
2020-08-19 15:55:09.737 18193-18193/? D/TAG: zwm, sum: 3

还可以直接调用lambda表达式:

fun method() {
    Log.d("TAG", "zwm, sum: ${{ x: Int, y: Int -> x + y }(1, 2)}")
}

日志打印:
2020-08-19 15:55:51.879 18556-18556/com.tomorrow.kotlindemo D/TAG: zwm, sum: 3

可以使用库函数run来执行传递给它的lambda:

fun method() {
    Log.d("TAG", "zwm, sum: ${run{{ x: Int, y: Int -> x + y }(1, 2)}}")
}

日志打印:
2020-08-19 16:01:36.906 19393-19393/com.tomorrow.kotlindemo D/TAG: zwm, sum: 3

Lambda表达式的简明语法推导

最初版本:

class Person(val name: String, val age: Int)

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy({ p: Person -> p.age })
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

Kotlin有这样一种语法约定:如果lambda表达式是函数调用的最后一个实参,它可以放到括号的外边:

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy(){ p: Person -> p.age }
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

当lambda是函数唯一的实参时,还可以去掉调用代码中的空括号对:

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy{ p: Person -> p.age }
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

和局部变量一样,如果lambda参数的类型可以被推导出来,就不需要显式地指定它:

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy{ p -> p.age }
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

如果当前上下文期望的是只有一个参数的lambda且这个参数的类型可以推断出来,可以使用默认参数名称it代替命名参数:(仅在实参名称没有显式地指定时这个默认的名称才会生成)

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy{ it.age }
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

如果用变量存储lambda,那么就没有可以推断出参数类型的上下文,必须显式地指定参数类型:

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    val getAge = { p: Person -> p.age }
    var theOldest = people.maxBy(getAge)
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型。

在作用域中访问变量

当在函数内声明一个匿名内部类的时候,能够在这个匿名内部类内部引用这个函数的参数和局部变量。也可以用lambda做同样的事情。如果在函数内部使用lambda,也可以访问这个函数的参数,还有在lambda之前定义的局部变量。

在lambda中使用函数参数:

fun method(name: String, age: Int) {
    { Log.d("TAG", "zwm, name: $name, age: $age") }()
}

日志打印:
2020-08-19 16:48:51.001 26858-26858/com.tomorrow.kotlindemo D/TAG: zwm, name: hello, age: 9

在lambda中改变局部变量:

fun method(name: String, age: Int) {
    var address = "bj"
    {
        address = "gz"
        Log.d("TAG", "zwm, name: $name, age: $age, address: $address")
    }()
    Log.d("TAG", "zwm, address: $address")
}

日志打印:
2020-08-19 17:06:33.875 28625-28625/? D/TAG: zwm, name: hello, age: 9, address: gz
2020-08-19 17:06:33.876 28625-28625/? D/TAG: zwm, address: gz

和Java不一样,Kotlin允许在lambda内部访问非final变量甚至修改它们。从lambda内访问外部变量,称这些变量被lambda捕捉。默认情况下,局部变量的生命期被限制在声明这个变量的函数中。但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行。

成员引用语法:

类::成员 //双冒号把类名称与要引用的成员(一个方法或者一个属性)名称隔开

引用属性:

class Person(val name: String, val age: Int)

fun method() {
    val people = listOf(Person("Android", 10), Person("Java", 20), Person("Kotlin", 5))
    var theOldest = people.maxBy(Person::age )
    Log.d("TAG", "zwm, theOldest: ${theOldest?.name} ${theOldest?.age}")
}

日志打印:
2020-08-19 17:14:23.246 28854-28854/com.tomorrow.kotlindemo D/TAG: zwm, theOldest: Java 20

引用顶层函数:

fun topMethod(language: String) {
    Log.d("TAG", "zwm, this is top method: $language")
}

fun method() {
    (::topMethod)("Kotlin")
}

2020-08-19 17:40:17.769 9798-9798/com.tomorrow.kotlindemo D/TAG: zwm, this is top method: Kotlin

如果lambda要委托给一个接收多个参数的函数,提供成员引用代替它将会非常方便:

class Person(val name: String, val age: Int)

fun sendEmail(person: Person, content: String) {
    Log.d("TAG", "zwm, sendEmail")
}

fun method() {
    val action = { person: Person, content: String -> sendEmail(person, content) } //这个lambda委托给sendEmail函数
    val nextAction = ::sendEmail //可以用成员引用代替
}

可以用构造方法引用存储或者延期执行创建类实例的动作。构造方法引用的形式是在双冒号后指定类名称:

class Person(val name: String, val age: Int)

fun method() {
    val createPerson = ::Person
    val p = createPerson("Kotlin", 5)
}

还可以用同样的方式引用扩展函数:

class Person(val name: String, val age: Int)

fun Person.isAdult() = age >= 21

fun method() {
    val adult = Person::isAdult //尽管isAdult是扩展函数不是Person类的成员,还是可以通过引用访问它
    val person = Person("Android", 10)
    Log.d("TAG", "zwm, ${adult(person)}")
    Log.d("TAG", "zwm, ${person.isAdult()}")
}

日志打印:
2020-08-19 18:05:07.156 14079-14079/com.tomorrow.kotlindemo D/TAG: zwm, false
2020-08-19 18:05:07.156 14079-14079/com.tomorrow.kotlindemo D/TAG: zwm, false

绑定引用:

class Person(val name: String, val age: Int)

fun method() {
    val p = Person("Android", 10)
    val ageFunction = Person::age //使用类引用成员
    Log.d("TAG", "zwm, ageFunction: ${ageFunction(p)}")

    val ageFunction2 = p::age //使用对象引用成员
    Log.d("TAG", "zwm, ageFunction2: ${ageFunction2()}")
}

日志打印:
2020-08-19 19:43:57.800 20766-20766/com.tomorrow.kotlindemo D/TAG: zwm, ageFunction: 10
2020-08-19 19:43:57.801 20766-20766/com.tomorrow.kotlindemo D/TAG: zwm, ageFunction2: 10

2.集合的函数式API

基础:filter和map

filter函数遍历集合并选出应用给定lambda后会返回true的那些元素,结果是一个新的集合:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.filter { it.age >= 10 }
    for((index, item) in result.withIndex())
    Log.d("TAG", "zwm, index: $index, name: ${item.name}")
}

日志打印:
2020-08-24 00:51:35.432 20309-20309/com.tomorrow.kotlindemo D/TAG: zwm, index: 0, name: Java
2020-08-24 00:51:35.432 20309-20309/com.tomorrow.kotlindemo D/TAG: zwm, index: 1, name: Android

map函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.map { it.name }
    for((index, item) in result.withIndex())
    Log.d("TAG", "zwm, index: $index, item: $item")
}

日志打印:
2020-08-24 01:03:01.554 20756-20756/com.tomorrow.kotlindemo D/TAG: zwm, index: 0, item: Kotlin
2020-08-24 01:03:01.554 20756-20756/com.tomorrow.kotlindemo D/TAG: zwm, index: 1, item: Java
2020-08-24 01:03:01.554 20756-20756/com.tomorrow.kotlindemo D/TAG: zwm, index: 2, item: Android
class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.map(Person::name)
    for((index, item) in result.withIndex())
    Log.d("TAG", "zwm, index: $index, item: $item")
}

日志打印:
2020-08-24 01:04:08.179 21530-21530/? D/TAG: zwm, index: 0, item: Kotlin
2020-08-24 01:04:08.179 21530-21530/? D/TAG: zwm, index: 1, item: Java
2020-08-24 01:04:08.179 21530-21530/? D/TAG: zwm, index: 2, item: Android

filter和map:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.filter { it.age >= 10 }.map(Person::name)
    for((index, item) in result.withIndex())
    Log.d("TAG", "zwm, index: $index, item: $item")
}

日志打印:
2020-08-24 01:06:41.200 21911-21911/com.tomorrow.kotlindemo D/TAG: zwm, index: 0, item: Java
2020-08-24 01:06:41.200 21911-21911/com.tomorrow.kotlindemo D/TAG: zwm, index: 1, item: Android

还可以对map应用过滤和变换函数,map的键和值分别由各自的函数来处理,filterKeys和mapKeys过滤和变换map的键,而另外的filterValues和mapValues过滤和变换对应的值。

fun method() {
    val numbers = mapOf(0 to "zero", 1 to "one", 2 to "two")
    Log.d("TAG", "zwm, filterKeys: ${numbers.filterKeys { it >= 1 }}")
    Log.d("TAG", "zwm, mapKeys: ${numbers.mapKeys { it.key + 10 }}")
    Log.d("TAG", "zwm, filterValues: ${numbers.filterValues { it == "two" }}")
    Log.d("TAG", "zwm, mapValues: ${numbers.mapValues { it.value + "_map" }}")
}

日志打印:
2020-08-24 01:25:09.827 25896-25896/? D/TAG: zwm, filterKeys: {1=one, 2=two}
2020-08-24 01:25:09.829 25896-25896/? D/TAG: zwm, mapKeys: {10=zero, 11=one, 12=two}
2020-08-24 01:25:09.829 25896-25896/? D/TAG: zwm, filterValues: {2=two}
2020-08-24 01:25:09.829 25896-25896/? D/TAG: zwm, mapValues: {0=zero_map, 1=one_map, 2=two_map}

"all" "any" "count" 和 "find":对集合应用判断式

对是否所有元素都满足判断式感兴趣,应该使用all函数:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.all { it.age >= 5 }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 01:34:13.967 26347-26347/com.tomorrow.kotlindemo D/TAG: zwm, result: true

需要检查集合中是否至少存在一个匹配的元素,那就用any:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.any { it.age >= 20 }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 01:36:13.500 26662-26662/com.tomorrow.kotlindemo D/TAG: zwm, result: true

!all(不是所有)加上某个条件,可以用any加上这个条件的取反来替换:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = !list.all { it.age >= 20 }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 01:39:54.101 27422-27422/com.tomorrow.kotlindemo D/TAG: zwm, result: true
class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.any { it.age < 20 }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 01:41:00.679 27898-27898/? D/TAG: zwm, result: true

想知道有多少个元素满足了判断式,使用count:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.count { it.age < 20 }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 01:43:00.513 28113-28113/com.tomorrow.kotlindemo D/TAG: zwm, result: 2

要找到一个满足判断式的元素,使用find函数:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.find { it.age < 20 }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 01:45:23.639 28734-28734/com.tomorrow.kotlindemo D/TAG: zwm, result: Kotlin

如果有多个匹配的元素就返回其中第一个元素,如果没有一个元素能满足判断式则返回null。find还有一个同义方法firstOrNull,可以使用这个方法更清楚地表述你的意图。

groupBy:把列表转换成分组的map

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Kotlin", 5), Person("Java", 20), Person("Android", 10))
    val result = list.groupBy { it.age }
    for((key, value) in result) {
        Log.d("TAG", "zwm, key: $key")
        for((index, item) in value.withIndex()) {
            Log.d("TAG", "zwm, index: $index, name: ${item.name}")
        }
    }
}

日志打印:
2020-08-24 01:57:13.505 30160-30160/com.tomorrow.kotlindemo D/TAG: zwm, key: 5
2020-08-24 01:57:13.506 30160-30160/com.tomorrow.kotlindemo D/TAG: zwm, index: 0, name: Kotlin
2020-08-24 01:57:13.506 30160-30160/com.tomorrow.kotlindemo D/TAG: zwm, key: 20
2020-08-24 01:57:13.506 30160-30160/com.tomorrow.kotlindemo D/TAG: zwm, index: 0, name: Java
2020-08-24 01:57:13.506 30160-30160/com.tomorrow.kotlindemo D/TAG: zwm, key: 10
2020-08-24 01:57:13.506 30160-30160/com.tomorrow.kotlindemo D/TAG: zwm, index: 0, name: Android

flatMap和flatten:处理嵌套集合中的元素

flatMap函数做了两件事情:首先根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(或者说平铺)成一个列表:

fun method() {
    val list = listOf("abc", "def")
    val result = list.flatMap { it.toList() }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 02:06:29.554 30522-30522/com.tomorrow.kotlindemo D/TAG: zwm, result: [a, b, c, d, e, f]

如果你不需要做任何变换,只是需要平铺一个集合,可以使用flatten函数:

fun method() {
    val list1 = listOf("abc", "def")
    val list2 = listOf("ghi", "jkl")
    val list = listOf(list1, list2)
    val result =  list.flatten()
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 02:12:29.813 31375-31375/com.tomorrow.kotlindemo D/TAG: zwm, result: [abc, def, ghi, jkl]

3.惰性集合操作:序列

Kotlin标准库参考文档有说明:filter和map都会返回一个列表。这意味着使用filter和map的链式调用会创建两个列表:一个保存filter函数的结果,另一个保存map函数的结果。如果源列表只有两个元素,这不是什么问题,但是如果有一百万个元素,链式调用就会变得十分低效。为了提高效率,可以把操作变成使用序列,而不是直接使用集合,这样就不会创建任何用于存储元素的中间集合:

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Apple", 5), Person("Billy", 20), Person("Android", 10))
    val result = list.asSequence() //把初始集合转换成序列
        .map(Person::name)
        .filter { it.startsWith("A") }
        .toList() //把结果序列转换回列表
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 02:32:16.517 973-973/? D/TAG: zwm, result: [Apple, Android]

Kotlin惰性集合操作的入口就是Sequence接口,这个接口表示的就是一个可以逐个列举元素的元素序列。Sequence只提供了一个方法,iterator,用来从序列中获取值。可以调用扩展函数asSequence把任意集合转换成序列,调用toList来做反向的转换。

执行序列操作:中间和末端操作

class Person(val name: String, val age: Int)

fun method() {
    val list = listOf(Person("Apple", 5), Person("Billy", 20), Person("Android", 10))
    val result = list.asSequence() 
        .map(Person::name) //中间操作
        .filter { it.startsWith("A") } //中间操作
        .toList() //末端操作
    Log.d("TAG", "zwm, result: $result")
}

对序列来说,所有操作是按顺序应用在每一个元素上:处理完第一个元素(先映射再过滤),然后完成第二个元素的处理,以此类推。

创建序列

在集合上调用asSequence()可以创建一个序列,另外也可以使用generateSequence函数。给定序列中的前一个元素,这个函数会计算出下一个元素:

fun method() {
    val naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
    val result = numbersTo100.sum() //当获取结果时,所有被推迟的操作都被执行
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 02:58:10.859 3913-3913/com.tomorrow.kotlindemo D/TAG: zwm, result: 5050

4.使用Java函数式接口

把lambda当做参数传递给Java方法:

button.setOnClickListener { Log.d("TAG", "zwm, focusable: ${it.focusable}") }

这种方式可以工作的原因是OnClickListener接口只有一个抽象方法。这种接口被称为函数式接口,或者SAM接口,SAM代表单抽象方法。Java API中随处可见像Runnable和Callable这样的函数式接口,以及支持它们的方法。Kotlin允许你在调用接收函数式接口作为参数的方法时使用lambda,来保证你的Kotlin代码既整洁又符合习惯。

使用匿名对象:

button.setOnClickListener { object : Runnable {
    override fun run() {
        Log.d("TAG", "zwm, focusable: ${it.focusable}")
    }
} }

使用匿名对象时,每次调用都会创建一个新的实例。而使用lambda时,如果lambda没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用;如果lambda从包围它的作用域中捕捉了变量,每次调用就不再可能重用同一个实例了,这种情况下,每次调用时编译器都要创建一个新对象,其中存储着被捕捉的变量的值。

method(1000) { println(42) } //整个程序只会创建一个实例

fun outMethod(id: String) {
    method(1000) { println(id) } //每次调用都会创建一个新实例
}

Lambda的实现细节

自Kotlin 1.0起,每个lambda表达式都会被编译成一个匿名类,除非它是一个内联lambda。如果lambda捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次对lambda的调用都会创建一个这个匿名类的新实例。否则,一个单例就会被创建。

SAM构造方法:显示地把lambda转换成函数式接口

SAM构造方法是编译器生成的函数,让你执行从lambda到函数式接口实例的显示转换。可以在编译器不会自动应用转换的上下文中使用它。例如,如果有一个方法返回的是一个函数式接口的实例,不能直接返回一个lambda,要用SAM构造方法把它包装起来:

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All done!") }
}

fun method() {
    createAllDoneRunnable().run()
}

SAM构造方法的名称和底层函数式接口的名称一样。SAM构造方法只接收一个参数:一个被用作函数式接口单抽象方法体的lambda,并返回实现了这个接口的类的一个实例。

除了返回值外,SAM构造方法还可以用在需要把从lambda生成的函数式接口实例存储在一个变量中的情况:

val listener = OnClickListener { view -> //使用SAM构造方法来重用listener实例
    val text = when (view.id) { //根据view.id来判断点击的是哪个按钮
        R.id.button1 -> "First button"
        R.id.button2 -> "Second button"
        else -> "Unknown button"
    }   
    toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

Lambda和添加/移除监听器

注意lambda内部没有匿名对象那样的this:没有办法引用到lambda转换成的匿名类实例。从编译器的角度来看,lambda是一个代码块,不是一个对象,而且也不能把它当成对象引用。Lambda中的this引用指向的是包围它的类。如果你的事件监听器在处理事件时还需要取消它自己,不能使用lambda这样做。这种情况使用实现了接口的匿名对象。在匿名对象内,this关键字指向该对象实例,可以把它传给移除监听器的API。

5.带接收者的lambda:with与apply

with函数:

fun method() {
    val stringBuilder = StringBuilder()
    val result = with(stringBuilder) { //指定接收者的值,你会调用它的方法
        for (letter in 'A'..'Z') {
            this.append(letter) //通过显式的this来调用接收者值的方法
        }
        append("done") //省掉this也可以调用接收者值的方法
        this.toString()
    }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 15:14:55.028 1709-1709/com.tomorrow.kotlindemo D/TAG: zwm, result: ABCDEFGHIJKLMNOPQRSTUVWXYZdone

with结构看起来像是一种特殊的语法结构,但它实际上是一个接收两个参数的函数:以上例子中两个参数分别是stringBuilder和一个lambda。这里利用了把lambda放在括号外的约定,这样整个调用看起来就像是内建的语言功能。with函数把它的第一个参数转换成作为第二个参数传给它的lambda的接收者。可以显式地通过this引用来访问这个接收者,也可以省略this引用,不用任何限定符直接访问这个值的方法和属性。

带接收者的lambda和扩展函数

在扩展函数体内部,this指向了这个函数扩展的那个类型的实例,而且也可以被省略掉,让你直接访问接收者的成员。注意一个扩展函数某种意义上来说就是带接收者的函数。

方法名称冲突:

class Outer(val name: String) {
    fun method() {
        val stringBuilder = StringBuilder()
        val result = with(stringBuilder) {
            append(this@Outer.toString()) //使用this@Outer引用外部类实例
            append("done")
            this.toString() //使用this引用接收者实例
        }
        Log.d("TAG", "zwm, result: $result")
    }

    override fun toString(): String {
        return "I am Outer"
    }
}

日志打印:
2020-08-24 15:37:16.150 3594-3594/com.tomorrow.kotlindemo D/TAG: zwm, result: I am Outerdone

apply函数:

with函数返回的值是执行了lambda代码的结果,该结果就是lambda中的最后一个表达式的值。apply函数几乎和with函数一模一样,唯一的区别是apply始终会返回作为实参传递给它的对象(即接收者对象)。

fun method() {
    val stringBuilder = StringBuilder()
    val result = stringBuilder.apply {
        for (letter in 'A'..'Z') {
            this.append(letter)
        }
        append("done")
        this.toString()
    }.toString()
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 15:43:16.246 4227-4227/com.tomorrow.kotlindemo D/TAG: zwm, result: ABCDEFGHIJKLMNOPQRSTUVWXYZdone

apply被声明成一个扩展函数,它的接收者变成了作为实参的lambda的接收者。在Kotlin中,可以在任意对象上使用apply,完全不需要任何来自定义该对象的库的特别支持。

with函数和apply函数是最基本和最通用的使用带接收者的lambda的例子,更多具体的函数也可以使用这种模式。例如,可以使用标准库函数buildString,它会负责创建StringBuilder并调用toString。buildString的实参是一个带接收者的lambda,接收者就是StringBuilder:

fun method() {
    val result = buildString {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("done")
    }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 15:55:52.341 4814-4814/com.tomorrow.kotlindemo D/TAG: zwm, result: ABCDEFGHIJKLMNOPQRSTUVWXYZdone

六、Kotlin的类型系统

1.可空性

Kotlin的类型结构:

Kotlin的类型结构

Kotlin和Java的类型系统之间第一条也可能是最重要的一条区别是,Kotlin对可空类型的显式的支持。问号可以加在任何类型的后面来表示这个类型的变量可以存储null引用,例如:String?、Int?、MyCustomType? 等等。

Type? = Type or null

没有问号的类型表示这种类型的变量不能存储null引用。这说明所有常见类型默认都是非空的,除非显式地把它标记为可空。

一旦你有一个可空类型的值,能对它进行的操作也会受到限制:

fun method(str: String?) {
    val length = str.length //编译错误,str可能为null
    val x: String? = null //正确,x可以为null
    val y: String = x //编译错误,y不可以为null
}

安全调用运算符:?,允许你把一次null检查和一次调用合并成一个操作:

fun method() {
    val str : String? = null
    val length = str?.length //访问属性
    val ch = str?.get(0) //调用方法
    Log.d("TAG", "zwm, length: $length, ch: $ch")
}

日志打印:
2020-08-24 19:50:28.066 25302-25302/com.tomorrow.kotlindemo D/TAG: zwm, length: null, ch: null

Elvis运算符:?:,也叫做null合并运算符,接收两个运算数,如果第一个运算数不为null,运算结果就是第一个运算数,如果第一个运算数为null,运算结果就是第二个运算数:

fun method() {
    val str : String? = null
    val length = str?.length ?: 0
    Log.d("TAG", "zwm, length: $length")
}

日志打印:
2020-08-24 20:04:06.707 2443-2443/com.tomorrow.kotlindemo D/TAG: zwm, length: 0

安全转换:as?,尝试把值转换成指定的类型,如果值不是合适的类型就返回null:

class Person(val name: String, val age: Int)

fun method() {
    val str: String? = "hello"
    val result = str as? Person
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-24 21:03:33.696 18222-18222/com.tomorrow.kotlindemo D/TAG: zwm, result: null

非空断言:!!,使用双感叹号表示,可以把任何值转换成非空类型,如果对null值做非空断言,则会抛出异常:

fun method() {
    val str: String? = null
    val result = str!!.length
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
kotlin.KotlinNullPointerException

let函数,让处理可空表达式变得更容易。和安全调用运算符一起,它允许你对表达式求值,检查求值结果是否为null,并把结果保存为一个变量,所有这些动作都在同一个简洁的表达式中:

fun method() {
    val str: String? = "Kotlin"
    str?.let { Log.d("TAG", "zwm, value: $it") }
}

日志打印:
2020-08-24 21:48:55.457 25465-25465/com.tomorrow.kotlindemo D/TAG: zwm, value: Kotlin

延迟初始化的属性

延迟初始化的属性都是var,因为需要在构造方法外修改它的值,而val属性会被编译成必须在构造方法中初始化的final字段:

class Person(val name: String) {
    lateinit var parent: Person //延迟初始化属性

    fun initPerson() {
        val father = Person("Father")
        parent = father //延迟初始化
        Log.d("TAG", "zwm, name: ${name} ${parent.name}")
    }
}

fun method() {
    val person = Person("Happy")
    person.initPerson()
}

日志打印:
2020-08-24 22:09:14.567 26769-26769/com.tomorrow.kotlindemo D/TAG: zwm, name: Happy Father

可空类型的扩展

为可空类型定义扩展函数是一种更强大的处理null值的方式。可以允许接收者为null的扩展函数调用,并在该函数中处理null,而不是在确保变量不为null之后再调用它的方法。只有扩展函数才能做到这一点,普通成员方法的调用是通过对象实例来分发的,因此实例为null时成员方法永远不能被执行。

函数isEmptyOrNull和isNullOrBlank就可以由String?类型的接收者调用:(不需要安全调用!)

fun method() {
    val str: String? = " "
    val str2: String? = null
    Log.d("TAG", "zwm, ${str.isNullOrBlank()} ${str.isNullOrEmpty()}")
    Log.d("TAG", "zwm, ${str2.isNullOrBlank()} ${str2.isNullOrEmpty()}")
}

日志打印:
2020-08-24 22:40:13.402 28472-28472/com.tomorrow.kotlindemo D/TAG: zwm, true false
2020-08-24 22:40:13.402 28472-28472/com.tomorrow.kotlindemo D/TAG: zwm, true true
fun String?.isNullOrBlank(): Boolean = //可空字符串的扩展
    this == null || this.isBlank() //第二个this使用了智能转换

当你为一个可空类型(以?结尾)定义扩展函数时,这意味着你可以对可空的值调用这个函数,并且函数体中的this可能为null,所以你必须显式地检查。

类型参数的可空性

Kotlin中所有泛型类和泛型函数的类型参数默认都是可空的。任何类型,包括可空类型在内,都可以替换类型参数。这种情况下,使用类型参数作为类型的声明都允许为null,尽管类型参数T并没有用问号结尾:

fun <T> printHashCode(t: T) { //类型参数T推导出的类型是可空类型Any?
    Log.d("TAG", "zwm, ${t?.hashCode()}") //因为t可能为null,所以必须使用安全调用
}

fun method() {
    printHashCode("abc")
}

日志打印:
2020-08-24 23:04:13.388 30688-30688/com.tomorrow.kotlindemo D/TAG: zwm, 96354

要使类型参数非空,必须要为它指定一个非空的上界,那样泛型会拒绝可空值作为实参:

fun <T: Any> printHashCode(t: T) { //类型参数T推导出的类型是可空类型Any
    Log.d("TAG", "zwm, ${t.hashCode()}") //因为t不可能为null,所以不需要使用安全调用
}

fun method() {
    printHashCode("abc")
}

日志打印:
2020-08-24 23:09:11.924 31193-31193/com.tomorrow.kotlindemo D/TAG: zwm, 96354

注意必须使用问号结尾来标记类型为可空的,没有问号就是非空的。类型参数是这个规则唯一的例外。

可空性和Java

Java中的@Nullable String被Kotlin当做String?,而@NotNull String就是String。

Java类型在Kotlin中表示为平台类型,既可以把它当作可空类型也可以当作非空类型来处理。这意味着,你要像在Java中一样,对你在这个类型上做的操作负有全部责任。

当通过继承在Kotlin中重写Java的方法时,可以选择把参数和返回类型定义成可空的,也可以选择把它们定义成非空的。注意,在实现Java类或者接口的方法时一定要搞清楚它的可空性,因为方法的实现可以在非Kotlin的代码中被调用。

2.基本数据类型和其他基本类型

基本数据类型:Int、Boolean及其他

Kotlin并不区分基本数据类型和包装类型,你使用的永远是同一个类型。大多数情况下,对于变量、属性、参数和返回类型,Kotlin的Int类型会被编译成Java基本数据类型int。唯一不可行的例外是泛型类,比如集合,用作泛型类型参数的基本数据类型会被编译成对应的Java包装类型,Kotlin的Int类型会被编译成Java包装类型java.lang.Integer。

val i: Int = 1 //编译成基本数据类型int
val list: List<Int> = listOf(1, 2, 3) //编译成包装类型java.lang.Integer

可空的基本数据类型:Int?、Boolean?及其他

Kotlin中的可空类型不能用Java的基本数据类型表示,因为null只能被存储在Java的引用类型的变量中。这意味着任何时候只要使用了基本数据类型的可空版本,它就会编译成对应的包装类型。

val i: Int? = 1 //编译成包装类型java.lang.Integer

数字转换

Kotlin不会自动地把数字从一种类型转换成另外一种,即便是转换成范围更大的类型,Kotlin要求转换必须是显式的:

val i = 1
val l: Long = i //编译错误:类型不匹配
val l: Long = i.toLong() //正确

对应到Java基本数据类型的类型完整列表如下:

整数类型:Byte、Short、Int、Long
浮点数类型:Float、Double
字符类型:Char
布尔类型:Boolean

基本数据类型字面值:

Long:123L
Double:0.12、2.0、1.2e10、1.2e-10
Float:123.4f、.456F、1e3f
十六机制:0xCAFEBABE、0xbcdL
二进制:0b000000101

字符串转换

Kotlin标准库提供了一套相似的扩展方法,用来把字符串转换成基本数据类型:toInt、toByte、toBoolean等,每个这样的函数都会尝试把字符串的内容解析成对应的类型,如果解析失败则抛出NumberFormatException。

Any和Any?:根类型

Any类型是Kotlin所有非空类型的超类型。当Kotlin函数使用Any时,它会被编译成Java字节码中的Object。所有Kotlin类都包含下面三个方法:toString、equals和hashCode,这些方法都继承自Any。Any并不能使用其他java.lang.Object的方法(比如wait和notify),但是可以通过手动把值转换成java.lang.Object来调用这些方法。

Unit类型:Kotlin的void

Unit是一个完备的类型,可以作为类型参数,而void却不行。只存在一个值是Unit类型,这个值也叫作Unit,并且在函数中会被隐式地返回。当你在重写返回泛型参数的函数时这非常有用,只需要让方法返回Unit类型的值:

interface Processor<T> {
    fun process(): T
}

class NoResultProcessor : Processor<Unit> { //使用Unit作为类型参数
    override fun process() { //返回Unit,但可以省略类型说明
        //do stuff //这里不需要显式的return
    }
}

Nothing类型:这个函数永不返回

Nothing类型没有任何值,只有被当作函数返回值使用,或者被当作泛型函数返回值的类型参数使用才会有意义。在其他所有情况下,声明一个不能存储任何值的变量没有任何意义。

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

fun method() {
    fail("I am fail")
}

日志打印:
java.lang.IllegalStateException: I am fail

3.集合与数组

Kotlin以Java集合库为基础构建,并通过扩展函数增加的特性来增强它。

集合的继承关系:

集合的继承关系

可空性和集合

Kotlin支持类型参数的可空性:

fun method() {
    val list = ArrayList<Int?>()
    list.add(999)
    list.add(666)
    list.add(888)
    list.add(null)
    Log.d("TAG", "zwm, list: $list")
}

日志打印:
2020-08-25 02:04:55.476 9146-9146/com.tomorrow.kotlindemo D/TAG: zwm, list: [999, 666, 888, null]

声明一个变量持有可空的列表,并且包含可空的数字:List<Int?>?,有两个问号,使用变量自己的值的时候,以及使用列表中每个元素的值的时候,都需要使用null检查。

Kotlin提供了一个标准库函数filterNotNull用来遍历一个包含可空值的集合并过滤掉null,这种过滤也影响了集合的类型:

fun method() {
    val list = ArrayList<Int?>()
    list.add(999)
    list.add(666)
    list.add(888)
    list.add(null)
    Log.d("TAG", "zwm, list: ${list.filterNotNull()}")
}

日志打印:
2020-08-25 02:09:59.072 9852-9852/com.tomorrow.kotlindemo D/TAG: zwm, list: [999, 666, 888]

只读集合与可变集合:

Kotlin把访问集合数据的接口和修改集合数据的接口分开了,kotlin.collections.Collection接口可以遍历集合中的元素、获取集合大小、判断集合中是否包含某个元素,以及执行其他从该集合中读取数据的操作。但这个接口没有任何添加或移除元素的方法。

使用kotlin.collections.MutableCollection接口可以修改集合中的数据。它继承了普通的kotlin.collections.Collection接口,还提供了方法来添加和移除元素、清空集合等。

fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
    for(item in source) {
        target.add(item)
    }
}

fun method() {
    val source: Collection<Int> = arrayListOf(3, 5, 7)
    val target: MutableCollection<Int> = arrayListOf(1)
    copyElements(source, target)
    Log.d("TAG", "zwm, target: $target")
}

日志打印:
2020-08-25 02:24:42.891 10690-10690/com.tomorrow.kotlindemo D/TAG: zwm, target: [1, 3, 5, 7]

使用集合接口时需要牢记的一个关键点是只读集合不一定是不可变的。如果你使用的变量拥有一个只读接口类型,它可能只是同一个集合的众多引用中的一个,任何其他的引用都可能拥有一个可变接口类型。

Kotlin集合和Java

集合创建函数:

集合类型 只读 可变
List listOf mutableListOf、arrayListOf
Set setOf mutableSetOf、hashSetOf、linkedSetOf、sortedSetOf
Map mapOf mutableMapOf、hashMapOf、linkedMapOf、sortedMapOf

Java并不会区分只读集合与可变集合,即使Kotlin中把集合声明成只读的,Java代码也能够修改这个集合。

作为平台类型的集合

Kotlin把那些定义在Java代码中的类型看成平台类型,Kotlin没有任何关于平台类型的可空性信息,所以编译器允许Kotlin代码将其视为可空或非空。同样,Java中声明的集合类型的变量也被视为平台类型,一个平台类型的集合本质上就是可变性未知的集合,Kotlin代码将其视为只读的或者可变的。

对象和基本数据类型的数组

要在Kotlin中创建数组,有下面这些方法:

  • arrayOf函数创建一个数组,它包含的元素是指定为该函数的实参。
  • arrayOfNulls创建一个给定大小的数组,包含的是null元素。当然,它只能用来创建包含元素类型可空的数组。
  • Array构造方法接收数组的大小和一个lambda表达式,调用lambda表达式来创建每一数组元素。这就是使用非空元素类型来初始化数组,但不用显式地传递每个元素的方式。

Kotlin中的数组是支持泛型的,当然也不再协变,也就是说你不能将任意一个对象数组赋值给Array<Any>或者Array<Any?>。

fun method() {
    val arr = arrayOf(3, 5, 7)
    val arr2 = arrayOfNulls<Int>(3)
    val arr3 = Array(3) { 100 + it }
    for (i in arr.indices) {
        Log.d("TAG", "zwm, ${arr[i]}")
    }
    for (i in arr2.indices) {
        Log.d("TAG", "zwm, ${arr2[i]}")
    }
    for (i in arr3.indices) {
        Log.d("TAG", "zwm, ${arr3[i]}")
    }
}

日志打印:
2020-08-25 03:11:50.443 14918-14918/? D/TAG: zwm, 3
2020-08-25 03:11:50.443 14918-14918/? D/TAG: zwm, 5
2020-08-25 03:11:50.443 14918-14918/? D/TAG: zwm, 7
2020-08-25 03:11:50.443 14918-14918/? D/TAG: zwm, null
2020-08-25 03:11:50.443 14918-14918/? D/TAG: zwm, null
2020-08-25 03:11:50.444 14918-14918/? D/TAG: zwm, 100
2020-08-25 03:11:50.444 14918-14918/? D/TAG: zwm, 101
2020-08-25 03:11:50.444 14918-14918/? D/TAG: zwm, 102

向vararg方法传递集合:

fun method() {
    val strings = listOf("a", "b", "c")
    val result = format("%s/%s/%s", *strings.toTypedArray()) //期望vararg参数时使用展开运算符(*)传递数组
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-25 03:17:48.876 15174-15174/com.tomorrow.kotlindemo D/TAG: zwm, result: a/b/c

数组类型的类型参数始终会变成对象类型。因此,如果你声明了一个Array<Int>,它将会是一个包含装箱整型的数组(它的Java类型将是java.lang.Integer[])。如果你需要创建没有装箱的基本数据类型的数组,必须使用一个基本数据类型数组的特殊类。

为了表示基本数据类型的数组,Kotlin提供了若干独立的类,每一种基本数据类型都对应一个。例如:IntArray、ByteArray、CharArray、BooleanArray等,所有这些类型都被编译成普通的Java基本数据类型数组,比如int[]、byte[]、char[]、boolean[]等,因此这些数组中的值存储时并没有装箱,而是使用了可能的最高效的方式。

要创建一个基本数据类型的数组,有如下方法:

  • 该类型的构造方法接收size参数并返回一个使用对应基本数据类型默认值(通常是0)初始化好的数组。
  • 工厂函数(IntArray的intArrayOf,以及其他数组类型的函数)接收变长参数的值并创建存储这些值的数组。
  • 另一种构造方法,接收一个大小和一个用来初始化每个元素的lambda。
fun method() {
    val arr = IntArray(3)
    val arr2 = intArrayOf(0, 0, 0)
    val arr3 = IntArray(3) { it + 10 }
    for (i in arr.indices) {
        Log.d("TAG", "zwm1, ${arr[i]}")
    }
    for (i in arr2.indices) {
        Log.d("TAG", "zwm2, ${arr2[i]}")
    }
    for (i in arr3.indices) {
        Log.d("TAG", "zwm3, ${arr3[i]}")
    }
}

日志打印:
2020-08-25 03:42:28.303 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm1, 0
2020-08-25 03:42:28.304 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm1, 0
2020-08-25 03:42:28.304 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm2, 0
2020-08-25 03:42:28.304 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm2, 0
2020-08-25 03:42:28.304 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm3, 10
2020-08-25 03:42:28.305 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm3, 11
2020-08-25 03:42:28.305 18958-18958/com.tomorrow.kotlindemo D/TAG: zwm3, 12

如果有一个持有基本数据类型装箱后的数组或者集合,可以用对应的转换函数把它们转换成基本数据类型的数组,比如toIntArray。

对于数组,除了那些基本操作(获取数组的长度,获取或者设置元素)外,Kotlin标准库支持一套和集合相同的用于数组的扩展函数,如filter、map等也适用于数组,包括基本数据类型的数组(注意,这些方法的返回值是列表而不是数组)。

对数组使用forEachIndexed:

fun method() {
    val arr = IntArray(3) { it + 10 }
    arr.forEachIndexed { index, i -> Log.d("TAG", "zwm, $index:$i") }
}

日志打印:
2020-08-25 03:51:16.075 19707-19707/com.tomorrow.kotlindemo D/TAG: zwm, 0:10
2020-08-25 03:51:16.075 19707-19707/com.tomorrow.kotlindemo D/TAG: zwm, 1:11
2020-08-25 03:51:16.076 19707-19707/com.tomorrow.kotlindemo D/TAG: zwm, 2:12

七、运算符重载及其他约定

1.重载算术运算符

重载二元算术运算符:

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point { //定义一个名为plus的方法
        return Point(x + other.x, y + other.y)
    }
}

fun method() {
    val p1 = Point(200, 300)
    val p2 = Point(300, 200)
    val p = p1 + p2
    Log.d("TAG", "zwm, x: ${p.x} y: ${p.y}")
}

日志打印:
2020-08-06 14:43:15.876 25513-25513/com.tomorrow.kotlindemo D/TAG: zwm, x: 500 y: 500

把运算符定义为扩展函数:

data class Point(val x: Int, val y: Int)

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

fun method() {
    val p1 = Point(200, 300)
    val p2 = Point(300, 200)
    val p = p1 + p2
    Log.d("TAG", "zwm, x: ${p.x} y: ${p.y}")
}

日志打印:
2020-08-06 14:57:13.529 26210-26210/com.tomorrow.kotlindemo D/TAG: zwm, x: 500 y: 500

可重载的二元算术运算符:

表达式 函数名
a * b times
a / b div
a % b mod
a + b plus
a - b minus

定义一个运算数类型不同的运算符:

data class Point(val x: Int, val y: Int)

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

fun method() {
    val p = Point(200, 300) * 1.5
    Log.d("TAG", "zwm, x: ${p.x} y: ${p.y}")
}

日志打印:
2020-08-06 15:09:11.415 27811-27811/com.tomorrow.kotlindemo D/TAG: zwm, x: 300 y: 450

定义一个返回结果不同的运算符:

data class Point(val x: Int, val y: Int)

operator fun Point.times(scale: Double): String {
    return "${(x * scale).toInt()}, ${(y * scale).toInt()}"
}

fun method() {
    val result = Point(200, 300) * 1.5
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-06 15:14:35.870 28822-28822/com.tomorrow.kotlindemo D/TAG: zwm, result: 300, 450

Kotlin没有为标准数字类型定义任何位运算符,因此,也不允许你为自定义类型定义它们。相反,它使用支持中缀调用语法的常规函数,可以为自定义类型定义相似的函数。

Kotlin中用于执行位运算的函数列表:

  • shl:带符号左移
  • shr:带符号右移
  • ushr:无符号右移
  • and:按位与
  • or:按位或
  • xor:按位异或
  • inv:按位取反
fun method() {
    val result1 = 0x0F and 0xF0
    val result2 = 0x0F or 0xF0
    val result3 = 0x1 shl 4
    Log.d("TAG", "zwm, $result1 $result2 $result3")
}

日志打印:
2020-08-06 15:28:50.149 29868-29868/com.tomorrow.kotlindemo D/TAG: zwm, 0 255 16

重載复合赋值运算符:(plusAssign、minusAssign、timesAssign等)

data class Point(var x: Int, var y: Int)

operator fun Point.plusAssign(other: Point) {
    x += other.x
    y += other.y
}

fun method() {
    val p = Point(200, 300)
    p.plusAssign(Point(300, 200))
    Log.d("TAG", "zwm, $p")
}

日志打印:
2020-08-06 15:45:40.161 31500-31500/com.tomorrow.kotlindemo D/TAG: zwm, Point(x=500, y=500)

重载一元运算符:

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus(): Point {
    return Point(-x, -y)
}

fun method() {
    val p = Point(200, 300).unaryMinus()
    Log.d("TAG", "zwm, $p")
}

日志打印:
2020-08-06 15:51:35.020 31955-31955/com.tomorrow.kotlindemo D/TAG: zwm, Point(x=-200, y=-300)

可重载的一元算法的运算符:

表达式 函数名
+a unaryPlus
-a unaryMinus
!a not
++a,a++ inc
--a,a-- dec

2.重载比较运算符

与算术运算符一样,在Kotlin中,可以对任何对象使用比较运算符(==、!=、>、<等),而不仅仅限于基本数据类型。

等号运算符:equals

如果在Kotlin中使用==运算符,它将被转换成equals方法的调用。使用!=运算符也会被转换成equals函数的调用,明显的差异在于,它们的结果是相反的。注意,和所有其他运算符不同的是,==和!=可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null。比较a==b会检查a是否为非空,如果是,就调用a.equals(b),否则,只有两个参数都是空引用,结果才是true。

a == b -> a?.equals(b) ?: (b == null)
data class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        if(other === this) return true //优化:检查参数是否与this是同一个对象
        if(other !is Point) return false //检查参数类型
        return other.x == x && other.y == y //智能转换为Point来访问x、y属性
    }
}

fun method() {
    val p1 = Point(200, 300)
    val p2 = Point(200, 300)
    val result = p1 == p2
    val result2 = null == p1
    Log.d("TAG", "zwm, result: $result result2: $result2")
}

日志打印:
2020-08-06 16:31:21.888 7153-7153/com.tomorrow.kotlindemo D/TAG: zwm, result: true result2: false

恒等运算符(===)与Java中的==运算符是完全相同的:检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查它们是否是相同的值)。在实现了equals方法之后,通常会使用这个运算符来优化调用代码。注意,===运算符不能被重载。

equals函数之所以被标记为override,那是因为与其他约定不同的是,这个方法的实现是在Any类中定义的(Kotlin中的所有对象都支持等式比较),Any中的基本方法就已经标记了operator,并且函数的operator修饰符适用于所有实现或重写它的方法。

另外equals不能实现为扩展函数,因为继承自Any类的实现始终优先于扩展函数。

排序运算符:compareTo

Kotlin中比较运算符(<、>、<=、>=)的使用将被转换为compareTo,compareTo的返回类型必须为Int。

a >= b -> a.compareTo(b) >= 0
data class Point(val x: Int, val y: Int): Comparable<Point> {
    override fun compareTo(other: Point): Int {
        return compareValuesBy(this, other, Point::x, Point::y) //按顺序调用给定的方法,并比较它们的值
    }
}

fun method() {
    val p1 = Point(100, 200)
    val p2 = Point(300, 100)
    val p3 = Point(50, 500)
    Log.d("TAG", "zwm, ${p1 > p2} ${p1 > p3}")
}

日志打印:
2020-08-06 16:50:28.966 10439-10439/com.tomorrow.kotlindemo D/TAG: zwm, false true

与equals一样,operator修饰符已经被用在了基类的接口中,因此在重写该接口时无须再重复。

3.集合与区间的约定

通过下标来访问元素:get和set

在Kotlin中,下标运算符是一个约定,使用下标运算符获取元素会被转换为get运算符方法的调用,并且写入元素将调用set。

实现get约定:

data class Point(val x: Int, val y: Int)

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun method() {
    val p = Point(100, 200)
    Log.d("TAG", "zwm, ${p[0]} ${p[1]}")
}

日志打印:
2020-08-07 08:12:42.490 15636-15636/com.tomorrow.kotlindemo D/TAG: zwm, 100 200

注意,get的参数可以是任何类型,而不只是Int。例如,当你对map使用下标运算符时,参数类型是键的类型,它可以是任意类型。还可以定义具有多个参数的get方法。如果需要使用不同的键类型访问集合,也可以使用不同的参数类型定义多个重载的get方法。

实现set约定:

data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    return when(index) {
        0 -> x = value
        1 -> y = value
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun method() {
    val p = MutablePoint(100, 200)
    p[0] = 50
    p[1] = 60
    Log.d("TAG", "zwm, $p")
}

日志打印:
2020-08-07 08:20:02.863 16265-16265/com.tomorrow.kotlindemo D/TAG: zwm, MutablePoint(x=50, y=60)

in的约定

in运算符用于检查某个对象是否属于集合,相应的函数叫作contains,in右边的对象将会调用contains函数,in左边的对象将会作为函数入参:

data class Point(val x: Int, val y: Int)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x &&
            p.y in upperLeft.y until  lowerRight.y
}

fun method() {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    val p = Point(20, 30)
    val result = p in rect
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-07 08:33:33.966 17526-17526/? D/TAG: zwm, result: true

标准库的until函数用于构建一个开区间,开区间是不包括最后一个点的区间。例如,如果用10..20构建一个普通的区间(闭区间),该区间则包括10到20的所有数字,包括20。开区间10 until 20包括10到19的数字,但不包括20。

rangeTo的约定

..运算符是调用rangeTo函数的一个简洁方法。rangeTo函数返回一个区间,你可以为自己的类定义这个运算符。但是,如果该类实现了Comparable接口,那么不需要了;你可以通过Kotlin标准库创建一个任意可比较元素的区间,这个库定义了可以用于任何可比较元素的rangeTo函数:

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
data class Point(val x: Int, val y: Int): Comparable<Point> {
    override fun compareTo(other: Point): Int {
        return compareValuesBy(this, other, Point::x, Point::y) //按顺序调用给定的方法,并比较它们的值
    }
}

fun method() {
    val p = Point(20, 30)..Point(200, 300)
    val result = Point(100, 200) in p
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-07 08:50:12.484 17878-17878/com.tomorrow.kotlindemo D/TAG: zwm, result: true

rangeTo运算符的优先级低于算术运算符,但是最好把参数括起来以免混淆:

fun method() {
    val n = 9
    Log.d("TAG", "zwm, ${5..(n+1)}")
}

日志打印:
2020-08-07 08:59:06.502 18325-18325/? D/TAG: zwm, 5..10

使用forEach:

fun method() {
    val n = 3
    (0..n).forEach {  Log.d("TAG", "zwm, $it") }
}

日志打印:
2020-08-07 09:01:14.898 18611-18611/com.tomorrow.kotlindemo D/TAG: zwm, 0
2020-08-07 09:01:14.898 18611-18611/com.tomorrow.kotlindemo D/TAG: zwm, 1
2020-08-07 09:01:14.898 18611-18611/com.tomorrow.kotlindemo D/TAG: zwm, 2
2020-08-07 09:01:14.898 18611-18611/com.tomorrow.kotlindemo D/TAG: zwm, 3

在for循环中使用iterator的约定

在Kotlin中,for循环也可以使用in运算符,和做区间检查一样。但是在这种情况下它的含义是不同的:它被用来执行迭代。这意味着一个诸如for(x in list) {...} 将被转换成list.iterator()的调用,然后就像在Java中一样,在它上面重复调用hasNext和next方法:

fun method() {
    val list = listOf(Point(10, 20), Point(20, 30))
    for(item in list) {
        Log.d("TAG", "zwm, $item")
    }
}

日志打印:
2020-08-07 09:10:59.935 19483-19483/com.tomorrow.kotlindemo D/TAG: zwm, Point(x=10, y=20)
2020-08-07 09:10:59.935 19483-19483/com.tomorrow.kotlindemo D/TAG: zwm, Point(x=20, y=30)

注意,在Kotlin中,这也是一种约定,这意味着iterator方法可以被定义为扩展函数,这就解释了为什么可以遍历一个常规的Java字符串:标准库已经为CharSequence定义了一个扩展函数iterator,而它是String的父类:

operator fun CharSequence.iterator(): CharIterator //这个库函数让迭代字符串成为可能
fun method() {
    val str = "abc"
    for(c in str) {
        Log.d("TAG", "zwm, $c")
    }
}

日志打印:
2020-08-07 09:16:30.770 19952-19952/? D/TAG: zwm, a
2020-08-07 09:16:30.770 19952-19952/? D/TAG: zwm, b
2020-08-07 09:16:30.771 19952-19952/? D/TAG: zwm, c

可以为自己的类定义iterator方法:

data class Point(val x: Int): Comparable<Point> {
    override fun compareTo(other: Point): Int {
        return compareValuesBy(this, other, Point::x)
    }
}

operator fun ClosedRange<Point>.iterator(): Iterator<Point> =
    object : Iterator<Point> {
        var current = start

        override fun hasNext() = current <= endInclusive

        override fun next() = current.apply {
           current = Point(this.x + 1)
        }
    }

fun method() {
    val p = Point(5)..Point(8)
    for(i in p) {
        Log.d("TAG", "zwm, result: $i")
    }
}

日志打印:
2020-08-07 09:42:07.980 21694-21694/? D/TAG: zwm, result: Point(x=5)
2020-08-07 09:42:07.980 21694-21694/? D/TAG: zwm, result: Point(x=6)
2020-08-07 09:42:07.980 21694-21694/? D/TAG: zwm, result: Point(x=7)
2020-08-07 09:42:07.980 21694-21694/? D/TAG: zwm, result: Point(x=8)

4.解构声明和组件函数

解构声明功能允许你展开单个复合值,并使用它来初始化多个单独的变量:

data class Point(val x: Int, val y: Int)

fun method() {
    val p = Point(100, 200)
    val (x, y) = p
    Log.d("TAG", "zwm, $x $y")
}

日志打印:
2020-08-07 09:53:25.024 23435-23435/com.tomorrow.kotlindemo D/TAG: zwm, 100 200

一个解构声明看起来像一个普通的变量声明,但它在括号中有多个变量。事实上,解构声明再次用到了约定的原理。要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N是声明中变量的位置。

对于数据类,编译器为每个在主构造方法中声明的属性生成一个componentN函数。而对于非数据类,则需要手动进行声明:

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

fun method() {
    val p = Point(100, 200)
    val (x, y) = p
    Log.d("TAG", "zwm, $x $y")
}

日志打印:
2020-08-07 10:01:43.264 24067-24067/? D/TAG: zwm, 100 200

使用解构声明来返回多个值:

data class Point(val x: Int, val y: Int)

fun getPoint(arr: Array<Int>): Point {
    return Point(arr[0], arr[1])
}

fun method() {
    val arr = arrayOf(100, 200)
    val (x, y) = getPoint(arr)
    Log.d("TAG", "zwm, $x $y")
}

日志打印:
2020-08-07 10:13:35.270 25173-25173/com.tomorrow.kotlindemo D/TAG: zwm, 100 200

使用解构声明来处理集合:

data class Point(val x: Int, val y: Int)

fun getPoint(str: String): Point {
    val (x, y) = str.split(',', limit = 2)
    return Point(x.toInt(), y.toInt())
}

fun method() {
    val arr = "100,200"
    val (x, y) = getPoint(arr)
    Log.d("TAG", "zwm, $x $y")
}

日志打印:
2020-08-07 10:19:10.701 25706-25706/com.tomorrow.kotlindemo D/TAG: zwm, 100 200

用解构声明来遍历map:

fun method() {
    val map = mapOf( 1 to "one", 2 to "two", 3 to "three")
    for((key, value) in map) {
        Log.d("TAG", "zwm, $key $value")
    }
}

日志打印:
2020-08-07 10:24:04.482 26149-26149/com.tomorrow.kotlindemo D/TAG: zwm, 1 one
2020-08-07 10:24:04.482 26149-26149/com.tomorrow.kotlindemo D/TAG: zwm, 2 two
2020-08-07 10:24:04.482 26149-26149/com.tomorrow.kotlindemo D/TAG: zwm, 3 three

5.重用属性访问的逻辑:委托属性

委托属性的基本操作

委托属性的基本语法是这样的:

class Foo {
    var p: Type by Delegate()
}

属性p将它的访问器逻辑委托给了另一个对象:这里是Delegate类的一个新的实例。通过关键字by对其后的表达式求值来获取这个对象,关键字by可以用于任何符合属性委托约定规则的对象。编译器生成的代码如下:

class Foo {
    private val delegate = Delegate() //编译器会自动生成一个辅助属性
    val p: Type
        set(value: Type) = delegate.setValue(..., value)
        get() = delegate.getValue(..)
}

例子:

class Foo {
    var city : String by Delegate()
}

class Delegate {
    operator fun getValue(foo: Foo, property: KProperty<*>): String {
        Log.d("TAG", "zwm, Delegate getValue, property name: ${property.name}")
        return "getValue"
    }

    operator fun setValue(foo: Foo, property: KProperty<*>, s: String) {
        Log.d("TAG", "zwm, Delegate setValue, property name: ${property.name}, value: $s")
    }
}

fun method() {
    val foo = Foo()
    Log.d("TAG", "zwm, foo.city 1: ${foo.city}")
    Log.d("TAG", "zwm, foo.city 2: ${foo.city}")
    foo.city = "guangzhou"
    Log.d("TAG", "zwm, foo.city 3: ${foo.city}")
}

日志打印:
2020-08-07 14:14:53.351 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, Delegate getValue, property name: city
2020-08-07 14:14:53.351 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, foo.city 1: getValue
2020-08-07 14:14:53.351 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, Delegate getValue, property name: city
2020-08-07 14:14:53.351 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, foo.city 2: getValue
2020-08-07 14:14:53.352 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, Delegate setValue, property name: city, value: guangzhou
2020-08-07 14:14:53.352 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, Delegate getValue, property name: city
2020-08-07 14:14:53.352 8868-8868/com.tomorrow.kotlindemo D/TAG: zwm, foo.city 3: getValue

使用委托属性:惰性初始化和by lazy()

惰性初始化是一种常见的模式,直到第一次访问该属性的时候,才根据需要创建对象的一部分。当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,这个非常有用。使用委托属性会让代码变得简单得多,可以封装用于存储值的支持属性和确保该值只被初始化一次的逻辑。在这里可以使用标准库函数lazy返回的委托,lazy函数返回一个对象,该对象具有一名为getValue且签名正确的方法,因此可以把它与by关键字一起使用来创建一个委托属性。lazy的参数是一个lambda,可以调用它来初始化这个值。

例子:

class Foo {
    val city: String by lazy {
        Log.d("TAG", "zwm, lazy")
        "lazy"
    }
}

fun method() {
    val foo = Foo()
    Log.d("TAG", "zwm, foo.city 1: ${foo.city}")
    Log.d("TAG", "zwm, foo.city 2: ${foo.city}")
}

日志打印:
2020-08-07 14:22:50.565 9447-9447/com.tomorrow.kotlindemo D/TAG: zwm, lazy
2020-08-07 14:22:50.565 9447-9447/com.tomorrow.kotlindemo D/TAG: zwm, foo.city 1: lazy
2020-08-07 14:22:50.566 9447-9447/com.tomorrow.kotlindemo D/TAG: zwm, foo.city 2: lazy

by lazy语法的特点如下:

  • 该变量必须是引用不可变的,而不能通过var来声明。
  • 在被首次调用时,才会进行赋值操作。一旦被赋值,后续它将不能被更改。

lazy的背后是接受一个lambda并返回一个Lazy<T>实例的函数,第一次访问该属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问该属性时只是返回记录的结果。另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。你还可以给lazy传递LazyThreadSafetyMode.NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。比如:

class Person(val color: String) {
    val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) {
        //并行模式
        if(color == "yellow") "male" else "female"
    }
}

class Person(val color: String) {
    val sex: String by lazy(LazyThreadSafetyMode.NONE) {
        //不做任何线程保证也不会有任何线程开销
        if(color == "yellow") "male" else "female"
    }
}

Delegates.observable函数可以用来添加属性更改的观察者:

class Foo {
    var city: String by Delegates.observable("init") {
        prop, old, new ->
        Log.d("TAG", "zwm, observable: ${prop.name} $old $new")
    }
}

fun method() {
    val foo = Foo()
    Log.d("TAG", "zwm, foo.city: ${foo.city}")
    foo.city = "guangzhou"
}

日志打印:
2020-08-07 14:28:44.590 10176-10176/com.tomorrow.kotlindemo D/TAG: zwm, foo.city: init
2020-08-07 14:28:44.591 10176-10176/com.tomorrow.kotlindemo D/TAG: zwm, observable: city init guangzhou

委托属性可以使用任意map来作为属性委托,来灵活处理具有可变属性集的对象:

class Site(val map: Map<String, Any?>) {
    val name: String by map
    val city: String by map
}

fun method() {
    val map = mutableMapOf("name" to "Tomy", "city" to "gz")
    val site = Site(map)
    Log.d("TAG", "zwm, name: ${site.name} url: ${site.city}")
    map["name"] = "Zhang"
    Log.d("TAG", "zwm, name: ${site.name} url: ${site.city}")
}

日志打印:
2020-08-07 14:49:31.047 11820-11820/com.tomorrow.kotlindemo D/TAG: zwm, name: Tomy url: gz
2020-08-07 14:49:31.047 11820-11820/com.tomorrow.kotlindemo D/TAG: zwm, name: Zhang url: gz

八、高阶函数:Lambda作为形参和返回值

1.声明高阶函数

高阶函数就是以另一个函数作为参数或者返回值的函数。在Kotlin中,函数可以用lambda或者函数引用来表示。

Kotlin中的函数类型语法:

(int, String) -> Unit //格式:(参数类型) -> 返回类型,如果没有参数类型就用()来表示

例如:

val sum = { x: Int, y:Int -> x + y }
val sum: (Int, Int) -> Int = { x, y -> x + y }

调用作为参数的函数:

fun twoAndThree(operation: (Int, Int) -> Int) { //定义一个函数类型的参数
    val result = operation(2, 3)
    Log.d("TAG", "zwm, result: $result")
}

fun method() {
    twoAndThree { x, y -> x + y }
}

日志打印:
2020-08-12 08:55:52.236 21495-21495/com.tomorrow.kotlindemo D/TAG: zwm, result: 5

在Java中可以很简单地调用使用了函数类型的Kotlin函数。Java 8的lambda会被自动转换为函数类型的值:

//shapes.kt
fun twoAndThree(operation: (Int, Int) -> Int): Int {
    val result = operation(2, 3)
    Log.d("TAG", "zwm, result: $result")
    return result
}

//JavaDemo.java
int result = ShapesKt.twoAndThree(x, y -> x + y);

在旧版的Java中(Java 8以前),可以传递一个实现了函数接口中的invoke方法的匿名类的实例:

//shapes.kt
package com.tomorrow.kotlindemo.example

import android.util.Log

fun twoAndThree(operation: (Int, Int) -> Int): Int { //定义一个函数类型的参数
    val result = operation(2, 3)
    Log.d("TAG", "zwm, result: $result")
    return result
}

//JavaDemo.java
package com.tomorrow.kotlindemo;

import android.util.Log;

import com.tomorrow.kotlindemo.example.ShapesKt;

import kotlin.jvm.functions.Function2;

public class JavaDemo {

    public void testDemo() {
        int result = ShapesKt.twoAndThree(new Function2<Integer, Integer, Integer>() {
            @Override
            public Integer invoke(Integer integer, Integer integer2) {
                return integer + integer2;
            }
        });
        Log.d("TAG", "zwm, java result: " + result);
    }
}

日志打印:
2020-08-12 09:22:31.998 24355-24355/com.tomorrow.kotlindemo D/TAG: zwm, result: 5
2020-08-12 09:22:31.998 24355-24355/com.tomorrow.kotlindemo D/TAG: zwm, java result: 5

函数类型的参数默认值和null值:

fun twoAndThree(operation: (Int, Int) -> Int = { x, y -> x * y }): Int { //定义一个函数类型的参数
    val result = operation(2, 3)
    Log.d("TAG", "zwm, result: $result")
    return result
}

fun method() {
    twoAndThree()
}

日志打印:
2020-08-12 09:28:53.663 25496-25496/com.tomorrow.kotlindemo D/TAG: zwm, result: 6
fun twoAndThree(operation: ((Int, Int) -> Int)? = null): Int {
    if(operation != null) { //显式判空
        val result = operation(2, 3)
        Log.d("TAG", "zwm, result: $result")
        return result
    }
    return -1
}

fun method() {
    twoAndThree()
}
fun twoAndThree(operation: ((Int, Int) -> Int)? = null): Int {
    val result = operation?.invoke(2, 3) ?: (2 + 3) //函数类型是一个包含invoke方法的接口的具体实现
    Log.d("TAG", "zwm, result: $result")
    return result
}

fun method() {
    twoAndThree()
}

日志打印:
2020-08-12 09:35:00.001 25823-25823/com.tomorrow.kotlindemo D/TAG: zwm, result: 5

返回函数的函数:

fun getAddCalculator(): (Int, Int) -> Int {
    return { x, y -> x + y }
}

fun method() {
    val calculator = getAddCalculator()
    val result = calculator(2, 3)
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-12 09:45:19.782 26722-26722/com.tomorrow.kotlindemo D/TAG: zwm, result: 5

2.内联函数:消除lambda带来的运行时开销

Lambda表达式会被正常地编译成匿名类,这表示每调用一次lambda表达式,一个额外的类就会被创建。并且如果lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象。这会带来运行时的额外开销,导致使用lambda比使用一个直接执行相同代码的函数效率更低。

如果使用inline修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用:

inline fun twoAndThree(operator: (Int, Int) -> Int) { //内联函数定义
    val result = operator(2, 3)
    Log.d("TAG", "zwm, result: $result")
}

fun method() {
    Log.d("TAG", "zwm, before inline")
    twoAndThree{ x, y -> x + y } //内联函数调用
    Log.d("TAG", "zwm, after inline")
}

不是所有使用lambda的函数都可以被内联。当函数被内联的时候,作为参数的lambda表达式的函数体会被直接替换到最终生成的代码中。这将限制函数体中的对应lambda参数的使用。如果lambda参数被调用,这样的代码能被容易地内联。但如果lambda参数在某个地方被保存起来,以便后面可以继续使用,lambda表达式的代码将不能被内联,因为必须要有一个包含这些代码的对象存在:

inline fun twoAndThree(operator: (Int, Int) -> Int) { //内联函数定义
    val result = operator(2, 3)
    Log.d("TAG", "zwm, result: $result")
}

fun callInlineMethod(param: (Int, Int) -> Int) {
    Log.d("TAG", "zwm, before call inline")
    twoAndThree(param) //在调用的地方还没有lambda,因此没有被内联
    Log.d("TAG", "zwm, after call inline")
}

如果一个函数期望两个或更多lambda参数,可以选择只内联其中一些参数。因为一个lambda可能会包含很多代码或者以不允许内联的方式使用:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
    //...
}

注意,编译器完全支持内联跨模块的函数或者第三方库定义的函数。也可以在Java中调用绝大部分内联函数,但这些调用并不会被内联,而是编译成普通的函数调用。

内联集合操作:

filter和map函数都被声明为inline函数,所以它们的函数体会被内联,因此不会产生额外的类或者对象。如果有大量元素需要处理,中间集合的运行开销将成为不可忽视的问题,这时可以在调用链后加一个asSequence调用,用序列来替代集合。但是用来处理序列的lambda没有被内联。每一个中间序列被表示成把lambda保存在其字段中的对象,而末端操作会导致由每一个中间序列调用组成的调用链被执行。因此,即便序列上的操作是惰性的,你不应该总是试图在集合操作的调用链后加上asSequence。这只在处理大量数据的集合时有用,小的集合可以用普通的集合操作处理。

决定何时将函数声明成内联:

使用inline关键字只能提高带有lambda参数的函数的性能,其他的情况需要额外的度量和研究。对于普通的函数调用,JVM已经提供了强大的内联支持。

Kotlin库中withLock函数的定义:

fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

Kotlin的withLock语句的使用:

fun method() {
    val lock: Lock = ReentrantLock()
    lock.withLock { Log.d("TAG", "zwm, with lock") }
}

日志打印:
2020-08-12 13:32:21.455 9622-9622/com.tomorrow.kotlindemo D/TAG: zwm, with lock

Java 7的try-with-resource语句的使用:

public String testDemo(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

Kotlin标准库中有个use函数,是一个扩展函数,被用来操作可关闭的资源,它接收一个lambda作为参数。这个方法调用lambda并且确保资源被关闭,无论lambda正常执行还是抛出了异常。当然,use函数是内联函数,所以使用它并不会引发任何性能开销:

fun method(path: String): String {
    BufferedReader(FileReader(path)).use { br -> return br.readLine() }
}

3.高阶函数中的控制流

lambda中的返回语句:从一个封闭的函数返回

如果你在lambda中使用return关键字,它会从调用lambda的函数中返回,并不只是从lambda中返回。这样的return语句叫作非局部返回,因为它从一个比包含return的代码块更大的代码块中返回了。

需要注意的是,只有在以lambda作为参数的函数是内联函数的时候才能从更外层的函数返回。forEach的函数体和lambda的函数体一起被内联了,所以在编译的时候能很容易做到从包含它的函数中返回。在一个非内联函数的lambda中使用return表达式是不允许的。

fun method() {
    val list = listOf(Person("Kotlin"), Person("Java"))
    list.forEach {
        Log.d("TAG", "zwm, name: ${it.name}")
        return
    }
    Log.d("TAG", "zwm, method end")
}

日志打印:
2020-08-12 14:04:43.195 14730-14730/com.tomorrow.kotlindemo D/TAG: zwm, name: Kotlin

从lambda返回:使用标签返回

想从一个lambda表达式处返回, 你可以标记它,然后在return关键字后面引用这标签。要标记一个lambda表达式,在lambda的花括号之前放一个标签名(可以是任何标识符),接着放一个@符号。要从一个lambda返回,在return关键字后放一个@符号,接着放标签名。

fun method() {
    val list = listOf(Person("Kotlin"), Person("Java"))
    list.forEach label@{
        Log.d("TAG", "zwm, name: ${it.name}")
        return@label
    }
    Log.d("TAG", "zwm, method end")
}

日志打印:
2020-08-12 14:26:21.882 16310-16310/? D/TAG: zwm, name: Kotlin
2020-08-12 14:26:21.883 16310-16310/? D/TAG: zwm, name: Java
2020-08-12 14:26:21.883 16310-16310/? D/TAG: zwm, method end

使用lambda作为参数的函数的函数名可以作为标签:

fun method() {
    val list = listOf(Person("Kotlin"), Person("Java"))
    list.forEach {
        Log.d("TAG", "zwm, name: ${it.name}")
        return@forEach
    }
    Log.d("TAG", "zwm, method end")
}

日志打印:
2020-08-12 14:28:12.182 16512-16512/com.tomorrow.kotlindemo D/TAG: zwm, name: Kotlin
2020-08-12 14:28:12.182 16512-16512/com.tomorrow.kotlindemo D/TAG: zwm, name: Java
2020-08-12 14:28:12.183 16512-16512/com.tomorrow.kotlindemo D/TAG: zwm, method end

如果你显式地指定了lambda表达式的标签,再使用函数名作为标签没有任何效果,一个lambda表达式的标签数量不能多于一个。

带标签的this表达式

如果你给带接收者的lambda指定标签,就可以通过对应的带有标签的this表达式访问它的隐式接收者:

fun method() {
    val result = StringBuilder().apply sb@{
        listOf(1, 2, 3).apply {
            this@sb.append(this.toString())
        }
    }
    Log.d("TAG", "zwm, result: ${result.toString()}")
}

日志打印:
2020-08-12 14:44:00.654 17336-17336/com.tomorrow.kotlindemo D/TAG: zwm, result: [1, 2, 3]

匿名函数:默认使用局部返回

在匿名函数中使用return:

data class Person(val name: String)

fun method() {
    val list = listOf(Person("Kotlin"), Person("Java"))
    list.forEach(fun (person) {
        Log.d("TAG", "zwm, name: ${person.name}")
        return
    })
    Log.d("TAG", "zwm, method end")
}

日志打印:
2020-08-12 14:53:45.399 18383-18383/? D/TAG: zwm, name: Kotlin
2020-08-12 14:53:45.399 18383-18383/? D/TAG: zwm, name: Java
2020-08-12 14:53:45.399 18383-18383/? D/TAG: zwm, method end

在filter中使用匿名函数:

data class Person(val name: String)

fun method() {
    val list = listOf(Person("Kotlin"), Person("Java"))
    val result = list.filter(fun (person): Boolean {
        return person.name == "Kotlin"
    })
    Log.d("TAG", "zwm, method end: $result")
}

日志打印:
2020-08-12 14:56:31.894 18622-18622/? D/TAG: zwm, method end: [Person(name=Kotlin)]

使用表达式体匿名函数:

data class Person(val name: String)

fun method() {
    val list = listOf(Person("Kotlin"), Person("Java"))
    val result = list.filter(fun (person) = person.name == "Kotlin" )
    Log.d("TAG", "zwm, method end: $result")
}

日志打印:
2020-08-12 14:59:32.693 19035-19035/com.tomorrow.kotlindemo D/TAG: zwm, method end: [Person(name=Kotlin)]

在匿名函数中,不带标签的return表达式会从匿名函数返回,而不是从包含匿名函数的函数返回。这条规则很简单:return从最近的使用fun关键字声明的函数返回。lambda表达式没有使用fun关键字,所以lambda中的return从最外层的函数返回。匿名函数使用了fun,因此return表达式从匿名函数返回,而不是从最外层的函数返回。尽管匿名函数看起来跟普通函数很相似,但它其实是lambda表达式的另一种语法形式而已。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345