这是阅读Kotlin Programming的笔记,基本上每个点都记录到了,但比较简略,不过这本书本来也比较简略,Kotlin也是不断发展的语言,有问题时再去查询。

基础

函数声明

fun main(args: Array) {

}
在一个函数声明中,必须为每一个参数指明类型,参数可以有默认值,并且每个参数都是只读的,相当于用val声明的变量一样。
上面的例子中没有写出函数的可见性和返回类型,对于可见性而言,省略意味着public;对于返回类型而言,省略意味着Unit。
kotlin中在函数内声明的变量作用域仅限于函数内,这一点和Java、C/C++一致,虽然看起来强调这一点有些没有必要,但毕竟有些语言比如python对此有不同的约定。

变量声明

var a; // 可读可写变量
val b; // 只读变量

编译时常量

编译时常量必须位于全局命名空间,类型必须是内置类型,当然对于Java这样的语言而言是没有所谓的编译时常量的,对于Java这样的概念会被实现为类常量成员。
const val MAX_EXPERIENCE: Int = 5000

上面的例子中,const标记编译时常量,编译时常量必须是只读的,因此const必须和val配合使用。看起来const有点多余,毕竟我们使用const是为了确保常量不变,而val已经提供了这一层保证了,但是const和val在语义层面上是不一样的,编译器可能会区分不同的语义来做一些内部的优化。

文件作用域变量

直接位于文件作用域下的变量在整个工程的任何地方都是可以直接访问的(当然可以通过限制可见性来进行限制),像这样的变量必须在声明的同时定义,否则会被当作一个编译错误。存在这样的限制是为了避免访问未初始化的全局变量可能带来的问题;对于局部变量而言编译器能够轻易分析出它在何处被使用,因此只需要在变量被读之前初始化即可,如果在访问变量前没有进行初始化编译器会报错。

常用内置类型

内置类型使用时不需要import,kotlin支持的内置类型有:

  • String
  • Char
  • Boolean
  • Long Int Short Byte
  • Double
  • List
  • Set
  • Map

kotlin中没有基础类型

kotlin中没有定义Java中的基础数字类型,原因是基础数字类型无法用于泛型,而且其语义和语法比较特殊。
不过,这并不意味着kotlin代码运行效率低下,以Java为例,kotlin会检查代码上下文,如果的确没必要使用对象表示数字这种基础类型,kotlin编译出来的java字节码会将变量类型变为基础类型,以提升效率。这样,kotlin对外提供了清晰一致的语法和语义,同时兼顾了代码执行的效率。

类型推断

大多数情况下都不需要在定义变量时声明类型,因为kotlin会根据等号右侧的值的类型推断变量类型,kotlin中类型是没有歧义的。
有时需要先声明再赋值,这种情况下则必须声明类型,否则kotlin就不知道变量的类型。这里和C++很像,但和groovy这样更具动态性的语言不同。

字符串拼接

和java一样,kotlin String支持+操作符;
但是在kotlin中更推荐使用字符串模板,kotlin允许在字符串模板的${}之间插入任意的【值表达式】,条件赋值可以用在这里:
“$name: ${healthStatus}aaaaa”
“isReal? ${if (isReal) “YES” else “NO”}}”

相等判断

java中常常需要使用equals方法对比两个引用是否虽然引用不同但内容相同,使用==对比两个引用是否引用了同一个对象。
在kotlin中,使用== / !=替代equals,使用 === / !==替代==。

条件赋值

if

kotlin支持一种比较特殊的赋值语句,允许在变量定义式中用条件判断代替值,条件判断的每一个分支直接写要定义的值:
val a = if(…) {“branchA”} else {“branchB”}
上面的语句可以去掉大括号,虽然一般建议if else总是应该写大括号,但这样写在一行的简单逻辑可以是个例外。话虽如此,但kotlin社区选择的规范是永远要写大括号。

when

kotlin推荐在条件比较多时选用when做条件赋值:
val faction = when (race) {
1 -> 1.23
2 -> 2.34
3 -> 3.43
4 -> 4.34
in 5..8 -> 6.43
in 9..13 -> if (larger) 12.4 else 9.2
else -> 0
}
when表达式语法比较灵活,可以这样理解:如果分支判断语法缺少了比较的对象,那么when修饰的对象会被插入;如果分支判断只是一个普通值,那么when修饰的对象会使用==判断是否满足分支。
when表达式是对其他语言中的switch-case的完全替代。

in关键字表示范围

java中常搭配使用>=和<=来判断范围,这种写法不太直观,因此kotlin提供了in关键字和..关键字。
in关键字可判断变量是否处于Range范围内,而..关键字用于生成Range对象(当然也可以选择不用..关键字而是写出rangeTo函数名,在希望先写上界再写下界时可以使用downTo函数)。
if (a in 1..10) {…}

单句表达式函数

如果一个函数body只包含一条语句,就可以省去return关键字和大括号:

  1. private fun getActionByName(actionName: String) = // return Int since all branch is Int
  2. when(actionName) {
  3. "ready" -> 1
  4. "start" -> 2
  5. "stop" -> 3
  6. else -> 0
  7. }
  8. private fun sayHi() = println("Hi!") // return Unit

无返回值函数是Unit函数

kotlin中,如果一个函数没有声明返回类型,意味着它的返回类型是Unit,Unit在kotlin中表示没有返回值的函数的返回类型。
其他语言通常使用void表示函数没有返回值,但void在泛型方面会造成麻烦,Java被迫引入Void类型代表一个不存在的值。kotlin为了规避泛型方面的问题,从一开始就引入Unit类型。

不返回的函数是Nothing函数

Nothing和Unit唯一的共同点就是表示函数不回返回值,但Nothing进一步表示函数不会return,不会结束,什么样的函数不会结束?一个一定会抛出异常的函数、一个用于终止当前程序的函数。
kotlin使用Nothing定义一个TODO函数,表示一个待实现的功能:
public inline fun TODO(reason: String): Nothing = throw NotImplementedError(“An operation is not implemented: $reason”)
kotlin编译器不会检查这样的函数之后的任何语句,因为后面的语句一定不会执行了。

指定函数调用参数名称

Java和C++调用函数时需要按照函数定义的参数顺序挨个传入参数,kotlin允许调用函数时指定函数参数名称来以不一样的顺序传入参数,当然你也可以以同样的顺序调用,此时写出函数参数名称有助于提高可读性。

Lambda/匿名函数

匿名函数使用大括号包围,匿名函数本身有类型,可以使用变量引用匿名函数:
val greetingFunction: () -> String = {
val currentYear = 2018
“Welcome, it’s ${currentYear}”
}
就像上面的例子一样,匿名函数里面不用写return,事实上匿名函数里面,除了一些场景外,是禁止写return关键字的,最后一行表达式会被当作结果返回,这是为了避免误导编译器,导致编译器不知道return表示的是从被调用的匿名函数中返回还是从调用匿名函数的函数返回。

当匿名函数没有参数时,可以不必写出形参;当匿名函数只有一个参数时,也可以(应该在逻辑简单意图明显的情况下)省略形参,此时可以用it引用唯一的参数;当有超过一个参数时,必须写出来:
var func: (String, String) -> String = { first, second ->
“$first, $second”
}

匿名函数变量的声明定义规则没有什么特殊的,这意味着可以省略声明时的类型,由编译器推断,但此时如果匿名函数有参数,必须写出参数类型:

  1. // 编译器自动推断的类型为(String, String) -> String
  2. var func = { first: String, second: String ->
  3. "$first, $second"
  4. }
  5. var name = func("hl", "hi")

Lambda和DSL

函数参数类型可以是lambda,当lambda是参数列表中的最后一个时,可以将lambda写在函数调用括号的外面,这种写法是Lambda DSL的基础。

inline函数

kotlin中可以使用inline关键字修饰函数,函数调用会展开为函数体,一些接受lambda的函数常常被声明为inline的,这样可以避免创建和传递lambda对象,但是,当然,会导致编译出来的程序大一些。

函数引用

kotlin允许使用::来引用函数,被引用的函数可以被当成一个lambda一样使用,除了它们并不是匿名的以外和lambda没什么区别。
需要注意的是,对象上定义的方法其首个参数是一个隐式的this,因此,T.method()和method(a: T),虽然一个是对象上定义的方法,一个是全局方法,但它们的类型都是一样的,都是T.() -> ReturnType。也就是说,以下两个函数 (File.() -> Unit) 和 ((File) -> Unit) 它们的类型是相同的。
以下代码是完全合法的kotlin代码:
var a: (File.() -> Unit)?= null
var b: ((File) -> Unit)? = null
a = b
b = a

闭包

kotlin允许函数返回lambda,这种场景下必须要考虑如果lambda中使用了函数作用域内的变量会怎么样,正常情况下当函数返回这些变量就不存在了,但被lambda引用的变量会继续存活。

编译时Null检查

和Java不同,Kotlin中每个变量在声明时默认都不可为null,如果一个变量被声明为可能为null的,在使用前就必须进行检查,这项特性能有效避免Java中常见的NullPointerException。
要将一个变量声明为可能为null的,需要写出它的类型并在类型后面加一个’?’,比如:
var nullableString: String? = null
如果不写出类型就直接赋值为null,kotlin就会把类型推断为Nothing?,这是一个只能被赋值为null的类型。

null安全编程

kotlin对于可能为null的变量有一些限制,比如不能直接调用这些变量的方法,这是为了避免异常。
当然,kotlin提供了几种范式用于在限制下写出优雅的null安全代码:

  • safe call操作符: ‘?.’

    • 要调用一个nullable变量的方法,可以使用.?操作符,kotlin会检查变量,如果为null就不调用后面的方法,整个表达式值被当作null
      • kotlin还提供了一系列方便的函数式风格的函数,可以在任何对象上调用,配合.?操作符可以写出表达能力很强的代码,比如let允许传入一个单参数lambda,该函数会使用this作为参数调用lambda,并返回lambda的返回值:
        1. let result = question?.let {
        2. if (it.isNotBlank()) {
        3. answer(it)
        4. } else {
        5. "error"
        6. }
        7. }
  • ‘!!.’

    • 严格来说使用!!.已经不算null安全编程了,如果你希望编译器不要在编译时进行检查,而是在运行时发现变量为null时抛出异常(KotlinNullPointerException),那么就使用!!.吧,正常来说,你不应该使用这个操作符,不过在处理一些底层逻辑,比如和其他语言互操作时,可能无法避免使用!!.
  • 使用if检查是否为null,kotlin会识别检查语句,并在判断非null后允许程序员使用.操作符直接调用方法,这个特性被称作’smart casting’,在if分支中kotlin意识到已经做过判断了,会将变量类型当作非null处理
  • null coalescing操作符:’?:’
    • 当nullable变量是null时,表达式会求值为合并操作符后面的那个表达式的值,通过合并操作符,在java中可能需要写6行if-else语句的检查参数,若不为空则赋值给变量,为空则赋值默认值给变量的代码(声明 - if - 赋值 - else - 默认值 - if闭合 ),在kotlin中只要一行就搞定了
    • 也有人管它叫猫王操作符,问号和冒号在一起是不是很像猫王的标志性发型?

异常

  • 什么时候抛出异常?
    • 使用!!.运算符在nullable对象上调用方法可能导致空指针异常;
    • 调用一些方法时,方法实现也可能会抛出异常;
      • kotlin预定义的precondition校验方法:
        • 空值校验,抛出IllegalStateException:checkNotNull
        • 参数校验,抛出IllegalArgumentException:require, requireNotNull, error
        • 断言,抛出AssertionError:assert
    • 可以使用throw写出抛出异常的代码;
  • 异常类型
    • 常见的语言预定义异常:IllegalStateException、KotlinNullPointerException;
    • 继承已有异常的自定义异常;
    • kotlin中一切异常都是UncheckedException,kotlin设计者认为用CheckedException要求开发者妥善处理异常是不现实的,常常强迫开发者为了让代码成功编译而写出try-catch并忽略异常,前者至少不会阻止编译,并且不会因此被忽略;
  • 如何捕获异常?
    • 使用try-catch语法捕获异常,语义和Java的try-catch一致,catch中需要写明异常类型,当代码块抛出和类型匹配的异常时就会执行对应catch块内的代码;

字符串API

几乎在任何语言中,字符串类型都是最重要的类型,kotlin中也是如此,因此有必要熟悉相关API,此外一些kotlin语言的设计思想也可以从这些API中得到反映。
kotlin中String是不可变的,任何用于修改字符串的方法都不会影响原对象,而是会返回新的修改后的对象。

  • 字符串分割
    • substring - 接受一个IntRange并提取出范围内的子字符串
    • split - 接受一个分割字符串并返回分割后的字符串数组
  • 字符串转换
    • replace - replace有多种重载,最灵活的一个接受一个regex和一个lambda,如果regex匹配则会使用regex匹配上的字符串作为参数调用lambda,并用lambda返回值替代regex匹配上的子字符串;
  • 字符串比较
    • kotlin中对non-nullable对象使用==相当于Java中的equals
  • 字符串和数字之间转换
    • to - 字符串转数字
    • format - 数字转字符串,字符串格式和经典的printf一样
      1. val pi = 3.14159265358979323
      2. val s = "pi = %.2f".format(pi)

数组解构语法

kotlin允许将数组解构赋值给由括号包围的一组变量:
val (val1, val2) = arrayOf(“123”, “345”)

kotlin标准库工具方法

下表列出了kotlin的标准工具方法,每一列分别表示:

  • 函数名称
  • 是否将当前对象作为唯一参数传入lambda?
  • 是否将当前对象作为lambda的relative scoping(即任何在lambda中的方法调用都优先解释为在调用当前对象上的方法)?
  • 返回值

其中,应该尽可能避免使用with和takeUnless,前者不符合链式调用风格,后者可读性不如takeIf。
屏幕快照 2019-12-05 下午8.07.33.png

好奇的开发者不应该仅仅满足于知道上述各个函数的语意,还应该想要了解语言提供了什么机制来定义出这些不同的语意,为了查清这一点,就不得不去查找kotlin文档,找出各个函数的定义,观察它们的区别,因为kotlin文档并没有明确要如何定义一个接受lambda的函数才能让lambda用于默认的receiver对象。
下面是apply和also的定义:
image.png

集合类型 - List和Set

  • List
    • List是一个泛型类,类型为List
    • 使用listOf创建不可变List,使用mutableListOf创建可变List
    • 使用toList和toMutableList来根据当前List创建一个新的可变或者不可变的List对象
    • 使用数组下标操作符能够访问指定位置的元素,越界会导致抛出ArrayIndexOutOfBoundsException
      • 为兼容越界,可以使用getOrElse或者getOrNull方法访问元素
    • 由于实现了Iterable,可以使用for in语法或者forEach / forEachIndexed方法遍历List或Set
    • 可以使用数组解构语法将数组解构赋值给多个变量,单个下划线表示忽略某个元素:
      • val (a, b, _, d) = myArray
    • kotlin为List重载了+=和-=运算符,用于方便地修改List元素
  • Set和List类似,但由于Set是无序的,它的API数量相对List少一些,不再展开

var myList = listOf(“hello”, “world”)

集合类型 - Map

Map类型具体支持的API参考文档,和List一样提供了一些kotlin独有的语法,比如for-in以及+=, -=操作符,没有什么特殊的,只不过Map的entry是Pair,而Pair可以通过 to 关键字创建。

var myMap = mapOf(“Erie” to “Mary”, “Tom” to “Jerry”)

特殊集合类型 - IntArray

Kotlin语言最初的设计目标是运行于JVM之上,并且能够和Java相互操作,因此必须提供等同于Java的int[]的类型,这个类型就是IntArray。
类似的还有LongArray等,这些类型一般只有在需要和Java代码互调时才有用。Kotlin中,只要使用通用的list就好。

class定义

kotlin中,可以定义一个没有属性和方法的class:class Player;
通常,class有属性和方法,默认所有属性和方法都是public的:

  1. // name是public属性,直接在primary constructor中声明,gender也是,但声明位置不同
  2. class Player(var name: String, _gender: String) {
  3. // class body本身就是primary function
  4. var gender = _gender.toLowerCase()
  5. fun sayHi() {
  6. println("Hi!")
  7. }
  8. }

属性getter/setter

kotlin中定义一个var属性相当于定义了getAttr和setAttr方法,它们的默认实现直接访问attr;而val属性相当于只定义了getAttr方法。kotlin在访问属性时,总是优先访问属性的getter和setter方法,只有当类定义上只定义了attr而没有定义getter/setter才会去直接访问属性(当java和kotlin混编时,会出现这种类定义)。
可以重写getter和setter,在重写的方法body中可以使用field表示属性本身,如果不使用field,那么kotlin类定义中将只定义对应的getter和setter方法,而不会定义属性。也就是说,可以提供一个属性的getter方法,直接在getter方法中返回一个根据其他属性计算出来的值,或者是一个常量,这种情况下kotlin编译结果里不存在实际的属性字段定义。

  1. // 一个最一般的getter/setter语法示例:
  2. class Test {
  3. var name = "Jerry"
  4. get() = field.capitalize()
  5. set(value) {
  6. field = value.trim()
  7. }
  8. }

class primary constructor

class只能有一个primary constructor,并且这个constructor接受的参数必须写在类名后面,其中如果一个参数只是简单的被赋值给属性,那么应该把属性定义也写在primary constructor参数列表里,在class body内,所有需要执行的代码都是primary constructor方法的body,除了属性初始化语句外,还可以写init语句以包含任意的代码,属性初始化语句和init语句都会按在代码中出现的顺序执行。需要注意的是,在init语句块和在属性初始化语句中,任何没有以this引用的变量都会优先匹配到primary constructor参数。

  1. class Test(var name: String, gender: String) {
  2. var gender = gender.toLowerCase()
  3. init {
  4. println("init")
  5. }
  6. }

此外,由于kotlin支持了参数默认值语法,在kotlin中可以在定义了参数默认值的构造函数使用@JvmOverloads自动生成多个Java重载方法,在继承Android View类往往都得这么写。

方法可见性

方法定义可见性(visibility)的方式和Java类似,使用Java的方式指定visibility即可,不过kotlin新增了internal visibility,意义和Java的package并不一样,指的是所属module内可用。
那java的package作用域怎么办?java的package作用域的确是不怎么实用,所以kotlin不支持它。
primary constructor可以指定visibility,需要使用稍微有点特殊的语法:

  1. class Test private constructor(var name: String, gender: String) {
  2. var gender = gender.toLowerCase()
  3. }

class secondary constructor

除了primary constructor,class也可以指定secondary constructor,必须使用constructor关键字定义secondary constructor,secondary constructor可以执行一些额外的代码,但一定要首先调用primary constructor:

  1. class Test private constructor(var name: String, gender: String) {
  2. var gender = gender.toLowerCase()
  3. init {
  4. println(gender) // 这里引用的是参数的gender
  5. }
  6. constructor(): this("hello", "world") {
  7. println("from secondary")
  8. gender = "1234" // 这里引用的是this.gender
  9. }
  10. }
  11. // 打印结果是:
  12. // world
  13. // from secondary

lateinit关键字

可以使用lateinit关键字来在kotlin的null类型安全系统上开一个口子:lateinit标记的nonnull属性不需要在对象初始化时定义。使用这个关键字相当于和kotlin保证:在使用这个属性前,它一定会初始化,我很确定,而且因为nullable变量涉及null检查,我不想使用nullable变量。

by关键字

可以使用by关键字实现属性的delegate,常用来实现lazy initialization,这种属性必须是val:val lazyval by lazy { hardwork() }
其中,lazy是一个方法,返回一个定义了getValue()方法的对象,当lazyval首次被使用时,就会调用getValue方法来获取属性值,由于属性必须被定义为val,因此属性是不可变的,只需要获取一次并记录下被获取的值即可。
这种写法被kotlin定义为“委托属性”,https://www.kotlincn.net/docs/reference/delegated-properties.html

继承

kotlin中使用冒号定义继承,默认不论是类还是属性、方法,都被定义为final,因此被继承的类必须定义为open的,在定义继承时必须同时说明父类的primary constructor调用方式:

  1. open class Room(val name: String)
  2. class TownSquare : Room("Town Square")

子类可以重写父类的属性或者方法,重写时需要使用override关键字。

类型检查

kotlin中使用is关键字做类型检查:room is Room

类型转换

kotlin中使用as关键字进行类型转换,但通常不直接使用这个关键字,而是通过smart casting特性,首先做类型检查然后再在if body中做特定类型相关的事。

kotlin类型体系

kotlin中,Any是一切类型的父类,在Any上定义了equals、hashCode方法,当kotlin的target是JVM时,any对应java.lang.Object。

object关键字

object关键字在kotlin中用法比较灵活:

  • 定义单例

object Game {

}

  • 定义匿名对象

var listener = object : OnClickListener {

}

  • 定义Companion对象,实现java的static语义

class Helper {
companion object {

}
}

object的用法比较灵活,只要是需要一个对象而又不需要为它定义一个具体类型的地方,就需要使用object,此外由于kotlin中没有类静态属性的概念,因此还使用companion object来提供类似java的static成员的能力,具体参考文档:
https://www.kotlincn.net/docs/reference/object-declarations.html

内嵌类型和inner class

定义在一个class内部的class是内嵌类型。内嵌类型和java的static inner class一样,但定义时不需要static关键字了。
kotlin中的内嵌类默认不会持有外部类实例引用,不可以访问外部类实例属性和方法,内嵌类默认对应java中的static inner class。

java风格的inner class

要想使用java的内部类,需要显式使用inner class关键字,这样的类只能由outer class实例创建,会持有对outer class实例的引用,并且可以访问任意outer class实例的属性和方法。
一个使用object关键字创建的匿名内部类是一个匿名的inner class实例。

data class

使用data关键字可以定义data class,data class是按照纯数据类的方式实现了equals、hashCode、toString方法的类。
此外,data class还额外实现了一个copy方法。
最后,data class支持解构赋值,它实现了operator fun component()系列方法,允许按照属性定义顺序做解构赋值。
data class的定义并没有什么特别的,仅仅是提供了以上便利而已,类型该怎么定义还怎么定义。
data class Coordinate(val x: Int, val y: Int) {
val isInBound = x >= 0 && y >= 0
}

枚举类

枚举类和Java类似。
枚举类可以定义自己的属性和constructor,此时每一个枚举值都是一个对象,Java中有人利用枚举的这个特点实现单例,但是这么做并没有什么特别的好处,大多数时候只会让阅读代码的人搞不懂代码的意图。
enum class Direction(private val coordinate: Coordinate) {
North(Coordinate(0, -1)),
East(Coordinate(1, 0));

fun updateCoordinate(currCoord: Coordinate) = Coordinate(currCoord.x + this.coordinate.x,
currCoord.y + this.coordinate.y)
}

操作符重载

使用operator关键字配合特定函数名称,可以定义操作符重载:
operator fun plus(other: Coordinate) = Coordinate(x + other.x, y + other.y)
kotlin允许重载的常用操作符有:
屏幕快照 2019-12-12 上午12.29.43.png

sealed class

sealed表示密封,它有两层含义:
sealed class的所有子类必须在源码编译时就确定,一但完成编译,依赖编译产物的其他源码就不可以编写任何继承自编译好的sealed class的子类了。
一个sealed class是比enum更加表示一组值的方式,可以表示一组相互之间平行的类,当使用when进行判断时,kotlin会针对enum和sealed这样提供了明确定义范围的类给出更近一步的提示:

  1. sealed class StudentStatus {
  2. object NotEnrolled: StudentStatus()
  3. class Active(val courseId: String): StudentStatus()
  4. object Graduated: StudentStatus()
  5. }
  6. // 在使用when对一个StudentStatus类做类型判断时,kotlin会检查是否针对每个子类型都做了检查,
  7. // 并允许省略else
  8. fun eval(status: StudentStatus): Double = when(status) {
  9. is Active -> 1.0
  10. NotEnrolled -> 2.0
  11. Graduated -> 3.0
  12. // 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
  13. }

接口与抽象类

kotlin中的interface和java类似,概念上是定义接口而不是实现,实际上允许指定默认实现;由于kotlin的属性并不是真正的属性,而是一组getter/setter,因此kotlin的接口中允许定义属性,实现必须override这些属性。接口及其属性、方法默认都是open的。
Java中,原本抽象类是实现和接口的混合,但自从接口有了默认实现的能力,抽象类和接口的关系就有点不明不白了。不过,二者还是有一个根本性的差别的:一个类可以实现多个接口,但只能继承自一个类。当多个接口对同名方法提供了默认实现时,必须由实现类重写。

泛型基础

  • 泛型方法:fun fetch(): T {…}
  • 泛型类:class LootBox(item: T) {…}
  • 泛型类型约束:约束泛型类型T必须是Loot或者Loot的子类

此外,kotlin实行和java类似的类型擦除:https://www.kotlincn.net/docs/reference/generics.html

可变参数

使用vararg关键字可以定义可变参数,可变参数调用时的语法非常灵活,在函数body内可变参数被转换成Array

泛型类型 - in/out(协变/逆变)

首先明确in和out的含义:

  • in指的是被标记的泛型对象作为被消费的对象
    • 典型的例子是:Comparable {compare(T param): int},在这种场景下,一个接收number作为参数的Comparator能够被安全的赋值给一个接收double的comparator,但反过来不行;
  • out则和java的extends限定类似,指的是对象只会被读取
    • 典型的例子就是各个集合类型,集合中,泛型对象并不是要传入什么方法进行处理的对象,外部传入泛型对象的唯一目的就是随后可以取回它们,因此需要确保集合保管的泛型对象符合泛型规定的类型;

in和out涉及到两个泛型对象能否相互赋值。

reified关键字

使用reified关键字配合inline关键字使用,可以避免泛型的类型擦出,从而在泛型方法中像使用一个普通的类型一样使用泛型类型。

Extension function

https://kotlinlang.org/docs/reference/extensions.html
kotlin允许为任何一个类定义extension function,即扩展方法。扩展方法其实就是Java中的Helper方法的语法糖,在java中常常会围绕一个类提供一系列helper方法,调用时将类实例传入helper方法。这么做的原因可能是没有办法修改类源码,也有可能是类本身已经足够复杂,这些helper方法不适合加入到类源码中。kotlin允许将这种helper方法定义在类的源码之外,同时还能够像调用实例方法一样调用这样的方法:
fun String.addEnthusiasm(amount: Int = 1) = this + “!”.repeat(amount)
在定义通用扩展方法时,需要使用泛型的写法,例如下面的例子:对Any的所有子类提供一个easyPrint方法,该方法打印当前对象,并返回当前对象实例以支持链式调用:
fun T.easyPrint(): T {
println(this)
return this
}
扩展方法也可以扩展没有field的属性;
当扩展的receiver是nullable的时,this的类型就是nullable的,需要做null安全处理。

扩展方法常常被单独定义在一个文件里,要使用扩展方法,需要从该文件里引入扩展方法,可以使用as关键字修改引入的扩展方法的名字。

kotlin标准库通过扩展方法提供了很多便捷能力,例如let、apply就是扩展方法。其中,apply这个扩展方法接受一个lambda,lambda body内的任何代码都优先把apply方法的receiver作为receiver,这是通过一个特殊的语法实现的,注意下面的T.() -> Unit这种写法,在扩展方法定义中,将一个通常的() -> Unit函数签名前面加上类型限定,即可获得这种特殊的能力:
public inline fun T.apply(block: T.() -> Unit): T {block(); return this}

此外,扩展方法中,默认receiver就是被扩展的类的当前实例。一些库中的代码,有的地方显式地写了this,有的又没有些,不要被误导,事实上总是可以不写this的。

反射

kotlin中,使用双冒号来获取一个对象/类的元数据,包括方法引用、属性引用、类型字面量,双冒号专门用于反射。和Java相比,kotlin使用专门的双冒号’::’来做反射相关操作,相比Java用点号通过方法做反射操作的方式,会更加好理解,毕竟反射这件事的确很特殊。
使用::class访问kotlin类型的类型字面量 - 一个KClass类型对象。为了获得Java的Class,需要使用::class.java。
另外,javaClass是一个对象属性,通过object.javaClass访问,如果你试图通过::javaClass获得java class,你会发现你获得了一个属性引用。
应该使用::class.java,还是.javaClass?答案是应该使用::class.java,原因在这里:https://stackoverflow.com/questions/46674787/instanceclass-java-vs-instance-javaclass
获得了属性引用或者方法引用后,要根据对象访问属性或者调用方法,只需要将对象作为额外的第一个参数调用属性引用或者方法引用即可,其他参数正常写在this后面。

  1. var x = "hello"
  2. class Test {
  3. var t = "test"
  4. companion object {
  5. var s = "companion"
  6. }
  7. }
  8. fun main(args: Array<String>) {
  9. // 非类实例属性
  10. println(::x.get())
  11. ::x.set("world")
  12. println(x)
  13. // companion属性和非类实例属性类似
  14. println(Test.Companion::s.get())
  15. Test.Companion::s.set("changed")
  16. println(Test.Companion.s)
  17. // 类实例属性
  18. val t = Test()
  19. println(Test::t.get(t))
  20. Test::t.set(t, "test__")
  21. println(t.t)
  22. }
  23. // 输出:
  24. hello
  25. world
  26. companion
  27. changed
  28. test
  29. test__

Bound Reference

可以使用反射获得一个绑定了this对象的方法引用,这样的方法引用在调用时不需要指定用哪个对象作为this:
val isNumber = “\d+”.toRegex()::matches
println(isNumber(“123”))

使用bound reference时,记住reference会导致被引用的对象不被释放,如果在对象应该被释放的时候,还有其他地方持有着对象的reference引用,会导致对象内存不被释放,存在内存泄漏隐患。

kotlin和Java互调

Kotlin可以和Java做interoperate。

  • kotlin源码和java源码可以混合编译,kotlin class和java class是可以相互调用的;
  • kotlin允许在文件中直接定义方法和属性,默认这些方法和属性被编译成.class文件后会存在于 ‘Kt’类中,可以在kotlin文件第一行使用@file:JvmName(““)指定类名;
  • Java方法如果返回值可能为空,需要使用@Nullable注解标记,不然kotlin会将方法返回类型识别为non-null类型;
  • kotlin中的Int映射为Java中的int,kotlin中的Int?映射为Java中的Integer;
  • kotlin中访问java对象属性时,如果java对象只定义了属性的getter和setter,那么kotlin中对属性的访问会被翻译成对getter和setter的调用;如果java对象将属性定义成了public的,那么kotlin中对属性的访问不论java对象是否提供了属性的getter/setter都是直接对java对象属性的访问;
  • Java语言没有方法的默认参数,也没有办法使用named parameter调用方法,这是语言的硬限制,kotlin无能为力,尽管如此,kotlin还是提供了@JvmOverloads注解来让Java调用kotlin时可以省略指定了默认值的后面的参数,部分提供了默认参数的能力给Java,但named parameter特性在Java中是无论如何不可用的;
  • kotlin默认为每个属性提供了getter和setter,Java中也只能通过属性的getter/setter访问属性,可以使用@JvmField注解让kotlin不为属性提供getter和setter,此时从Java可以直接访问属性,并且kotlin也禁止为这样的属性提供getter/setter;当@JvmField用在Companion中的属性上时,作用是让kotlin将属性编译成class的static属性,而不是Companion类的static属性;
  • kotlin中可以通过类直接访问Companion中的属性,但Java中需要通过Companion对象才能访问到,因为kotlin中的Companion提供了java的static语义,因此可以让kotlin将Companion中定义的内容直接编译成static的,使用@JvmStatic标记Companion中的方法,可以让kotlin在编译后将这些方法定义在class下;
  • kotlin中将所有异常都视作unchecked exception,如果需要向java提供API,需要将可能抛出的checked exception通过注解@Throws声明,如@Throws(IOException::class),这样生成的java字节码方法才会有异常声明;
  • kotlin中的函数类型对应到java中是一个定义了对应方法的接口,这点和java8一致;

协程

  • launch
  • async
  • await

kotlin的协程和JS为代表的其他基于Future概念,并提供async await的语言不太一样,并且kotlin的tutorial也没说明白协程背后的工作方式。

下面这两篇,一个是kotlin设计文档的翻译(翻译的不太好),一个是参与翻译的人写的博客(看起来比翻译舒服多了),还是参考博客就好,理解kotlin协程和其他语言不同的地方。
https://github.com/Kotlin-zh/KEEP/blob/master/proposals/coroutines.md
https://www.jianshu.com/p/d23c688feae7