Kotlin笔记

一、Kotlin基础

1.1 变量

在Kotlin中变量分为可变引用var和不可变引用val,val对应的是java中的final变量。尽管val的引用地址是不可变的,但它指向的对象完全是可变的。在val变量的代码块执行期间,它只能进行唯一一次初始化,如果编译器能确保只有唯一一条初始化语句会被执行,可以根据条件使用不同的值来初始化。

// 如果编译器可以推导出类型,那么不用显示声明类型。
val answer = 10
// 确保只有唯一一条初始化语句被执行
val answer: Int
if (...) {
  answer = 10
} else {
  answer = 20
}

1.2 字符串模板

字符串模板指在String值中引用局部变量,只需在变量名前加上$,其效率等价于Java中的字符串拼接。除了直接引用变量,还可以使用表达式,只需用{}包裹表达式即可。

// 引用变量
val s = "world"
print("hello $s")
// 引用表达式
val array = mutableListOf("hello", "world")
println("message is ${array[0]} and ${array[1]}")

其实编译后的代码创建了一个StringBuilder对象,并把常量部分和变量部分附加上去。

1.3 类和属性

在Kotlin中声明一个实体类非常简单,不需要像Java一样声明所有属性和getter/setter方法。下方就是一个包含name属性和isMarried属性的Person实体类,它没有声明类的访问权限,Kotlin中的默认可见性就是public。

class Person(val name: String, var isMarried: Boolean)

Kotlin会为Person类的属性生成getter/setter方法,name属性是val变量,只会生成getter方法;而isMarried属性是var变量,会生成getter和setter方法。

访问Person对象的属性是通过person.name的方式,内部实际调用person.getName()方法;修改属性是通过person.isMarried = true,实际调用person.setMarried(true)方法。对于那些在Java中定义的类,一样可以使用Kotlin的属性语法。

1.4 迭代语法

Kotlin在迭代数字时使用了区间的概念,有两种常见的操作符如下所示,".."操作符表示闭区间,"until"操作符表示左闭右开区间。

// 表示[1, 10]
for (i in 1..10) {
    println(i)
}
// 表示[1, 10)
for (i in 1 until 10) {
    println(i)
}

这两种迭代的步长都为1,如果想跳过一些数字,可以在迭代时指定步长。

for (i in 100 downTo 1 step 2) {
    // ......
}

迭代map也可以用很简洁的模式,如下所示,使用for循环展开迭代中的集合的元素,把展开的结果存储到了2个独立的变量中。

val map = TreeMap<String, String>()
// 添加元素至map......
for ((key, value) in map) {
    // ......
}

1.5 异常处理

Kotlin中throw结构是一个表达式,能作为另一个表达式的一部分使用。如果number值正常,则percentage变量会被初始化,否则变量不会被初始化,直接抛出异常。

val percentage = 
    if (number in 0..100)
        number
    else 
        throw IllegalArgumentException("......")

与之类似的是,异常处理的"try"也可以作为表达式,下方的代码在不出现异常时可以得到正确地值,出现异常时number会被赋值为null。

val number = try {
    Integer.parseInt(str)
} catch (e: NumberFormatException) {
    null
}

二、函数

2.1 Kotlin函数基础

在Kotlin中可以为函数的参数指定默认值,调用时可以省略这些有默认值的参数。调用Kotlin函数可以像Java一样按照参数的顺序传参,在参数较多时也可以显示指定参数的名字来避免混淆。

// 函数定义
fun collect(collection: Collection<String>, separator: String = ", ",
            prefix: String = "<", postfix: String = ">") {
    // ......
}

// 函数调用,显示指定了参数名,并省略了两个默认参数
collect(collection = strs, separator = " ")

由于Java没有参数默认值的概念,当从Java中调用Kotlin函数时必须显示指定所有参数值。如果需要从Java代码中频繁地调用,并且希望能对Java的调用者更简便,可以使用@JvmOverloads注解,它会让编译器为函数生成Java重载函数,调用时就可以从最后一个参数开始省略。

2.2 扩展函数与属性

Kotlin可以为现有的类添加扩展函数,用以平滑地与现有代码集成。例如可以为String类添加一个扩展函数来获取字符串的最后一个字符。

package com.test

fun String.lastChar(): Char {
    return this[this.length - 1]
}

定义后的扩展函数不会在整个项目范围内生效,在使用扩展函数时需要导入。除了直接导入原函数外,还可以为扩展函数指定别名。

// 导入扩展函数
import com.test.lastChar
println("Kotlin".lastChar())

// 使用别名导入扩展函数
import com.test.lastChar as last
println("Kotlin".last())

扩展函数的本质是静态函数,因此不存在重写。当从Java调用Kotlin的扩展函数时,只需要通过文件名调用该扩展函数,并将接受者对象作为第一个参数传入即可。

扩展属性实际是通过扩展类的API来访问属性,例如可以为StringBuilder类定义一个lastChar属性表示最后一个字符,内部定义getter和setter方法,在Kotlin中访问属性时实际调用了getter或setter方法。当从Java中访问扩展属性时,需要显式调用其getter或setter函数。

var StringBuilder.lastChar: Char {
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }
}

2.3 Kotlin函数的其余特性

2.3.1 可变参数

在Kotlin中可以通过val list = listOf(1, 2, 3)这样的形式来创建列表等集合,此时可以传递任意个参数,这就是可变参数。在Java中可以通过...表示可变个参数,而Kotlin中使用vararg,如下所示。

fun listOf<T>(vararg values: T): List<T> {
    //......
}

2.3.2 中缀调用

当使用mapOf(1 to "one", 2 to "two", 3 to "three")来创建map时,可以通过to表示键值的对应关系,to并不是关键字,而是表示一种函数调用。在声明中缀调用时,需要使用infix修饰符,一个to函数的声明如下。

infix fun Any.to(other: Any) = Pair(this, other)

三、类,对象和接口

3.1 类继承结构

3.1.1 接口

Kotlin声明接口的关键字与Java一样都是interface,并且可以像Java8一样为接口的方法提供默认实现。

interface clickable {
    fun click()
    fun showoff() = println("call showoff in clickable")
}

interface Focusable {
    fun setFocusable()
    fun showoff() = println("call showoff in focusable")
}

如果一个类实现了上面2个接口,那么必须实现showoff()方法,否则编译器会报错,如果想调用某一个接口的默认实现,则需通过super指定该接口/父类。

override fun showoff() = super<Clickable>.showoff()

Java中的类默认是可以被继承的,也可以重写父类的方法,但是这也可能导致子类不正确行为。因此在Kotlin中,类和方法默认都是final的,对可被继承的类需要使用open修饰符,对每一个可被重写的方法都要添加open修饰符。

open class Button: Clickable() {
    fun disable()
    open fun animate() {...}
    override fun click() {...}
}

3.1.2 可见性修饰符

Kotlin的可见性修饰符与Java一样是public、protected与private,但是默认的可见性有所不同:Java中为protected,Kotlin中为public。需要注意的是,Kotlin中的protected成员只在类和子类中可见,并且类的扩展函数不能访问它的private和protected成员。

Kotlin还有个关键字internal表示“只在模块内部可见”,模块指的是一组一起编译的Kotlin文件,internal对模块提供了细节实现的封装。在Java中这种封装很容易被破坏,因为外部代码可以将类定义到相同的包中从而得到访问模块私有声明的权限。

3.1.3 内部类

Java中的内部类隐式持有外部类的引用,内部类可以直接访问外部类的属性和方法,如果不希望内部类持有外部类的引用,可以使用static修饰内部类。

Kotlin中的默认内部类与Java中static修饰的内部类相同。如果希望内部类持有外部类的引用,需要使用inner修饰内部类。在内部类中访问外部类的引用时,需要使用this@Outer访问外部类。

class Outer {
    inner class Inner {
        fun getOuterRef(): Outer = this@Outer
    }
}

3.2 构造方法

Kotlin可以用很简单的方式声明一个类和它的构造函数,如下所示。User类包含nickname属性,并且有一个以nickname为参数的构造函数。

class User(val nickname: String)

这是声明User类的简写,如果不采用简写,那么是这样的。

class User constructor(_nickname: String) {
    val nickname: String
    init {
        nickname = _nickname
    }
}

如果需要将构造函数私有化,可以使用private修饰构造器。

class Secretive private constructor() {}

在Java中使用private修饰构造器表示一个更通用的意思:这个类是一个静态工具成员或是单例。Kotlin针对这种特性提供了语言级别的功能,例如我们之后会提到的lazy。

当为一个类(例如Android中的视图)声明多个构造函数时,可以通过super或this关键字使该构造方法调用父类或者自己的构造方法,如下所示。

class CustomView: View {
    // 调用当前类的构造方法
    constructor(context: Context): this(context, null) {
        // ......
    }

    // 调用父类的构造方法
    constructor(context: Context, attr: AttributeSet): this(context, attr) {
        // ......
    }
}

3.3 实体类

在使用Java实现实体类时,我们一般需要重写toString(), equals()hashCode()方法,现在来看看用Kotlin如何实现,如下所示。实现equals()方法后,在Kotlin中可以直接通过"=="表示两个对象是否equals,如果想要像Java中表示两个对象的引用是否相等,那么需要使用"==="操作符。

class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode

    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client) {
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }

    override fun toString(): String = "Client(name = $name, postalCode = $postalCode)"
}

顺便提一下为什么要重写hashCode()方法:如果只重写equals()而不重写hashCode()方法,那么对象的hash值就是对象引用地址的hash。在对HashMap等数据结构调用add()方法时,会计算当前对象的hashcode判断集合中是否已经存在同样hash的对象,如果存在相等的hashcode,会再判断是否存在equal的对象。显而易见,如果不重写hashcode()方法,向HashMap中不断add()相等的对象时,由于它们的hashcode不相等,都会被添加到集合中。

每次实现一个实体类都要重写这3个方法,看起来有点麻烦。不过Kotlin提供了data关键字来修饰实体类,toString(), equals()hashCode()这3个方法会被自动创建。

data class Client(val name: String, val postalCode: Int)

这种实体类的属性都应该修饰为val,表示对象创建后不可变,只有不可变对象才能作为HashMap的Key。而且在多线程编程中不可变对象也更有优势,因为不用担心其他线程修改了它的状态。

3.4 object关键字

通过object关键字可以很轻易地实现单例模式,object表示定义一个类并创建一个对象,如下所示。由于这是单例,可以直接通过Singleton.function1()这样的形式调用单例的方法。在Java中则需使用Singleton.INSTANCE.function1()调用。

object Singleton {
    fun function1() {...}
}

当object修饰的类是嵌套类时,在整个系统中同样只具有一个实例。

data class Person(val name: String) {
    object NameComparator: Comparator<Person> {...}
}

使用companion object关键字可以构建伴生对象,伴生对象定义在类中,表示这是当前类的单例,可以直接通过A.function1()的方式调用伴生对象的方法。在Java中可以通过A.Companion.function1()的方式调用伴生对象的方法。

class A {
    companion object {
        fun function1() {...}
    }
    // 也可以为伴生对象指定名字, 调用时通过 A.Obj.function2()
    companion object Obj {
        fun function2() {...}
    }
}

四、Lambda编程

4.1 Lambda基础

Lambda编程可以将函数作为值使用,在调用方法时直接传递一段代码作为形参。例如可以定义一个函数,表示求2个值的和。

val sumFun = {x: Int, y: Int -> x + y}

当需要获得集合中满足某个条件的对象时,例如获取Person集合中年级最大的对象,可以使用people.maxByOrNull({ p: Person -> p.age })。当Lambda是函数调用的最后一个实参时,可以省略括号,而且可以用it指代Lambda的参数,因此最后可以简写为people.maxByOrNull({ it.age })

那么这个maxByOrNull()做了什么呢?来看一下它的源码。其中T泛型表示集合的类型,R泛型表示selector返回的结果。再看函数逻辑,maxByOrNull()函数遍历了集合并对每个元素调用selector(e)方法得到v,最后得到集合中v最大的元素。

public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var maxElem = iterator.next()
    if (!iterator.hasNext()) return maxElem
    var maxValue = selector(maxElem)
    do {
        val e = iterator.next()
        val v = selector(e)
        if (maxValue < v) {
            maxElem = e
            maxValue = v
        }
    } while (iterator.hasNext())
    return maxElem
}

在Java中使用匿名内部类时,匿名内部类中的代码可以访问外部的final对象,在Kotlin中可以做到同样的事情。有意思的时,Kotlin允许在lambda内部访问非final变量甚至修改它们。当从lambda内部访问外部变量时,称这些变量被lambda捕捉,就像下方例子的prefix。

fun printMsg(messages: Collection<String>, prefix: String) {
    messages.forEach { 
        print("$prefix $it")
    }
}

当捕捉final变量时,变量和lambda会被存储并稍后执行;当捕捉非final变量时,该变量会被封装到一个特殊的包装器,随后包装器和lambda会被存储并稍后执行。

4.2 集合的函数式API

假设有个整型集合val list = listOf(1, 2, 3, 4),现在通过list来看集合API的功能。

  1. filter: 对集合过滤,传入的lambda表示过滤条件。list.filter{ it % 2 == 0 }结果为{2, 4}
  2. map: 对集合中的每一个元素运行lambda,得到一个全新的集合list.map{it * it}结果为 {1, 4, 9, 16}
  3. all: 判断集合中的元素是否都满足lambda的条件。list.all { it <= 4 }结果为 true
  4. any: 判断集合中是否存在一个匹配lambda的元素list.any { it == 4 }结果为 true
  5. count: 判断集合中有多少个元素满足条件。list.count { it <= 3 }结果为3
  6. find: 找到一个满足条件的元素,同义方法firstOrNulllist.find { it <= 3 }结果为1

除了上述这些基础的集合API,还有一些可以转换集合的API,例如groupBy、flatMap等。
groupBy根据集合元素的特征将它们划分为不同的组,得到一个map,举个栗子。

val people = listOf(Person("A", 20), Person("B", 19), Person("C", 20))
people.groupBy{ it.age }

这里根据Person的age分组,得到一个的map,其中key为20的有Person("A", 20), Person("C", 20)这2个元素,key为19的有Person("B", 19)这1个元素。

flatMap会根据lambda对元素做变换,然后把列表合并(平铺)成一个列表。下方的代码先把字符串转为list,生成了[a, b, c]和[d, e, f]这2个list,随后合并为一个,结果为[a, b, c, d, e, f]。

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

4.3 序列:惰性集合操作

上面提到的map和filter等操作符会返回一个集合对象。假设当前有一个需求,要得到people中名字以"A"开头的名字列表,你可能会通过people.map(Person::name).filter{ it.startsWith("A") },但由于这2个操作符都会创建中间集合,那么上方的链式调用会创建2个列表而降低效率。

针对这种情况我们可以使用序列,而不是直接使用集合,如下所示。Sequence接口表示一个可以逐个枚举元素的序列,Sequence只提供了一个方法iterator用来获取元素值。

people.asSequence()
    .map(Person::name)
    .filter{ it.startsWith("A") }
    .toList()

对序列操作时,会将操作符依次应用在每一个元素上,如果在遍历完元素之前得到过了结果,那么之后的元素都不会发生变化。例如下面这个例子,处理到2时就得到了结果,之后的元素不会被处理,这就是惰性的含义。

listOf(1, 2, 3, 4)
    .asSequence
    .map{ it * it }
    .find{ it > 3 }

4.4 带接收者的Lambda

with是一个接受两个参数的函数,第一个参数为Lambda的接收者,第二个参数为Lambda。在Lambda中可以通过this访问接收者对象,一般来说this可省略,with的返回值就是Lambda的运行结果。例如下方代码就会返回"Hello World"的String。

with(StringBuilder()) {
    append("Hello ")
    append("World")
    toString()
}

apply与with的用法类似,区别是apply会返回作为实参传递给它的对象,就是接收者对象,例如下方代码会返回一个StringBuilder。由于apply返回接收者对象的特性,可以将其用于对象初始化。

StringBuilder().apply {
    append("Hello ")
    append("World")
}

五、Kotlin的类型系统

5.1 可空性

Kotlin在避免空指针异常上做出了很多努力,其中最重要的一条就是支持可空类型,这意味着你可以在程序中指出哪些变量是可以为null的,而哪些变量是不允许的。如果一个变量允许为null,那么直接对它调用方法是不安全的,这样的设计可以避免很多异常。例如,在Kotlin中使用var s: String = ""声明的String变量不允许为空。如果需要一个可以为null的变量,那么需要在类型后面加上?,例如var ss: String? = null就声明了一个可空的String变量。

5.1.1 安全调用运算符"?."

对于可空的变量,需要使用安全调用运算符"?.",它会将null检查与和一次方法调用合并成一个操作。例如s?.toUpperCase()等价于if (s != null) s.toUpperCase() else null。也就是说,如果变量为null,"?."调用后的结果也为null。因此当需要对可空类型链式调用时,可以采用stringBuilder?.append("...")?.append("...")这样的形式。

5.1.2 通过"?:"提供默认值

当需要对null变量提供默认值时可以使用"?:"操作符,例如val r: String = s ?: ""就使用空字符串代替了null:表示s不为null时使用s的值,为null时使用空字符串。由于return和throw这样的操作也是表达式,可以跟在"?:"后面,当"?:"左边的值为null时直接返回或抛出异常。

5.1.3 安全类型转换"as?"

当在Kotlin中使用"as"进行类型转换时,如果类型不匹配会抛出ClassCastException异常,虽然可以用"is"来检查类型,但是不够简洁。而使用"as?"可以进行安全的类型转换,如果类型不匹配则返回null而不是抛出异常。

5.1.4 "let"函数

处理可空表达式可以使用"let"函数,例如当前有个函数fun send(s: String)只接受不为空的参数,如果有个可空类型的字符串则需要显式判断它是否为空再调用方法。
不过还有一种方式是通过"let"函数:s?.let {send(it)},"let"的作用就是把调用它的对象作为lambda表达式的参数,结合安全调用的方法,它能将调用let函数的可空对象转化为非空类型。

5.1.5 延迟初始化的属性

很多框架都会在实例创建之后用专门的方法来初始化对象,此时需要将属性声明为可空类型,因为属性在定义时都是为空的,之后每次使用这些属性时都需要判空或者使用"?."进行安全调用。
为了解决这个问题,可以使用lateinit修饰符将变量声明为可以延迟初始化的。延迟初始化的属性都是var,因为需要在构造方法外修改它的值。

class Test {
    private lateinit var service: MyService

    fun setUp() {
        service = MyService()
    }
    
    fun testAction() {
        service.performAction()
    }
}

5.1.6 可空性与Java

Kotlin与Java是可以无缝兼容的,而Java中并不存在可空类型,此时Kotlin该如何调用Java代码呢,是不是使用每个值之前都需要检查是否为null?
其实Java也有时候包含了可空性的信息,例如@Nullable@NonNull这两个注解就分别表示可空和不可空。但是大部分情况下,Kotlin都不知道Java类型的可空性信息,这种不清楚可空性的类型被称为平台类型,开发人员需要对平台类型的操作负有全部责任,需要像在Java中一样进行判空。

5.2 基本数据类型

Java中对基本数据类型和引用类型进行了区分,例如int这样的基本数据类型直接存储了它的值,而一个引用类型存储了该对象的内存地址引用,对于int类型提供了包装类Integer作为引用类型。
不过Kotlin并不区分基本数据类型和包装类型,对于整型永远使用Int,这并不意味着所有的Int都是对象:大多数情况下Kotlin中的Int会被编译为Java中的int,只有作为泛型(用于集合中)或者使用可空类型时才会被编译为包装类型。

Kotlin与Java处理数字转换的方式是不一样的:Kotlin不会自动地把数字从一种类型转换为另一种,即使是范围更大的类型。例如当前有val i = 1,使用val l: Long = i时会出现类型不匹配的错误,必须使用val l: Long = i.toLong()进行显式转换。Kotlin规定所有的基本数据类型转换都必须是显式的,并为除了Boolean以外的基本数据类型都定义了转换函数。

Kotlin使用"Any"和"Any?"作为根类型,这就像Java使用Object作为所有引用类型的超类型。当使用Object时,必须要使用Integer这样的装箱类型来表示基本类型的值。而在Kotlin中,Any时所有类型的超类型,包括Int这样的基础数据类型。
和Java一样,当使用val v: Any = 1把基本数据类型的值赋值给Any时,变量会被自动装箱。这里的Any表示变量不可为空,如果变量可能为null,则需要使用Any?类型,Any在底层对应Java的Object类型。

5.3 集合与数组

在集合方面,Kotlin支持类型参数的可空性,例如List<Int?>是持有Int?类型值的列表,即可以持有Int或null,而List<Int>?指的是列表本身可能为空。针对List<Int?>这种持有可空类型的集合,Kotlin提供了标准库函数filterNotNull()过滤空元素,一个List<Int?>类型的集合过滤后就不存在可空元素了,就变成了List<Int>类型。

Kotlin将集合的访问和修改接口分开了,kotlin.collections.Collection接口可以执行访问集合的操作,但是没有添加或移除元素的方法。使用kotlin.collections.MutableCollection接口可以修改集合中的元素。

只读集合与可变集合的分离使得程序的可读性更强,如果函数接受Collection作为参数,就代表它不会修改集合;如果函数接受MutableCollection作为参数,则认为它会修改数据,如果你使用了集合作为组件状态的一部分,可以考虑拷贝一份再传递给这样的函数。需要注意的是,只读集合不一定是不可变的,因为MutableCollection接口继承自Collection接口,某个只读集合可能只是同一个集合众多引用中的一个。

在Kotlin中只读接口和可变接口的基本类型与java.util中的Java集合接口是平行的,可变接口直接对应java.util包中的接口,而只读版本缺少了所有产生改变的方法。下表展示了Kotlin创建集合的函数。

集合类型 只读 可变
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, LinkedSetOf, sortedSetOf
Map mapOf mutableMapOf, hashMapOf, LinkedMapOf, sortedMapOf

除了集合,Kotlin也支持创建数组,我们可以通过arrayOf创建一个数组,或者arrayOfNulls创建一个包含可空类型元素的数组。也可以通过val ss = Array<Int>(10) {...}这样的方式,这里面的Lambda表达式用于创建每一个数组元素。不过这种方式下声明的数组的元素类型都是装箱类型(如Integer),如果想要创建基本数据类型的数组,可以使用IntArray, ByteArray, CharArray等,它们对应Java中的int[]等基本数据类型数组。

val zeros = IntArray(5)
val zeros2 = IntArrayof(0, 0, 0, 0, 0)
val squares = IntArray(5) -> {i -> i * i}

六、运算符重载等约定

6.1 基础运算符重载

Kotlin提供了一系列的运算符重载方法,当重写这些方法后,就可以使用运算符直接调用这些方法,可重载的二元运算符如下所示。

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

举个栗子,假设当前有一个data类Point,重写其plus(...)方法如下,对于重载运算符的方法需要加上operator关键字,之后就可以通过"+"运算符对两个Point对象调用加法。
自定义类型的运算符基本与标准数字类型的运算符有着相同的优先级,例如在a + b * c中,乘法始终在加法之前执行。

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

// 调用时如下所示
val p1 = Point(1, 1)
val p2 = Point(2, 2)
println(p1 + p2)

很多情况下一个类没有重载运算符,那么需要使用扩展方法的形式。

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

运算符重载支持两种不同的类型进行运算,例如可以重载Point的乘法操作表示缩放。值得注意的是,运算符重载不支持交换律,例如下面这种情况只支持p * 1.5,如果希望还能使用1.5 * p,需要单独定义operator fun Double.times(p: Point): Point方法。

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

除了二元运算符,还可以重载以下一元运算符。

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

Kotlin还提供了"+=", "-="这一类复合赋值运算符的重载,例如"+="的重载方法为plusAssign()。一般来说,plus()plusAssign()只要重载其中一个即可,如果重载了2个,在调用"+="时可能2个函数都适用,编译器会报错。

Kotlin标准库中的集合支持这两种方法,+和-运算符总是返回一个新的集合。+=和-=用于可变集合时就始终修改它们,而用于只读集合时,会返回一个修改过的副本。

6.2 重载比较运算符

在Kotlin中可以对任何对象使用比较运算符(==, !=, >, <等),当使用==运算符时,它会被转换成equals()方法的调用。运行a==b时,实际得到的是a?.equals(b) ?: (b == null)的结果。而Kotlin中的恒等运算符===与java中的==完全相同,它用于检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查它们是否是相同的值)。

调用比较运算符时会转化为compareTo()方法的调用,该方法必须返回Int值,调用a >= b时返回a.compareTo(b) >= 0的结果。所有在Java中实现了Comparable接口的类,都可以在Kotlin中使用运算符语法。

6.3 解构声明和组件函数

解构声明用于展开单个复合值,并使用它来初始化多个单独的变量。实际上解构声明用到了Kotlin的约定原理,对于data类,编译器为每个在主构造方法中声明的属性生成一个componentN()函数(N代表第N个属性,最多5个)。

val p = Point(10, 20)
val (x, y) = p

对于非data类,可以手动为它们生成componentN()函数。

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

解构声明也可以用于迭代map,这是因为Entry上有扩展函数component1()component2(),分别返回Entry的key和value。

fun printEntries(map: Map<String, String>) {
    for ((key, value) in map) {
        // ......
    }
}

6.4 委托属性

委托属性表示将属性的访问器逻辑委托给了另一个对象,通过关键字by对其后的表达式求值来获取这个对象,关键字by可以用于任何符合属性委托约定规则的对象。

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

按照约定,Delegate类必须具有getValue()setValue()方法,后者仅适用于可变属性,当调用Foo.p = newValue时实际调用了Delegate#setValue()方法。下方定义了委托类的2个方法,其中参数p表示接收属性的实例,参数prop表示属性本身,这个属性的类型为KProperty。

class Delegate(var propValue: Int) {
    operator fun getValue(p: Type, prop: KProperty<*>): Int { }
    operator fun setValue(p: Type, prop: KProperty<*>, newValue: Int) { }
}

属性委托经常与lazy函数一起用于实现惰性初始化,例如val emails by lazy { loadEmails() },只有在emails变量被第一次使用时才会调用loadEmails()方法对其进行初始化。lazy函数的参数是一个lambda,默认情况下lazy函数是线程安全的,当然也可以设置使用别的锁或完全避开同步。

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

7.1 高阶函数

高阶函数就是指以另一个函数作为参数或返回值的函数,Kotlin中的函数可以通过lambda函数或函数引用来表示。函数类型的显示声明如下,Unit表示函数不返回任何有用的值。

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

声明函数时,编译器可以推导出变量是否为函数类型,因此可以不声明类型。

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

7.1.1 将函数作为参数

下面声明一个高阶函数,它以函数作为形参。此时需要显示指定函数的类型,包括函数的参数类型以及返回值的类型。

fun advancedFunction(operation: (Int, Int) -> Int) {
    val result = operation(...)
}

如果实现一个基于String类型的filter函数,其中传入predicate函数来表示过滤规则。

fun String.filter(predicate: (Char) -> Boolean) : String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) {
            sb.append(element)
        }
    }
    return sb.toString()
}

Kotlin可以为函数参数指定一个默认值表示默认行为,该默认值也可以为空。

// 指定默认函数实现
fun function(callback : (() -> Unit) = {
        println("default invoke")}) {
    callback()
}

// 函数参数可以为空
fun function(callback : (() -> Unit)?) {
    callback?.invoke()
}

7.1.2 将函数作为返回值

将函数作为返回值时,需要指定该函数的类型,包括函数的参数类型和返回值类型。

fun getCalculator(type: Int): (Int) -> Int {
    if (type == 1) {
        return {value -> value * 10}
    } else {
        return {value -> value * 20}
    }
}

7.2 内联函数:消除Lambda的运行时开销

当一个函数被声明为inline函数时,编译器不会为其生成一个函数,而是使用该函数的真实代码替换每一次调用。以集合的filter函数为例,该函数被声明为inline函数,参数中传递的predicate函数在调用时也会被内联。

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

不过使用inline关键字只能提高带lambda参数的函数的性能,因为不内联的话,lambda表达式会在调用时生成一个匿名类对象,从而影响性能。普通函数不需要使用inline去声明,编译器会进行优化。将函数声明为inline时需要注意代码的长度,如果代码过长,将函数的字节码拷贝到每一个调用点会极大地增大字节码的长度。

7.3 高阶函数中的return

如果在lambda中使用return,会从调用lambda的函数中返回,这样的return语句称为非局部返回。需要注意的是,只有当该lambda函数为inline函数时才能从更外层的函数返回。
如果想要在lambda中返回,则需要为该lambda指定标签,然后return该标签,如下所示:

list.forEach label@ {
    if (it > 5) {
        return@label
    }
    // ...
}

7.4 Kotlin自带高阶函数

7.4.1 let函数

public inline fun <T, R> T.let(block: (T) -> R): R {}

根据let方法的定义,它接收一个类型为(T) -> R的方法作为参数,在lambda中可以通过it来访问调用let的对象。一般可以用let函数来进行判空后的一些操作。

var str : String? = null
// str操作...
val len = str?.let {
    it.length
}

7.4.2 run函数

public inline fun <R> run(block: () -> R): R {}
public inline fun <T, R> T.run(block: T.() -> R): R {}

主要关注第二个run函数,其传入的block类型为T.() -> R,表示这是一个带接受者的lambda,会将当前的T对象携带到lambda中,即可在block中直接访问T的属性和方法。

val sb = StringBuilder()
// 如果传入的参数可能为空, 则需使用?.操作符, 为空时不执行lambda
val str = sb.run {
    append("abc")
    append("def")
    toString()
}

7.4.3 with函数

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {}

with函数与run函数的区别在于,它将receiver放入了参数中,因此调用的方式也有点不同。run函数可以在调用之前就进行判空,但是with函数就要在lambda内部判断。

val len = with(sb) {
    this?.append("abc")
        ?.append("def")
        ?.length
}

7.4.4 apply函数

public inline fun <T> T.apply(block: T.() -> Unit): T {}

从函数定义来看,apply函数就是没有返回值的with函数,一般用于对象新建后的初始化,如下所示。

val test = Test().apply {
    param1 = true
    param2 = "test"
    param3 = 1
}

八、泛型

8.1 泛型类型参数

在使用泛型类型参数时可以使用类型参数限制,例如为类型形参指定上界,将类型指定为Number的子类:

fun <T : Number> List<T>.sum(): T

对于复杂的参数,也可以为其指定多个约束,例如规定类型实参必须实现CharSequence和Appendable接口。

fun <T> ensure(seq T)
    where T: CharSequence, T: Appendable {
    // ......
}

而没有指定上界的类型形参将会使用Any?这个默认的上界,因此调用时需要使用?.操作符。如果想保证替换类型始终是非空类型,可以通过Any代替默认的Any?作为上界。

8.2 泛型擦除与实化类型参数

Kotlin在不进行特殊声明的情况下,和Java一样进行了泛型擦除,因此下面的代码是无法编译的。

fun <T> isT(value : Any) = value is T

对于List<*>来说,在运行时只能判断当前对象是否为List,而不能判断具体的类型实参。例如if (value is List<String>)就无法被编译。如果想判断对象是否为List,那么可以使用if (value is List<*>)来检查。

Kotlin的内联函数可以实化泛型,被实化的泛型需要用reified标记。在编译时,内联函数生成的字节码会插入到函数调用的地方,而字节码引用了具体的类,而不是类型参数,因此不受泛型擦除的影响。

inline fun <reified T> isT(value : Any) = value is T

例如Kotlin标准库中的filterIsInstance()方法用于返回指定类的实例,它的简化实现如下。

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val destination = mutableListOf<T>()
    for (element in this) {
        if (element is T) {
            destination.add(element)
        }
    }
}

还有一个例子是简化Android中的startActivity,可以使用实化类型参数来代替activity类。

inline fun <reified T: Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

startActivity<DetailActivity>()

8.3 协变

一个协变类是一个泛型类,如Producer<T>,对于这种类来说,如果A是B的子类型,那么Producer<A>就是Producer<B>的子类型。如果要声明类在某个类型参数上是可以协变的,在类型参数前加上out关键字即可。

interface Producer<out T> {
    fun produce(): T
}

在类成员的生命中,类型参数的使用分为in位置和out位置,如果函数把T当做返回类型,那它在out位置;如果T用作函数参数的类型,那么它在in位置。

例如Kotlin中的List<Interface>接口,由于List是只读的,所以它只有一个返回类型为T的get()方法,所以T在out位置。

interface List<out: T>: Collection<T> {
    operator fun get(index: Int): T
}

推荐阅读更多精彩内容