人生苦多,快来 Kotlin ,快速学习Kotlin!
什么是Kotlin?
Kotlin 是种静态类型编程语言 用于现代多平台应用 100%可与Java™和Android™互操作,它是[JetBrains]开发的基于JVM的语言
开发IDE : Intellij / AndroidStudio3.0 preview
参考: Kotlin 官网 / Kotlin 语言中文站
Example
Github KotlinDemo
源文件与包
包
Kotlin 源文件以 kt 结尾. 源文件所有内容(无论是类还是函数)都包含在声明的包内.
NOTE: 源文件通常以包声明开头, 没有指明包,则该文件的内容属于无名字的默认包(属于root package)。
1 | package demo |
NOTE: 若声明的包路径与文件路径不一致,亦可以正常编译. 不过会有如Java 一样的警告 Package directive doesn’t match file location
默认导入
Kotlin 如同Java 一样 默认源文件会导入以下包:
1 | kotlin.* |
import
1 | import (used by preamble) |
Java vs Kotlin
1.如果出现名字冲突,Kotlin 可以使用 as 关键字在本地重命名冲突项来消歧义
2.Kotlin 的关键字 import 不仅仅限于导入类,还可以导入顶层函数及属性,在对象声明中声明的函数和属性,枚举常量等.
NOTE: 与 Java 不同,Kotlin 没有单独的 import static
语法; 所有这些声明都用 import 关键字导入。
顶层声明
1.同 package 下的Kotlin 源文件,在顶层所声明的常量,变量以及函数等不允许重复定义,否则报Conflicting 错误。
2.若声明用 private 可见性修饰符修饰时,属于当前文件私有。
基本数据类型
Numbers
Kotlin 一切都是对象。Kotlin提供以下代表数字、字符的内置类型(这很接近Java)
Type | Bit width | 包装器类型 |
---|---|---|
Double | 64 | Double |
Float | 32 | Float |
Long | 64 | Long |
Int | 32 | Integer |
Short | 16 | Short |
Byte | 8 | Byte |
Char | 16 (Unicode character) | Character |
NOTE:仅 Char 不是Kotlin的数字。如下
1 | val c:Char='c' |
字面常量
Kotlin 唯独不支持八进制
- 十进制:
123
- 二进制:
0b00001011
- 十六进制:
0x0F
Kotlin 数值表示方法
- 默认 Double:
123.5
、123.5e10
- Float 用
f
或者F
标记:123.5f
- Long 用大写
L
标记:123L
NOTE:支持数字字面值中的下划线(自 kotlin1.1 起)
1 | val oneMillion = 1_000_000 |
Kotlin 装箱机制
Kotlin 内置类型在 Java 平台上是存储为 JVM 的原生类型,但一个可空的引用(如 Int?
)或泛型情况下(如 Array<Int>,List<Int> ...
) 会把数字和字符自动装箱成相应包装类, 请参考 Numbers。
1 | val low = -127 |
==
和 ===
请参考 类型相等性 。
1 | val anIntegerA: Int? = 123 //对应 java.lang.Integer 一个装箱的 Int |
NOTE:一个可空的引用(如 Int?
)能不能装换成 Int
,答案是肯定的。强制转换或者 !!
1 | val anIntegerA: Int? = 123 |
显式转换
每个数字类型支持如下的转换:
toByte(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
toChar(): Char
如下示例 Java 中,包装类不能隐式转换, Kotlin 也是如此, 不同类型之间是不能相互隐式转换的。
1 | Byte a = 1; |
1 | val b: Byte = 1 // OK, 字面值是静态检测的 |
运算
Kotlin支持数字运算的标准集,运算被定义为相应的类成员(但编译器会将函数调用优化为相应的指令)。 参见操作符重载。
对于位运算,没有特殊字符来表示,而只可用中缀方式调用命名函数,例如:
1 | val x = (1 shl 2) and 0x000FF000 |
这是完整的位运算列表(只用于 Int
和 Long
):
- shl(bits) – 有符号左移 (Java 的 <<)
- shr(bits) – 有符号右移 (Java 的 >>)
- ushr(bits) – 无符号右移 (Java 的 >>>)
- and(bits) – 位与
- or(bits) – 位或
- xor(bits) – 位异或
- inv() – 位非
字符串
字符串用 String
类型表示。字符串是不可变的。 字符串的元素——字符可以使用索引运算符访问: s[i]
。 可以用 for 循环迭代字符串:
1 | val s = "Hello, world!\n" //字符串字面值 |
NOTE: 模板表达式以美元符($
)开头,若要对象属性时要花括号括起来,若要表示字面值 $
字符z则:
1 | val price = "${'$'}9.99" |
数组
数组在 Kotlin 中使用 Array
类来表示,它定义了 get
和 set
函数(按照运算符重载约定这会转变为 []
)和 size
属性,以及一些其他有用的成员函数:
1 | class Array<T> private constructor() { |
Library.kt
中 arrayOf()
arrayOfNulls
函数以及Array
构造函数能创建数数组:
1 | val args: Array<Int> = arrayOf(1, 2, 3) |
NOTE: 与 Java 不同的是,Kotlin 中数组是不型变的(invariant)。这意味着 Kotlin 不让我们把 Array<String>
赋值给 Array<Any>
,以防止可能的运行时失败(但是你可以使用 Array<out Any>
, 参见类型投影)。
之前所说的在泛型情况下Kotlin 会把数字和字符自动装箱成相应包装类, Arrays.kt
中有以下
ByteArray
CharArray
ShortArray
IntArray
LongArray
FloatArray
DoubleArray
BooleanArray
无装箱开销的专门的类来表示原生类型数组, 和 Array
并没有继承关系,但是它们有同样的方法属性集。它们也都有相应的工厂方法。
1 | val x: IntArray = intArrayOf(1, 2, 3) |
数组迭代通过 iterator()
函数返回 Iterator<T>
对象进行迭代:
1 | val iterator = args.iterator() |
NOTE: forEach
forEachIndexed
这些是Array 的扩展函数, 背后实现也是 [for 循环 ](#For 循环)
区间
区间表达式由具有操作符形式 ..
的 rangeTo
和 downTo
函数辅以 in
和 !in
形成。 区间是为任何可比较类型(Comparable<in T>
)定义的,但对于整型原生类型(Int ,Long,Char),Ranges.kt
中实现了常用的整型区间(IntRange
、 LongRange
、 CharRange
),而在 Primitives.kt
中的 Int ,Long,Char
类实现了rangeTo
函数。以下是使用区间的一些示例
1 | println((1.rangeTo(3)).contains(1)) //使用区间rangeTo函数 |
..
创建一个区间, 实际是调用 rangeTo
函数返回原生类型 *Range
对象, in
则调用 contains
函数。in *Range
还可以用在迭代(for-循环)中。
1 | for (index in 1..4) print(index) |
NOTE:rangeTo
创建的区间, 范围值是小到大, downTo
反之。他们默认 step 分别为1,-1
1 | // val intRange = 1..4 //step 1 default |
背后实现原理
区间实现了该库中的一个公共接口:ClosedRange<T>
。
ClosedRange<T>
在数学意义上表示一个闭区间,它是为可比较类型定义的。 它有两个端点:start
和 endInclusive
他们都包含在区间内。 其主要操作是 contains
,通常以 in/!in 操作符形式使用。
整型数列(IntProgression
、 LongProgression
、 CharProgression
)表示等差数列。 数列由 first
元素、last
元素和非零的 step
定义。 第一个元素是 first
,后续元素是前一个元素加上 step
。 last
元素总会被迭代命中,除非该数列是空的。
数列是 Iterable<N>
的子类型,其中 N
分别为 Int
、 Long
或者 Char
,所以它可用于 for-循环以及像 map
、filter
等函数中。 对 Progression
迭代相当于 Java/JavaScript 的基于索引的 for-循环:
1 | for (int i = first; i != last; i += step) { |
对于整型类型,..
操作符创建一个同时实现 ClosedRange<T>
和 *Progression
的对象。 例如,IntRange
实现了 ClosedRange<Int>
并扩展自 IntProgression
,因此为 IntProgression
定义的所有操作也可用于 IntRange
。 downTo()
和 step()
函数的结果总是一个 *Progression
。
数列由在其伴生对象中定义的 fromClosedRange
函数构造:
1 | IntProgression.fromClosedRange(start, end, step) |
数列的 last
元素这样计算:对于正的 step
找到不大于 end
值的最大值、或者对于负的 step
找到不小于 end
值的最小值,使得 (last - first) % increment == 0
。
一些实用函数
- rangeTo()
- downTo()
- reversed()
- step()
程序结构
变量
分别使用 var ,val 声明可变和不可变的变量.例子如下
1 | val s = "Example" // A Immutable String |
声明可变变量语法
1 | var <propertyName>[: <PropertyType>] [= <property_initializer>] |
声明不可变变量(仅赋值一次只读变量)语法
1 | var <propertyName>[: <PropertyType>] = <property_initializer> |
默认 Kotlin 变量类型是能通过赋值时智能推断该变量的类型,且该var
变量只能该类型的的值。显式确定变量类型,必须要接收该类型的初始化。通过一个简单例子说明
1 | val aImmutableIntVariable = 0x001 //aImmutableIntVariable 类型为 Int |
NOTE: var
变量直接赋值为 null
,该变量则不符合预期的类型 简单来说(Nothing),再次赋值时报错。
1 | var aNullable = null |
更详细的类型介绍:类型安全和智能转换
常量 (编译期)
已知值的属性可以使用 const 修饰符标记为 编译期常量.必须满足以下需求
- 位于顶层或者是 object 的一个成员
- String 或原生类型 值初始化
- 没有自定义 getter
1 | const val CONST_VAL = 1 |
幕后字段
Kotlin 中类不能有字段。然而,当使用自定义访问器时,有时有一个幕后字段(backing field)有时是必要的。为此 Kotlin 提供一个自动幕后字段,它可通过使用 field
标识符访问。
1 | var counter = 0 // 此初始器值直接写入到幕后字段 |
field
标识符只能用在属性的访问器内。
如果属性至少一个访问器使用默认实现,或者自定义访问器通过 field
引用幕后字段,将会为该属性生成一个幕后字段。
例如,下面的情况下, 就没有幕后字段:
1 | val isEmpty: Boolean |
如果你的需求不符合这套“隐式的幕后字段”方案,那么总可以使用 幕后属性(backing property):
1 | private var _table: Map<String, Int>? = null |
从各方面看,这正是与 Java 相同的方式。因为通过默认 getter 和 setter 访问私有属性会被优化,所以不会引入函数调用开销。
控制流:if、when、for、while
if 语句、if - else 表达式
在 Kotlin 中,没有Java中三元运算符(条件 ? 然后 : 否则), 因为if - else 是一个表达式,即它会返回一个值。
1 | val num1 = 1 |
if的分支可以是代码块,最后的表达式作为该块的值:
1 | println( |
When 表达式
在 Kotlin 中,when
取代了Java中 switch
。声明语法如下:
1 | when[(表达式)]{ |
SEMI 代表 ;或者 换行
, 在controlStructureBody 是代码块且有变量声明时使用, 示例:
1 | when { |
NOTE: when
作为一个表达式使用,则必须有 else
分支, 除非所有的可能情况都已经覆盖了。
1 | val an = when (1) { |
For 循环
for
循环可以对任何提供迭代器(iterator)的对象进行遍历。语法
1 | for (item in collection) print(item) |
示例
1 | val array = arrayOf(1, 2, 3) |
如上所述,for 可以循环遍历任何提供了迭代器的对象。即:
- 有一个成员函数或者扩展函数
iterator()
,它的返回类型Iterator<T>
- 有一个成员函数或者扩展函数
next()
- 有一个成员函数或者扩展函数
hasNext()
返回Boolean
。
这三个函数都需要标记为 operator
。
While 循环
while
和 do..while
照常使用。小白应该也可以搞掂吧。。。
循环中的Break和continue
在循环中 Kotlin 支持传统的 break
和 continue
操作符。
返回和跳转
Kotlin 有三种结构化跳转表达式:
- return。默认从最直接包围它的函数或者匿名函数返回。
- break。终止最直接包围它的循环。
- continue。继续下一次最直接包围它的循环。
标签
在 Kotlin 中任何表达式都可以用标签(label)来标记。 标签的格式为标识符后跟 @
符号,例如:abc@
、fooBar@
都是有效的标签(参见语法)。 要为一个表达式加标签,我们只要在其前加标签即可。
Break 和 Continue 的标签控制跳转, return 标签控制返回目标,示例:
1 | out@ for (i in 1..3) { |
面向对象
类与对象
声明类(class)语法
1 | [访问修饰符 默认public] [非访问修饰符 默认 final] class 类名 |
定义类,我们通过下面的例子来说明:
1 | class EmptyClass |
NOTE: [] 代表可以省略. Kotliin 中修饰符 与Java 略不同,Java语言提供了很多修饰符,主要分为以下两类:
- 访问修饰符
- 非访问修饰符
更详细的 Java 修饰符 请参考
Java 修饰符 _ 菜鸟教程
Kotliin 中没显式声明修饰符 ,默认可见性是 public
。
访问控制修饰符
类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。 (getter 总是与属性有着相同的可见性。) 在 Kotlin 中有这四个可见性修饰符:private
、 protected
、 internal
和 public
。
修饰符 | 是否支持顶级声明 | 当前文件 | 同一模块 |
---|---|---|---|
private | Y | Y | N |
protected | N | ||
internal | Y | Y | Y |
public | Y | Y | Y |
NOTE:
protected
不支持顶级声明,因为文件没有继承关系。internal
是编译在一起的一套 Kotlin 文件:- 一个 IntelliJ IDEA 模块;
- 一个 Maven 项目;
- 一个 Gradle 源集;
- 一次 <kotlinc> Ant 任务执行所编译的一套文件。
对于类和接口内部声明的成员可见修饰符与Java 类似:
private
仅该类和接口内部可见;protectd
该类和接口内部可见且子类可见internal
该模块内 可见public
都可见
非访问控制修饰符
kotlin 定义类、对象、构造函数、方法、属性时默认加了 final
修饰符, 接口默认是 open
与之相反。能被继承、被覆盖。
NOTE:在 final 修饰 class 下 用 open 修饰该类的成员无效,在 final 缺省修饰符下 再用 final 修饰显得 Redundant
冗余,但在 override
时可使用final
关键字再度修饰
我们通过下面的例子来说明:
1 |
|
NOTE:局部变量、函数和类不能有可见性修饰符。Kotlin 中外部类不能访问内部类的 private 成员(与Java不同)。
类成员
类可以包含
构造函数
一个类可以有一个主构造函数和一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(和访问修饰符 [默认 public])后。主构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面。非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。
NOTE:若要修改主构造函数的可见性,需要添加一个显式 constructor 关键字
1 | class A private constructor() { …… } |
Kotlin 十分简便, 可以在主构造函数内声明属性(可变的(var)或只读的(val))以及初始化属性默认值(次构造函数是不允许的), 且为该类成员属性, 主构造函数内不能包含除了声明属性任何的代码。提供了 init 关键字作为前缀的初始化块(initializer blocks)。
次构造函数
声明在类体内以 constructor
关键字的函数。若该类有主构造函数,次构造函数都需要用 this
关键字直接或间接委托给主构造函数。
1 | open class Person /*private*/ constructor(firstName: String) { |
1 | 注意:在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成 一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。 |
创建类的实例
Kotlin 并不需要 new 关键字创建实例, 像普通函数一样调用构造函数即可。
继承
Java 的超类是 Object
, 而 Kotlin 的是 Any。
若父类有主构造函数且带参数,子类必须用主构造函数将参数初始化,如下:
1 | class Student(firstName: String) : Person(firstName) { |
注意:参数初始化时,子父类必须一致。
父类没有主构造函数, 那么每个次构造函数必须使用 super 关键字初始化其基类型。
1 | open class Human { |
覆盖(override)
final
, open
是否可覆盖修饰符 和 override
标注覆盖类、对象、接口、构造函数、方法、属性。
覆盖规则
在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)来消除歧义。 为了表示采用 从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super ,如 super
1 | open class Thread { |
抽象类
类和其中的某些成员可以声明为 abstract 。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数——因为这不言而喻。
1 | abstract class AbstractClass{ //open 多余的,因为抽象类终究是父类,所以更不能用final 修饰 |
接口
用关键字 interface 来定义接口。Kotlin 的接口函数可以有实现, 属性必须是抽象的(默认抽象), 或者提供 get
访问器实现, 且不能有幕后字段(backing field)。
1 | fun main(args: Array<String>) { |
嵌套类和内部类
类可以嵌套在其他类中
1 | fun main(args: Array<String>) { |
如果对象是函数式 Java 接口(即具有单个抽象方法的 Java 接口)的实例, 你可以使用带接口类型前缀的lambda表达式创建它:
1 | val run = Runnable { } |
对象(object)
在Java 中, 匿名内部类随处可见。然而 Kotlin 用 object
关键字提供了对象声明以及对象表达式特性, 创建单例、匿名对象, 伴生对象(类内部的对象声明) so easy。
1 | val point = object /*: Any()*/ { //默认继承 Any |
NOTE: 如何区分对象声明和对象表达式, 顾名思义, 有名字的是对象声明(object Singleton), 没名字的是对象表达式(anonymous object)。
关于 object
使用细节,下面通过一个简单例子为大家演示:
1 | class KotlinObject { |
私有函数时,返回object
类型是匿名对象类型, 否则是 Any
。与Java 不同内部类也可访问非 final 变量。对象声明实则是单例。
伴生对象(companion object)
与 Java 或 C# 不同,在 Kotlin 中类没有静态方法。在大多数情况下,它建议简单地使用包级函数。
类内部的对象声明可以用 companion
关键字标记:
1 | open class World { |
Java 中调用
1 | public class StaticTest { |
NOTE:伴生对象实际是对象的实例成员, JVM 平台,如果使用 @JvmStatic
注解,你可以将伴生对象的成员生成为真正的静态方法和字段。更详细信息请参见Java 互操作性一节 。
对象表达式和对象声明之间有一个重要的语义差别:
- 对象表达式是在使用他们的地方立即执行(及初始化)的
- 对象声明是在第一次被访问到时延迟初始化的
- 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配
数据类
我们经常创建一些只保存数据的类。在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data
:
1 | fun main(args: Array<String>) { |
编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()
/hashCode()
对,toString()
格式是"User(name=John, age=42)"
,componentN()
函数 按声明顺序对应于所有属性,copy()
函数, 复制一个对象仅改变某些属性。
为了确保生成的代码的一致性和有意义的行为,数据类必须满足以下要求:
- 主构造函数需要至少有一个参数;
- 主构造函数的所有参数需要标记为
val
或var
; - 数据类不能是抽象、开放、密封或者内部的;
- (在1.1之前)数据类只能实现接口。
自 1.1 起,数据类可以扩展其他类(示例请参见密封类)。
在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。 (参见构造函数)。
密封类
密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
NOTE: sealed
不能修饰 interface ,abstract class(会报 warning,但是不会出现编译错误)
1 | fun main(args: Array<String>) { |
枚举类
枚举类的最基本的用法是实现类型安全的枚举, 每个枚举常量都是一个对象, 需用逗号分开。示例如下
1 | fun main(args: Array<String>) { |
枚举常量还实现了 Comparable 接口, 其中自然顺序是它们在枚举类中定义的顺序。
NOTE: val (name, ordinal) = KotlinEnumClass.Direction.EAST
之所以可以编译通过, 因为我对枚举类进行解构声明
1 | //学而致用 |
注解类
学习Java 的应该对注解不陌生,不了解可以先看看 Java的注解。
注解声明
1 | [访问修饰符 默认public] [非访问修饰符 默认只能为 final 不能显式修饰] annotation class 类名 |
1 | internal annotation class KotlinFileName(val name:String) |
允许的参数类型有:
- 对应于 Java 原生类型的类型(Int、 Long等)以及字符串
- 类
KClass
、枚举 - 其他注解
- 上面已列类型的数组
NOTE: 注解参数不能有可空类型,因为 JVM 不支持将 null
作为注解属性的值存储。如果注解用作另一个注解的参数,则其名称不以 @ 字符为前缀, 且新的注解类访问权限不能比其中一个注解的参数的访问权限要大
1 | internal annotation class FileScope |
注解的附加属性可以通过用元注解标注注解类来指定:
@Target
指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等);@Retention
指定该注解是否存储在编译后的 class 文件中,以及它在运行时能否通过反射可见 (默认都是 true);@Repeatable
允许在单个元素上多次使用相同的该注解;@MustBeDocumented
指定该注解是公有 API 的一部分,并且应该包含在生成的 API 文档中显示的类或方法的签名中。
1 |
|
Lambda 表达式
注解也可以用于 lambda 表达式。它们会被应用于生成 lambda 表达式体的 invoke()
方法上。
1 | annotation class Anonymous |
Use-site Targets (使用处 目标)
当对属性或主构造函数参数进行标注时,从相应的 Kotlin 元素生成的 Java 元素会有多个,因此在生成的 Java 字节码中该注解有多个可能位置 。支持的使用处目标的完整列表为:
file
property
(具有此目标的注解对 Java 不可见)field
get
(属性 getter)set
(属性 setter)receiver
(扩展函数或属性的接收者参数)param
(构造函数参数)setparam
(属性 setter 参数)delegate
(为委托属性存储其委托实例的字段)
可以使用相同的语法来注释整个文件。要执行此操作,请将目标文件的注释放在文件的顶层,在包指令之前或在所有导入之前,如果文件位于默认包中:
1 | "KotlinAnnotationKt") ( |
如果要指定精确地指定应该如何生成该注解,请使用以下语法:
1 | @处目标元素:[注解A 注解B ] ... //同一目标只有1个注解时方括号可以省略 |
简单示例如下:
1 | class User( val name: String, [ApplicationScope FunScope] val age: Int) |
如果不指定使用处目标,则根据正在使用的注解的 @Target
注解来选择目标 。
Java 注解
Java 注解与 Kotlin 100% 兼容:
kotlin
1 | //声明注解 |
java
1 | .ApplicationScope |
泛型
与 Java 类似,Kotlin 中的泛型,如下示例:
1 | fun main(args: Array<String>) { |
与 Java 不同,Kotlin 中的泛型没有通配符类型,它有:声明处型变(declaration-site variance)与类型投影(type projections)。
型变
Java 中的泛型是不型变的,这意味着 List<String>
并不是List<Object>
的子类型。
1 | List<String> strs = new ArrayList<String>(); |
PECS原则,在Java <? extends T>、<? super T>
通配符类型参数,前者只能读取, 不能写入,后者反之。便有一条规律,”Producer Extends, Consumer Super”:
Producer Extends
– 如果你需要一个只读List,用它来produce T,那么使用? extends T
。Consumer Super
– 如果你需要一个只写List,用它来consume T,那么使用? super T
。- 如果需要同时读取以及写入,那么我们就不能使用通配符了。
同样PECS原则适用于 Kotlin:
Producer Extends
– 使得类型是协变的(covariant)。Consumer Super
– 使得类型是逆变性(contravariance)。
NOTE: PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。一个生产者对象,只是保证类型安全。
声明处型变
Java 中List<String>
不能直接赋值List<Object>
,在 Kotlin 中,提供 out 修饰符确保接口或类成员中返回out
(生产),并从不被 in
(消费)。
1 | val stringList = listOf<String>() |
kotlin List 接口声明:
1 | public interface List<out E> : Collection<E> |
in。它使得一个类型参数逆变:只可以被消费而不可以被生产。逆变类的一个很好的例子是 Comparable
:
1 | abstract class Comparable<in T> { |
类型参数 T
被声明为 out 时,虽然 **<Base>
可以安全地作为 **<Derived>
的超类, 就只能出现输出-位置。
因为它在类型参数声明处提供,所以被称做声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。in 反之。
*NOTE:消费者 in, 生产者 out *
类型投影 (使用处型变)
将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T
比如 Array:
1 | val ints: Array<out Int> = arrayOf(1, 2, 3) |
上面out in 类型投影, 也就是Java 的使用处型变 ? [extends][super] T
星投影
若类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化将是该投影的子类型。
Kotlin 为此提供了所谓的星投影语法:
1 | val star: List<*> = listOf("C", "D", 1, 2) |
- 对于
Foo <out T>
,其中T
是一个具有上界TUpper
的协变类型参数,Foo <*>
等价于Foo <out TUpper>
。 这意味着当T
未知时,你可以安全地从Foo <*>
读取TUpper
的值。 - 对于
Foo <in T>
,其中T
是一个逆变类型参数,Foo <*>
等价于Foo <in Nothing>
。 这意味着当T
未知时,没有什么可以以安全的方式写入Foo <*>
。 - 对于
Foo <T>
,其中T
是一个具有上界TUpper
的不型变类型参数,Foo<*>
对于读取值时等价于Foo<out TUpper>
而对于写值时等价于Foo<in Nothing>
。
如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>
,我们可以想象以下星投影:
Function<*, String>
表示Function<in Nothing, String>
;Function<Int, *>
表示Function<Int, out Any?>
;Function<*, *>
表示Function<in Nothing, out Any?>
。
注意:星投影非常像 Java 的原始类型,但是安全。
泛型约束
能够替换给定类型参数的所有可能类型的集合可以由泛型约束限制。
最常见的约束类型是与 Java 的 extends 关键字对应的 上界:
1 | fun <T : Number> add(t: T) { |
默认的上界(如果没有声明)是 Any?
。在尖括号中只能指定一个上界。 如果同一类型参数需要多个上界,我们需要一个单独的 where-子句:
1 | fun <T> cloneWhenGreater(t: T) |
扩展
Kotlin 同 C# 和 Gosu 类似,能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式。 这通过叫做 扩展 的特殊声明完成。Kotlin 支持 扩展函数 和 扩展属性。
扩展函数和属性
声明一个扩展函数和属性,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀。
1 | class KotlinExtension { |
NOTE: this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象)
可空接收者
可空的接收者类型也能定义扩展,在对象变量上调用值为 null时,并且可以在函数体内检测 this == null
,
检测发生在扩展函数的内部。最好的例子,如 Library.kt中:
1 | public fun Any?.toString(): String |
伴生对象的扩展
伴生对象的扩展和定义扩展函数和属性一致:
1 | val KotlinExtension.Companion.anProperty: Int get() = 1 |
扩展的作用域
大多数在顶层定义扩展,要使用所定义包之外的一个扩展,导包就可以使用它。类内部也可以声明扩展(我认为这并无卵用)在这样的扩展内部,该类的对象和接收者的对象成员,自由访问。扩展声明所在的类的实例称为 分发接收者,扩展方法调用所在的接收者类型的实例称为 扩展接收者。
1 | class KotlinInteriorExtension { |
扩展是静态解析的
谨记扩展不能真正的修改他们所扩展的类, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数。
扩展函数是静态分发的,是由函数调用所在的表达式的类型来决定。
1 | //扩展是静态解析的 |
函数的分发对于分发接收者类型是虚拟的,但对于扩展接收者类型一定是静态的。
委托
kotlin 支持委托类和属性, 使用关键字 by
.
类委托
1 | interface Printer { |
by xxa
-子句表示xxa
将会在 类中内部存储。 并且编译器将生成转发给 xxa
的所有成员函数。
委托属性
kotlin 标准库实现如下常见的属性类型:
- 延迟属性(lazy properties): 其值只在首次访问时计算,
- 可观察属性(observable properties): 监听器会收到有关此属性变更的通知,
- 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。
延迟属性 Lazy
lazy()
是接受一个 lambda 并返回一个 Lazy <T>
实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get()
会执行已传递给 lazy()
的 lambda 表达式并记录结果, 后续调用 get()
只是返回记录的结果。
默认情况下,对于 lazy 属性的求值是同步锁的(synchronized):该值只在一个线程中计算,并且所有线程会看到相同的值。如果初始化委托的同步锁不是必需的,这样多个线程可以同时执行,那么将 LazyThreadSafetyMode.PUBLICATION
作为参数传递给 lazy()
函数。 而如果你确定初始化将总是发生在单个线程,那么你可以使用 LazyThreadSafetyMode.NONE
模式, 它不会有任何线程安全的保证和相关的开销。
1 | val lazyValue by lazy<String>(LazyThreadSafetyMode.SYNCHRONIZED) { |
这个例子输出:
1 | computed! |
可观察属性 Observable
Delegates.observable()
接受两个参数:初始值和修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值和新值。
如果你想能够截获一个赋值并“否决”它,就使用 vetoable()
取代 observable()
。 在属性被赋新值生效之前会调用传递给 vetoable
的处理程序。
1 | var name by Delegates.observable("No Name") { prop, old, new -> |
这个例子输出:
1 | 被赋值的属性:name, No Name > 両儀式 |
把属性储存在映射中
Map 可作为委托来实现委托属性。
1 | val languageMap = mapOf("language" to "kotlin") |
若要 var
属性只需要使用 MutableMap
。同样也适用于类
1 | class User(map: Map<String, Any?>) { |
局部委托属性
what? 看 lazy()
强大的初始化:
1 | fun letMake(take: () -> User) { |
自定义委托
var 属性需要实现 getValue()
setValue()
函数,val 只是需要getValue()
即可。两函数都需要用 operator
关键字来进行标记。
委托类还可以实现包含所需 operator
方法的 ReadOnlyProperty
或 ReadWriteProperty
接口之一。 这俩接口是在 Kotlin 标准库中声明的:
1 | class Delegate { |
背后原理
在使用委托的时, 不难发现该属性是委托类型。比如: p is String
,输出false。
在每个委托属性的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。 例如,对于属性 prop
,生成隐藏属性 prop$delegate
,而访问器的代码只是简单地委托给这个附加属性:
1 | class C { |
Kotlin 编译器在参数中提供了关于 prop
的所有必要信息:第一个参数 this
引用到外部类 C
的实例而 this::prop
是 KProperty
类型的反射对象,该对象描述 prop
自身。
提供委托
kotlin 提供 provideDelegate
操作符,可以扩展创建属性实现所委托对象的逻辑。使用场景是在创建属性时(而不仅在其 getter 或 setter 中)检查属性一致性。
1 | class R { |
提供委托, 并不复杂。通过一个函数去获取委托而已。provideDelegate
方法只影响辅助属性的创建,并不会影响为 getter 或 setter 生成的代码。
函数
函数用法
Kotlin 中的函数使用 fun
关键字声明
1 | fun funName(参数)[: returnType(默认 Unit)] ... |
函数参数规则
- 函数参数使用 Pascal 表示法定义,即 name: type , 参数用逗号隔开。
- 每个参数必须有显式类型, 参数还可以有默认值,当省略相应的参数时使用默认值, 以减少重载数量。
- 覆盖带有默认参数值的方法时,默认参数值省略。
- 如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用命名参数调用该函数来使用
- 如果最后一个 lambda 表达式参数从括号外传给函数函数调用,那么允许默认参数不传值
1 | fun invoke(method: String, invoke: Any = this) { |
可变数量的参数(Varargs)
函数的参数(通常是最后一个)可以用 vararg
修饰符标记:
1 | fun varargFun(method: String = "varargFun", vararg s: Int) { |
伸展(spread)操作符(在数组前面加 *
),可以数组元素添加到vararg 变量中去
返回 Unit 的函数
如果一个函数不返回任何有用的值,它的返回类型是 Unit
。Unit
是一种只有一个值——Unit
的类型。这个值不需要显式返回。Unit 就像Java 的 Void
1 | fun printHello(name: String?): Unit { |
单表达式函数
当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可。当返回值类型可由编译器推断时,显式声明返回类型是可选的, 但具有块代码体的函数必须始终显式指定返回类型。
1 | fun double(x: Int) = x * 2 |
中缀表示法
Kotlin支持数字运算的标准集,正是用了中缀表示法,当函数满足以下条件就能用 infix
关键字标注
- 他们是成员函数或扩展函数
- 他们只有一个参数
1 | infix fun String.append(s: String): String { |
函数作用域
在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 或 Scala 那样创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。
- 在类或对象内部定义的函数——成员函数
- 一个函数在另一个函数内部——局部函数
1 | //成员函数 |
泛型函数
函数可以有泛型参数,通过在函数名前使用尖括号指定。
1 | fun <T> singletonList(item: T): List<T> { |
高阶函数
高阶函数是将函数用作参数或返回值的函数。
1 | //函数用作参数 () -> Unit 不带参数并 且返回 Unit 类型值的函数 |
() -> Unit
被称为函数类型 , ::
操作符可参见函数引用, 当一个函数接受另一个函数作为最后一个参数,lambda 表达式参数可以在圆括号参数列表之外传递。 参见 callSuffix 的语法。
Lambda 表达式与匿名函数
一个 lambda 表达式或匿名函数是一个“函数字面值”,即一个未声明的函数, 作为表达式传递。
Lambda 表达式语法
lambda 表达式总是被大括号括着,完整语法形式的参数声明放在括号内,并有可选的类型标注, 函数体跟在一个 ->
符号之后。
1 | println({}) //输出: () -> kotlin.Unit |
挖槽,上面的是什么鬼。没了解Lambda 表达式 的,当然会困惑不已。
1 | fun explicitAnonymous(): () -> Int { |
这样一来就简单明了。{}
声明了个匿名函数,编译器作以下处理
1 | local final fun <anonymous>(): Unit |
当一个空参数的匿名函数, 如 { "String" }
,编译器会将lambda 主体中的最后一个或可能是单个)表达式会视为返回值。若是{ "String";1 }
则输出 () -> kotlin.Int
可选的类型标注,单表达式函数时,显式声明返回类型是可选的,匿名的参数类型也是可选的。非单表达式函数时,则变量名可选。
1 | val sum = { x: Int, y: Int -> x + y } //val sum: (Int, Int) → Int |
在 Kotlin 中Lambda表达式约定
- 函数的最后一个参数是一个函数,并且你传递一个 lambda 表达式作为相应的参数,你可以在圆括号之外传递
- lambda 是该调用的唯一参数,则调用中的圆括号可以完全省略。
- 函数字面值只有一个参数时, 那么它的声明可以省略(连同
->
),其名称是it
。 - 未使用的变量可用下划线取代其名称
- lambda 隐式返回最后一个表达式的值,可以用限定的返回语法显式返回
1 | fun <T> filter(predicate: (T) -> Boolean) { |
匿名函数
顾名思义,与常规函数相同不需要指定函数名
1 | val sumAnonymous = fun(x: Int, y: Int) = x + y //返回类型可以自动推断 |
匿名函数和lambda 是有区别的,匿名函数参数只能在括号内传递。 允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式。Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 return 语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 return将从匿名函数自身返回。
闭包
Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作用域中声明的变量。 与 Java 不同的是可以修改闭包中捕获的变量:
1 | var aNumber = 0 |
带接收者的函数字面值
Kotlin 提供了使用指定的 接收者对象 调用函数字面值的功能。 在函数字面值的函数体中,可以调用该接收者对象上的方法而无需任何额外的限定符。 这类似于扩展函数,它允许你在函数体内访问接收者对象的成员。 其用法的最重要的示例之一是类型安全的 Groovy-风格构建器。
1 | val sumR = fun Int.(other: Int): Int = this + other //val sumR: Int.(Int) → Int |
内联函数
要知道使用高阶函数时,每一个函数都是一个对象,且会捕获一个闭包。 所以带来一些运行时的效率损失,即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。
kotlin 支持 inline
修饰具有lambda参数的函数,以消除这类的开销。(仅支持顶层、成员函数,即不支持局函数)
1 | inline fun <T> lockInline(lock: Lock, body: () -> T): T { |
内联原理其实是编译器拷贝代码副本(如:body () -> T
),这可能导致生成的代码增加,但在循环中的“超多态(megamorphic)” 情况下,将在性能上有所提升。
不具有lambda参数的函数:
1 | inline fun test() { //warn 内联函数最适用于具有lambda参数的函数 |
NOTE:内联函数不支持局部函数
禁用内联
对于具有多个lambda参数的内联函数来说,默认内联, 可用 noinline
修饰lambda参数,禁用内联。
1 | inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { |
noinline
仅在内联函数中可使用
1 | inline fun foo(noinline notInlined: () -> Unit) { |
非局部返回
lambda 表达式内部不允许无标签的return
, 但传给的函数是内联的,该 return 也可以内联,所以它是允许返回。称为非局部返回。
1 | fun <T> lock(body: () -> T): Unit { |
循环中常用这种结构
1 | fun hasZeros(ints: List<Int>): Boolean { |
一些内联函数可能调用传给它们的不是直接来自函数体、而是来自另一个执行上下文的 lambda 表达式参数,例如来自局部对象或嵌套函数。在这种情况下,该 lambda 表达式中也不允许非局部控制流。为了标识这种情况,该 lambda 表达式参数需要用 crossinline
修饰符标记:
1 | inline fun post(crossinline body: () -> Unit) { |
具体化的类型参数
内联函数能有具体化的类型参数(Reified type parameters),用 reified
修饰符来限定类型参数
在前面泛型函数学习中,是不能有具体化参数,获取 class时带来不便。
1 | fun <T> findType(t: T) { |
内联属性
inline
修饰符还可以修饰没有幕后字段的属性的访问器(有setter/getter),可单独标注。
1 | val max inline get() = Int.MAX_VALUE |
公有 API 内联函数的限制
当一个内联函数是 public
或 protected
而不是 private
或 internal
声明的一部分时,就会认为它是一个模块级的公有 API。可以在其他模块中调用它,并且也可以在调用处内联这样的调用。
这带来了一些由模块做这样变更时导致的二进制兼容的风险——声明一个内联函数但调用它的模块在它修改后并没有重新编译。
为了消除这种由非公有 API 变更引入的不兼容的风险,公有 API 内联函数体内不允许使用非公有声明,即,不允许使用 private
与 internal
声明以及其部件。
一个 internal
声明可以由 @PublishedApi
标注,这会允许它在公有 API 内联函数中使用。当一个 internal
内联函数标记有 @PublishedApi
时,也会像公有函数一样检查其函数体。
1 | //公有 API 内联函数限制使用private 与 internal 声明以及其部件 (顶层声明) |
Kotlin与 Java 混合开发
Kotlin 中调用 Java
已映射类型
在Kotlin 中使用Java 代码,编译期间, Java 的原生类型映射到相应的 Kotlin 类型,运行时表示保持不变。
Kotlin 类型 | Java 类型 |
---|---|
kotlin.Byte | byte |
kotlin.Short | short |
kotlin.Int | int |
kotlin.Long | long |
kotlin.Char | char |
kotlin.Float | float |
kotlin.Double | double |
kotlin.Boolean | boolean |
Java 的装箱原始类型映射到可空的 Kotlin 类型:
Kotlin 类型 | Java 类型 |
---|---|
kotlin.Byte? | java.lang.Byte |
kotlin.Short? | java.lang. Short |
kotlin.Int? | java.lang.Integer |
kotlin.Long? | java.lang.Long |
kotlin.Char? | java.lang.Character |
kotlin.Float? | java.lang.Float |
kotlin.Double? | java.lang.Double |
kotlin.Boolean? | java.lang. Boolean |
一些非原生的内置类型也会作映射:
Kotlin 类型 | Java 类型 |
---|---|
kotlin.Any! | java.lang.Object |
kotlin.Cloneable! | java.lang.Cloneable |
kotlin.Comparable! | java.lang.Comparable |
kotlin.Enum! | java.lang.Enum |
kotlin.Annotation! | java.lang.Annotation |
kotlin.Deprecated! | java.lang.Deprecated |
kotlin.CharSequence! | java.lang.CharSequence |
kotlin.String! | java.lang.String |
kotlin.Number! | java.lang.Number |
kotlin.Throwable! | java.lang.Throwable |
NOTE: String!
为平台类型表示法
集合类型在 Kotlin 中可以是只读的或可变的,因此 Java 集合类型作如下映射: (下表中的所有 Kotlin 类型都驻留在 kotlin.collections
包中):
Java 类型 | Kotlin 只读类型 | Kotlin 可变类型 | 加载的平台类型 |
---|---|---|---|
Iterator |
Iterator |
MutableIterator |
(Mutable)Iterator |
Iterable |
Iterable |
MutableIterable |
(Mutable)Iterable |
Collection |
Collection |
MutableCollection |
(Mutable)Collection |
Set |
Set |
MutableSet |
(Mutable)Set |
List |
List |
MutableList |
(Mutable)List |
ListIterator |
ListIterator |
MutableListIterator |
(Mutable)ListIterator |
Map<K, V> | Map<K, V> | MutableMap<K, V> | (Mutable)Map<K, V>! |
Map.Entry<K, V> | Map.Entry<K, V> | MutableMap.MutableEntry<K,V> | (Mutable)Map.(Mutable)Entry<K, V>! |
请注意,用作类型参数的装箱原始类型映射到平台类型: 例如,List<java.lang.Integer>
在 Kotlin 中会成为 List<Int!>
。
Java 的数组按如下所述映射:
Java 类型 | Kotlin 类型 |
---|---|
int[] | kotlin.IntArray! |
String[] | kotlin.Array<(out) String>! |
空安全和平台类型
Java 中任何引用都可能是 null
,而Kotlin 类型安全(空安全)。 Java 声明的类型在 Kotlin 中空检查跟Java相同(可空,非空)称为平台类型。平台类型可用助记符!
加在后面来表示,但切记不能在程序中这样写,kotlin 并没有相应语法,IDE Doc 可以显示。
1 | val nullAny = JavaDataType.nullObj //实际: val nullAny: Any! |
NOTE:只要不是Java基本类型,在Kotlin中都会映射为 T!
Getter 和 Setter
遵循 Java 约定的 getter 和 setter 的方法(名称以 get
开头的无参数方法和以 set
开头的单参数方法)在 Kotlin 中表示为属性。 Boolean
访问器方法(其中 getter 的名称以 is
开头而 setter 的名称以 set
开头)会表示为与 getter 方法具有相同名称的属性。 例如:
1 | import java.util.Calendar |
请注意,如果 Java 类只有一个 setter,它在 Kotlin 中不会作为属性可见,因为 Kotlin 目前不支持只写(set-only)属性。
返回 void 的方法
如果一个 Java 方法返回 void,那么从 Kotlin 调用时中返回 Unit
。 万一有人使用其返回值,它将由 Kotlin 编译器在调用处赋值, 因为该值本身是预先知道的(是 Unit
)。
将 Kotlin 中是关键字的 Java 标识符进行转义
一些 Kotlin 关键字在 Java 中是有效标识符:in、 *object、 *is 等等。 如果一个 Java 库使用了 Kotlin 关键字作为方法,你仍然可以通过反引号(`)字符转义它来调用该方法
1 | foo.`is`(bar) |
Java 泛型
Kotlin 的泛型与 Java 有点不同(参见泛型)。当将 Java 类型导入 Kotlin 时,我们会执行一些转换:
- Java 的通配符转换成类型投影
Foo<? extends Bar>
转换成Foo<out Bar!>!
Foo<? super Bar>
转换成Foo<in Bar!>!
- Java的原始类型转换成星投影
List
转换成List<*>!
,即List<out Any?>!
和 Java 一样,Kotlin 在运行时不保留泛型,即对象不携带传递到他们构造器中的那些类型参数的实际类型。 即 ArrayList<Integer>()
和 ArrayList<Character>()
是不能区分的。 这使得执行 is-检测不可能照顾到泛型。 Kotlin 只允许 is-检测星投影的泛型类型:
1 | if (a is List<Int>) // 错误:无法检查它是否真的是一个 Int 列表 |
Java 集合
java 集合类型映射的平台类型都是可变的,用法如kotlin 一样,而且 操作符约定同样有效
Java 数组
与 Java 不同,Kotlin 中的数组是不型变的。这意味着 Kotlin 不允许我们把一个 Array<String>
赋值给一个 Array<Any>
, 从而避免了可能的运行时故障。Kotlin 也禁止我们把一个子类的数组当做超类的数组传递给 Kotlin 的方法, 但是对于 Java 方法,这是允许的(通过 Array<(out) String>!
这种形式的平台类型)。
Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特化的类(IntArray
、 DoubleArray
、 CharArray
等等)来处理这种情况。 它们与 Array
类无关,并且会编译成 Java 原生类型数组以获得最佳性能。
假设有一个接受 int 数组索引的 Java 方法:
1 | public class JavaArrayExample { |
在 Kotlin 中你可以这样传递一个原生类型的数组:
1 | val javaObj = JavaArrayExample() |
当编译为 JVM 字节代码时,编译器会优化对数组的访问,这样就不会引入任何开销:
1 | val array = arrayOf(1, 2, 3, 4) |
即使当我们使用索引定位时,也不会引入任何开销
1 | for (i in array.indices) {// 不会创建迭代器 |
最后,in-检测也没有额外开销
1 | if (i in array.indices) { // 同 (i >= 0 && i < array.size) |
Java 可变参数
Java 类有时声明一个具有可变数量参数(varargs)的方法来使用索引。
1 | public class JavaArrayExample { |
在这种情况下,你需要使用展开运算符 *
来传递 IntArray
:
1 | val javaObj = JavaArrayExample() |
目前无法传递 null 给一个声明为可变参数的方法。
操作符
由于 Java 无法标记用于运算符语法的方法,Kotlin 允许具有正确名称和签名的任何 Java 方法作为运算符重载和其他约定(invoke()
等)使用。 不允许使用中缀调用语法调用 Java 方法。
受检异常
在 Kotlin 中,所有异常都是非受检的,这意味着编译器不会强迫你捕获其中的任何一个。 因此,当你调用一个声明受检异常的 Java 方法时,Kotlin 不会强迫你做任何事情:
对象方法
当 Java 类型导入到 Kotlin 中时,类型 java.lang.Object
的所有引用都成了 Any
。 而因为 Any
不是平台指定的,它只声明了 toString()
、hashCode()
和 equals()
作为其成员, 所以为了能用到 java.lang.Object
的其他成员,Kotlin 要用到扩展函数。
wait()/notify()
Effective Java 第 69 条善意地建议优先使用并发工具(concurrency utilities)而不是 wait()
和 notify()
。 因此,类型 Any
的引用不提供这两个方法。 如果你真的需要调用它们的话,你可以将其转换为 java.lang.Object
:
1 | (foo as java.lang.Object).wait() |
getClass()
要取得对象的 Java 类,请在类引用上使用 java
扩展属性。
1 | val fooClass = foo::class.java |
上面的代码使用了自 Kotlin 1.1 起支持的绑定的类引用。你也可以使用 javaClass
扩展属性。
1 | val fooClass = foo.javaClass |
clone()
要覆盖 clone()
,需要继承 kotlin.Cloneable
:
1 | class Example : Cloneable { |
不要忘记 Effective Java 的第 11 条: 谨慎地改写clone。
finalize()
要覆盖 finalize()
,所有你需要做的就是简单地声明它,而不需要 override 关键字:
1 | class C { |
根据 Java 的规则,finalize()
不能是 private 的。
####
访问静态成员
Java 类的静态成员会形成该类的“伴生对象”。我们无法将这样的“伴生对象”作为值来传递, 但可以显式访问其成员,例如:
1 | val character = Character |
Java 反射
Java 反射适用于 Kotlin 类,反之亦然。如上所述,你可以使用 instance::class.java
,ClassName::class.java
或者 instance.javaClass
通过 java.lang.Class
来进入 Java 反射。
其他支持的情况包括为一个 Kotlin 属性获取一个 Java 的 getter/setter 方法或者幕后字段、为一个 Java 字段获取一个 KProperty
、为一个 KFunction
获取一个 Java 方法或者构造函数,反之亦然。
SAM 转换
就像 Java 8 一样,Kotlin 支持 SAM 转换。这意味着 Kotlin 函数字面值可以被自动的转换成只有一个非默认方法的 Java 接口的实现,只要这个方法的参数类型能够与这个 Kotlin 函数的参数类型相匹配。
你可以这样创建 SAM 接口的实例:
1 | val runnable = Runnable { println("This runs in a runnable") } |
……以及在方法调用中:
1 | val executor = ThreadPoolExecutor() |
如果 Java 类有多个接受函数式接口的方法,那么可以通过使用将 lambda 表达式转换为特定的 SAM 类型的适配器函数来选择需要调用的方法。这些适配器函数也会按需由编译器生成。
1 | executor.execute(Runnable { println("This runs in a thread pool") }) |
请注意,SAM 转换只适用于接口,而不适用于抽象类,即使这些抽象类也只有一个抽象方法。
还要注意,此功能只适用于 Java 互操作;因为 Kotlin 具有合适的函数类型,所以不需要将函数自动转换为 Kotlin 接口的实现,因此不受支持。
Java中调用 Kotlin
Java 可以轻松调用 Kotlin 代码。
属性
Kotlin 属性会编译成以下 Java 元素:
- 一个 getter 方法,名称通过加前缀
get
算出; - 一个 setter 方法,名称通过加前缀
set
算出(只适用于var
属性); - 一个私有字段,与属性名称相同(仅适用于具有幕后字段的属性)。
例如,var firstName: String
编译成以下 Java 声明:
1 | private String firstName; |
如果属性的名称以 is
开头,则使用不同的名称映射规则:getter 的名称与属性名称相同,并且 setter 的名称是通过将 is
替换为 set
获得。 例如,对于属性 isOpen
,其 getter 会称做 isOpen()
,而其 setter 会称做 setOpen()
。 这一规则适用于任何类型的属性,并不仅限于 Boolean
。
包级函数
在 org.foo.bar
包内的 example.kt
文件中声明的所有的函数和属性,包括扩展函数, 都编译成一个名为 org.foo.bar.ExampleKt
的 Java 类的静态方法。
1 | // example.kt |
1 | // Java |
可以使用 @JvmName
注解修改生成的 Java 类的类名:
1 | @file:JvmName("DemoUtils") |
1 | // Java |
如果多个文件中生成了相同的 Java 类名(包名相同并且类名相同或者有相同的 @JvmName
注解)通常是错误的。然而,编译器能够生成一个单一的 Java 外观类,它具有指定的名称且包含来自所有文件中具有该名称的所有声明。 要启用生成这样的外观,请在所有相关文件中使用 @JvmMultifileClass 注解。
1 | // oldutils.kt |
1 | // newutils.kt |
1 | // Java |
实例字段
如果需要在 Java 中将 Kotlin 属性作为字段暴露,那就需要使用 @JvmField
注解对其标注。 该字段将具有与底层属性相同的可见性。如果一个属性有幕后字段(backing field)、非私有、没有 open
/override
或者 const
修饰符并且不是被委托的属性,那么你可以用 @JvmField
注解该属性。
1 | class C(id: String) { |
1 | // Java |
延迟初始化的属性(在Java中)也会暴露为字段。 该字段的可见性与 lateinit
属性的 setter 相同。
静态字段
在命名对象或伴生对象中声明的 Kotlin 属性会在该命名对象或包含伴生对象的类中具有静态幕后字段。
通常这些字段是私有的,但可以通过以下方式之一暴露出来:
@JvmField
注解;lateinit
修饰符;const
修饰符。
使用 @JvmField
标注这样的属性使其成为与属性本身具有相同可见性的静态字段。
1 | class Key(val value: Int) { |
1 | // Java |
在命名对象或者伴生对象中的一个延迟初始化的属性具有与属性 setter 相同可见性的静态幕后字段。
1 | object Singleton { |
1 | // Java |
用 const
标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段:
1 | // 文件 example.kt |
在 Java 中:
1 | int c = Obj.CONST; |
静态方法
如上所述,Kotlin 将包级函数表示为静态方法。 Kotlin 还可以为命名对象或伴生对象中定义的函数生成静态方法,如果你将这些函数标注为 @JvmStatic
的话。 如果你使用该注解,编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。 例如:
1 | class C { |
现在,foo()
在 Java 中是静态的,而 bar()
不是:
1 | C.foo(); // 没问题 |
对于命名对象也同样:
1 | object Obj { |
在 Java 中:
1 | Obj.foo(); // 没问题 |
@JvmStatic
注解也可以应用于对象或伴生对象的属性, 使其 getter 和 setter 方法在该对象或包含该伴生对象的类中是静态成员。
可见性
Kotlin 的可见性以下列方式映射到 Java:
private
成员编译成private
成员;private
的顶层声明编译成包级局部声明;protected
保持protected
(注意 Java 允许访问同一个包中其他类的受保护成员, 而 Kotlin 不能,所以 Java 类会访问更广泛的代码);internal
声明会成为 Java 中的public
。internal
类的成员会通过名字修饰,使其更难以在 Java 中意外使用到,并且根据 Kotlin 规则使其允许重载相同签名的成员而互不可见;public
保持public
。
KClass
有时你需要调用有 KClass
类型参数的 Kotlin 方法。 因为没有从 Class
到 KClass
的自动转换,所以你必须通过调用 Class<T>.kotlin
扩展属性的等价形式来手动进行转换:
1 | kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class) |
用 @JvmName 解决签名冲突
有时我们想让一个 Kotlin 中的命名函数在字节码中有另外一个 JVM 名称。 最突出的例子是由于类型擦除引发的:
1 | fun List<String>.filterValid(): List<String> |
这两个函数不能同时定义,因为它们的 JVM 签名是一样的:filterValid(Ljava/util/List;)Ljava/util/List;
。 如果我们真的希望它们在 Kotlin 中用相同名称,我们需要用 @JvmName
去标注其中的一个(或两个),并指定不同的名称作为参数:
1 | fun List<String>.filterValid(): List<String> |
在 Kotlin 中它们可以用相同的名称 filterValid
来访问,而在 Java 中,它们分别是 filterValid
和 filterValidInt
。
同样的技巧也适用于属性 x
和函数 getX()
共存:
1 | val x: Int |
生成重载
通常,如果你写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads
注解。
该注解也适用于构造函数、静态方法等。它不能用于抽象方法,包括在接口中定义的方法。
1 | class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { |
对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 :
1 | // 构造函数: |
请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数。这就算没有 @JvmOverloads
注解也有效。
受检异常
如上所述,Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常。 于是如果我们有一个这样的 Kotlin 函数:
1 | // example.kt |
然后我们想要在 Java 中调用它并捕捉这个异常:
1 | // Java |
因为 foo()
没有声明 IOException
,我们从 Java 编译器得到了一个报错消息。 为了解决这个问题,要在 Kotlin 中使用 @Throws
注解。
1 | @Throws(IOException::class) |
空安全性
当从 Java 中调用 Kotlin 函数时,没人阻止我们将 null 作为非空参数传递。 这就是为什么 Kotlin 给所有期望非空参数的公有函数生成运行时检测。 这样我们就能在 Java 代码里立即得到 NullPointerException
。
型变的泛型
当 Kotlin 的类使用了声明处型变,有两种选择可以从 Java 代码中看到它们的用法。让我们假设我们有以下类和两个使用它的函数:
1 | class Box<out T>(val value: T) |
一种看似理所当然地将这俩函数转换成 Java 代码的方式可能会是:
1 | Box<Derived> boxDerived(Derived value) { …… } |
问题是,在 Kotlin 中我们可以这样写 unboxBase(boxDerived("s"))
,但是在 Java 中是行不通的,因为在 Java 中类 Box
在其泛型参数 T
上是不型变的,于是 Box<Derived>
并不是 Box<Base>
的子类。 要使其在 Java 中工作,我们按以下这样定义 unboxBase
:
1 | Base unboxBase(Box<? extends Base> box) { …… } |
这里我们使用 Java 的通配符类型(? extends Base
)来通过使用处型变来模拟声明处型变,因为在 Java 中只能这样。
当它作为参数出现时,为了让 Kotlin 的 API 在 Java 中工作,对于协变定义的 Box
我们生成 Box<Super>
作为 Box<? extends Super>
(或者对于逆变定义的 Foo
生成 Foo<? super Bar>
)。当它是一个返回值时, 我们不生成通配符,因为否则 Java 客户端将必须处理它们(并且它违反常用 Java 编码风格)。因此,我们的示例中的对应函数实际上翻译如下:
1 | // 作为返回类型——没有通配符 |
注意:当参数类型是 final 时,生成通配符通常没有意义,所以无论在什么地方 Box<String>
始终转换为 Box<String>
。
如果我们在默认不生成通配符的地方需要通配符,我们可以使用 @JvmWildcard
注解:
1 | fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value) |
另一方面,如果我们根本不需要默认的通配符转换,我们可以使用@JvmSuppressWildcards
1 | fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value |
注意:@JvmSuppressWildcards
不仅可用于单个类型参数,还可用于整个声明(如函数或类),从而抑制其中的所有通配符。
Nothing 类型翻译
类型 Nothing
是特殊的,因为它在 Java 中没有自然的对应。确实,每个 Java 引用类型,包括java.lang.Void
都可以接受 null
值,但是 Nothing 不行。因此,这种类型不能在 Java 世界中准确表示。这就是为什么在使用 Nothing
参数的地方 Kotlin 生成一个原始类型:
1 | fun emptyList(): List<Nothing> = listOf() |
在 Kotlin 中使用 JNI
要声明一个在本地(C 或 C++)代码中实现的函数,你需要使用 external
修饰符来标记它:
1 | external fun foo(x: Int): Double |
其余的过程与 Java 中的工作方式完全相同。
Kotlin 高级编程
领域特定语言 DSL
域特定语言(DSL)的基本思想是针对特定类型的问题的计算机语言,而不是面向任何类型的软件问题的通用语言。
类型安全的构建器
构建器(builder)的概念在 Groovy 社区中非常热门。 构建器允许以半声明(semi-declarative)的方式定义数据。构建器很适合用来生成 XML、 布局 UI 组件、 描述 3D 场景以及其他更多功能……
Kotlin 允许检查类型的构建器,比 Groovy 自身的动态类型实现更具吸引力。
HTML DSL kotlin 官方示例:
1 | fun main(args: Array<String>) { |
上面实现 HTML 标签,实际上是调用一个 lambda函数,用一个标签接收者的函数类型zuo作为参数,使在函数内部调用该实例的成员。
几个厉害的 DSL 项目
- Anko 用于 Android 的,用于描述 UI 。
- Gensokyo 用于 Swing 的,用于描述 UI
- KotlinTest Kotlin测试框架基于优秀的Scalatest
协程 Coroutine
在 Kotlin 1.1 中协程是实验性的。另外kotlin 为了减少程序体积,根据需要使用协程,你要加入kotlinx-coroutines-core
库.
一些 API 启动长时间运行的操作(例如网络 IO、文件 IO、CPU 或 GPU 密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。
许多在其他语言中可用的异步机制可以使用 Kotlin 协程实现为库。这包括源于 C# 和 ECMAScript 的 async
/await
、源于 Go 的 管道 和 select
以及源于 C# 和 Python 生成器/yield
。关于提供这些结构的库请参见其下文描述。
阻塞 vs 挂起
基本上,协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中一个会导致一些重要的任务被延迟。
另一方面,协程挂起几乎是无代价的。不需要上下文切换或者 OS 的任何其他干预。最重要的是,挂起可以在很大程度上由用户库控制:作为库的作者,我们可以决定挂起时发生什么并根据需求优化/记日志/截获。
另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。
挂起函数
当我们调用标记有特殊修饰符 suspend
的函数时,会发生挂起:
1 | suspend fun doSomething(foo: Foo): Bar { |
这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程,必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起 lambda 表达式)。让我们来看一个例子,一个简化的 async()
函数(源自 kotlinx.coroutines
库):
1 | fun <T> async(block: suspend () -> T) |
这里的 async()
是一个普通函数(不是挂起函数),但是它的 block
参数具有一个带 suspend
修饰符的函数类型: suspend () -> T
。所以,当我们将一个 lambda 表达式传给 async()
时,它会是挂起 lambda 表达式,于是我们可以从中调用挂起函数:
1 | async { |
继续该类比,await()
可以是一个挂起函数(因此也可以在一个 async {}
块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果:
1 | async { |
更多关于 async/await
函数实际在 kotlinx.coroutines
中如何工作的信息可以在这里找到。
请注意,挂起函数 await()
和 doSomething()
不能在像 main()
这样的普通函数中调用:
1 | fun main(args: Array<String>) { |
还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定 suspend
修饰符:
1 | interface Base { |
@RestrictsSuspension 注解
扩展函数(和 lambda 表达式)也可以标记为 suspend
,就像普通的一样。这允许创建 DSL 及其他用户可扩展的 API。在某些情况下,库作者需要阻止用户添加新方式来挂起协程。
为了实现这一点,可以使用 @RestrictsSuspension
注解。当接收者类/接口 R
用它标注时,所有挂起扩展都需要委托给 R
的成员或其它委托给它的扩展。由于扩展不能无限相互委托(程序不会终止),这保证所有挂起都通过调用 R
的成员发生,库的作者就可以完全控制了。
这在少数情况是需要的,当每次挂起在库中以特殊方式处理时。例如,当通过 buildSequence()
函数实现下文所述的生成器时,我们需要确保在协程中的任何挂起调用最终调用 yield()
或 yieldAll()
而不是任何其他函数。这就是为什么 SequenceBuilder
用 @RestrictsSuspension
注解:
1 |
|
参见其 Github 上 的源代码。
协程的内部机制
我们不是在这里给出一个关于协程如何工作的完整解释,然而粗略地认识发生了什么是相当重要的。
协程完全通过编译技术实现(不需要来自 VM 或 OS 端的支持),挂起通过代码来生效。基本上,每个挂起函数(优化可能适用,但我们不在这里讨论)都转换为状态机,其中的状态对应于挂起调用。刚好在挂起前,下一状态与相关局部变量等一起存储在编译器生成的类的字段中。在恢复该协程时,恢复局部变量并且状态机从刚好挂起之后的状态进行。
挂起的协程可以作为保持其挂起状态与局部变量的对象来存储和传递。这种对象的类型是 Continuation
,而这里描述的整个代码转换对应于经典的延续性传递风格(Continuation-passing style)。因此,挂起函数有一个 Continuation
类型的额外参数作为高级选项。
关于协程工作原理的更多细节可以在这个设计文档中找到。在其他语言(如 C# 或者 ECMAScript 2016)中的 async/await 的类似描述与此相关,虽然它们实现的语言功能可能不像 Kotlin 协程这样通用。
协程的实验性状态
协程的设计是实验性的,这意味着它可能在即将发布的版本中更改。当在 Kotlin 1.1 中编译协程时,默认情况下会报一个警告:“协程”功能是实验性的。要移出该警告,你需要指定 opt-in 标志。
由于其实验性状态,标准库中协程相关的 API 放在 kotlin.coroutines.experimental
包下。当设计完成并且实验性状态解除时,最终的 API 会移动到 kotlin.coroutines
,并且实验包会被保留(可能在一个单独的构件中)以实现向后兼容。
重要注意事项:我们建议库作者遵循相同惯例:给暴露基于协程 API 的包添加“experimental”后缀(如 com.example.experimental
),以使你的库保持二进制兼容。当最终 API 发布时,请按照下列步骤操作:
- 将所有 API 复制到
com.example
(没有 experimental 后缀), - 保持实验包的向后兼容性。
这将最小化你的用户的迁移问题。
标准 API
协程有三个主要组成部分:
- 语言支持(即如上所述的挂起功能),
- Kotlin 标准库中的底层核心 API,
- 可以直接在用户代码中使用的高级 API。
底层 API:kotlin.coroutines
底层 API 相对较小,并且除了创建更高级的库之外,不应该使用它。 它由两个主要包组成:
kotlin.coroutines.experimental
带有主要类型与下述原语kotlin.coroutines.experimental.intrinsics
带有甚至更底层的内在函数如suspendCoroutineOrReturn
关于这些 API 用法的更多细节可以在这里找到。
kotlin.coroutines 中的生成器 API
kotlin.coroutines.experimental
中仅有的“应用程序级”函数是
这些包含在 kotlin-stdlib
中因为他们与序列相关。这些函数(我们可以仅限于这里的 buildSequence()
)实现了 生成器 ,即提供一种廉价构建惰性序列的方法:
1 | val fibonacciSeq = buildSequence { |
这通过创建一个协程生成一个惰性的、潜在无限的斐波那契数列,该协程通过调用 yield()
函数来产生连续的斐波纳契数。当在这样的序列的迭代器上迭代每一步,都会执行生成下一个数的协程的另一部分。因此,我们可以从该序列中取出任何有限的数字列表,例如 fibonacciSeq.take(8).toList()
结果是 [1, 1, 2, 3, 5, 8, 13, 21]
。协程足够廉价使这很实用。
为了演示这样一个序列的真正惰性,让我们在调用 buildSequence()
内部输出一些调试信息:
1 | val lazySeq = buildSequence { |
运行上面的代码看,是不是我们输出前三个元素的数字与生成循环的 STEP
有交叉。这意味着计算确实是惰性的。要输出 1
,我们只执行到第一个 yield(i)
,并且过程中会输出 START
。然后,输出 2
,我们需要继续下一个 yield(i)
,并会输出 STEP
。3
也一样。永远不会输出再下一个 STEP
(以及END
),因为我们再也没有请求序列的后续元素。
为了一次产生值的集合(或序列),可以使用 yieldAll()
函数:
1 | val lazySeq = buildSequence { |
buildIterator()
的工作方式类似于 buildSequence()
,但返回一个惰性迭代器。
可以通过为 SequenceBuilder
类写挂起扩展(带有上文描述的 @RestrictsSuspension
注解)来为 buildSequence()
添加自定义生产逻辑(custom yielding logic):
1 | suspend fun SequenceBuilder<Int>.yieldIfOdd(x: Int) { |
其他高级 API:kotlinx.coroutines
只有与协程相关的核心 API 可以从 Kotlin 标准库获得。这主要包括所有基于协程的库可能使用的核心原语和接口。
大多数基于协程的应用程序级API都作为单独的库发布:kotlinx.coroutines
。这个库覆盖了
- 使用
kotlinx-coroutines-core
的平台无关异步编程
- 此模块包括支持
select
和其他便利原语的类似 Go 的管道 - 这个库的综合指南在这里。
基于 JDK 8 中的
CompletableFuture
的 API:kotlinx-coroutines-jdk8
基于 JDK 7 及更高版本 API 的非阻塞 IO(NIO):
kotlinx-coroutines-nio
支持 Swing (
kotlinx-coroutines-swing
) 和 JavaFx (kotlinx-coroutines-javafx
)支持 RxJava:
kotlinx-coroutines-rx
这些库既作为使通用任务易用的便利的 API,也作为如何构建基于协程的库的端到端示例。
更多
集合
与大多数语言不同,Kotlin 区分可变集合和不可变集合(lists、sets、maps 等)。精确控制什么时候集合可编辑有助于消除 bug 和设计良好的 API。
预先了解一个可变集合的只读 视图 和一个真正的不可变集合之间的区别是很重要的。它们都容易创建,但类型系统不能表达它们的差别,所以由你来跟踪(是否相关)。
Kotlin 的 List<out T>
类型是一个提供只读操作如 size
、get
等的接口。和 Java 类似,它继承自 Collection<T>
进而继承自 Iterable<T>
。改变 list 的方法是由 MutableList<T>
加入的。这一模式同样适用于 Set<out T>/MutableSet<T>
及 Map<K, out V>/MutableMap<K, V>
。
我们可以看下 list 及 set 类型的基本用法:
1 | val numbers: MutableList<Int> = mutableListOf(1, 2, 3) |
Kotlin 没有专门的语法结构创建 list 或 set。 要用标准库的方法,如 listOf()
、 mutableListOf()
、 setOf()
、 mutableSetOf()
。 在非性能关键代码中创建 map 可以用一个简单的惯用法来完成:mapOf(a to b, c to d)
。
注意上面的 readOnlyView
变量(译者注:与对应可变集合变量 numbers
)指向相同的底层 list 并会随之改变。 如果一个 list 只存在只读引用,我们可以考虑该集合完全不可变。创建一个这样的集合的一个简单方式如下:
1 | val items = listOf(1, 2, 3) |
目前 listOf
方法是使用 array list 实现的,但是未来可以利用它们知道自己不能变的事实,返回更节约内存的完全不可变的集合类型。
注意这些类型是协变的。这意味着,你可以把一个 List<Rectangle>
赋值给 List<Shape>
假定 Rectangle 继承自 Shape。对于可变集合类型这是不允许的,因为这将导致运行时故障。
有时你想给调用者返回一个集合在某个特定时间的一个快照, 一个保证不会变的:
1 | class Controller { |
这个 toList
扩展方法只是复制列表项,因此返回的 list 保证永远不会改变。
List 和 set 有很多有用的扩展方法值得熟悉:
1 | val items = listOf(1, 2, 3, 4) |
…… 以及所有你所期望的实用工具,例如 sort、zip、fold、reduce 等等。
Map 遵循同样模式。它们可以容易地实例化和访问,像这样:
1 | val readWriteMap = hashMapOf("foo" to 1, "bar" to 2) |
类型安全和智能转换
空安全
Kotlin 的类型系统
- 可空类型
- 非空类型
它消除了很多编程语言(如: Java)来自于代码空引用,而导致的 NullPointerException
或简称 NPE
。
NOTE: Kotlin 发生 NPE 原因可能如下:
- 显式调用
throw NullPointerException()
- 使用了下文描述的
!!
操作符 - 外部 Java 代码导致的
在上面 变量 中, Kotlin 默认声明变量时是非空类型的,要使该变量接收 null
值,需使用 ?
操作符 , 例子如下
1 | var aNullNothing = null |
当声明可空类型变量时,它是不安全的,访问方法或属性时需要作处理:
- 在条件中检查 null ,但仅适用于
val
且不可覆盖(即不能用 open 修饰)或者get
的不可变的变量。 - 安全的调用
?.
, 若为null 则跳过,否则接着调用 - !! 操作符 ,会返回一个非空的值,否则抛出一个
NPE
异常
条件中检查 nul 例子
1 | open class TestCheckNull { |
安全调用和!! 操作符对比
1 | cNullUnable.dec() //保证不会导致 NPE |
类型检测和安全的类型转换
is
!is
运算符检测一个表达式是否某类型的一个实例。在许多情况下,不需要在 Kotlin 中使用显式转换操作符,因为编译器跟踪不可变值的is
-检查,并在需要时自动插入(安全的)转换:
1 | val obj: Any = "" |
as
as?
运算符能把对象转换为目标类型,常规类型转换可能会导致ClassCastException
。使用安全的类型转换符as?
,如果尝试转换不成功则返回 null:
1 | val father = Father() |
NOTE: Kotlin 类型检测十分智能, 想了解请更多参考 Type Checks and Casts
操作符重载
Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示(如 +
或 *
)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或扩展函数。
重载操作符的函数需要用 operator
修饰符标记。
另外,我们描述为不同操作符规范操作符重载的约定。
一元前缀操作符
表达式 | 翻译为 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
这个表是说,当编译器处理例如表达式 +a
时,它执行以下步骤:
- 确定
a
的类型,令其为T
。 - 为接收者
T
查找一个带有operator
修饰符的无参函数unaryPlus()
,即成员函数或扩展函数。 - 如果函数不存在或不明确,则导致编译错误。
- 如果函数存在且其返回类型为
R
,那就表达式+a
具有类型R
。
注意 这些操作以及所有其他操作都针对基本类型做了优化,不会为它们引入函数调用的开销。
以下是如何重载一元减运算符的示例:
1 | data class Point(val x: Int, val y: Int) |
递增与递减
表达式 | 翻译为 |
---|---|
a++ | a.inc() + 见下文 |
a– | a.dec() + 见下文 |
inc()
和 dec()
函数必须返回一个值,它用于赋值给使用++
或 --
操作的变量。它们不应该改变在其上调用 inc()
或 dec()
的对象。
编译器执行以下步骤来解析后缀形式的操作符,例如 a++
:
- 确定
a
的类型,令其为T
。 - 查找一个适用于类型为
T
的接收者的、带有operator
修饰符的无参数函数inc()
。 - 检查函数的返回类型是
T
的子类型。
计算表达式的步骤是:
- 把
a
的初始值存储到临时存储a0
中, - 把
a.inc()
结果赋值给a
, - 把
a0
作为表达式的结果返回。
对于 a--
,步骤是完全类似的。
对于前缀形式 ++a
和 --a
以相同方式解析,其步骤是:
- 把
a.inc()
结果赋值给a
, - 把
a
的新值作为表达式结果返回。
二元操作算术运算符
表达式 | 翻译为 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b)、 a.mod(b) (已弃用) |
a..b | a.rangeTo(b) |
对于此表中的操作,编译器只是解析成翻译为列中的表达式。
请注意,自 Kotlin 1.1 起支持 rem
运算符。Kotlin 1.0 使用 mod
运算符,它在
Kotlin 1.1 中被弃用。
示例
下面是一个从给定值起始的 Counter 类的示例,它可以使用重载的 +
运算符来增加计数。
1 | data class Counter(val dayIndex: Int) { |
In操作符
表达式 | 翻译为 |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
对于 in
和 !in
,过程是相同的,但是参数的顺序是相反的。
索引访问操作符
表达式 | 翻译为 |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ……, i_n] | a.get(i_1, ……, i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ……, i_n] = b | a.set(i_1, ……, i_n, b) |
方括号转换为调用带有适当数量参数的 get
和 set
。
调用操作符
表达式 | 翻译为 |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ……, i_n) | a.invoke(i_1, ……, i_n) |
圆括号转换为调用带有适当数量参数的 invoke
。
广义赋值
表达式 | 翻译为 |
---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b), a.modAssign(b)(已弃用) |
对于赋值操作,例如 a += b
,编译器执行以下步骤:
- 如果右列的函数可用
- 如果相应的二元函数(即
plusAssign()
对应于plus()
)也可用,那么报告错误(模糊)。 - 确保其返回类型是
Unit
,否则报告错误。 - 生成
a.plusAssign(b)
的代码
- 如果相应的二元函数(即
- 否则试着生成
a = a + b
的代码(这里包含类型检查:a + b
的类型必须是a
的子类型)。
注意:赋值在 Kotlin 中不是表达式。
相等与不等操作符
表达式 | 翻译为 |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null )) |
注意:===
和 !==
(同一性检查)不可重载,因此不存在对他们的约定
这个 ==
操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null
值。null == null
总是 true,对于非空的 x
,x == null
总是 false 而不会调用 x.equals()
。
比较操作符
表达式 | 翻译为 |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
所有的比较都转换为对 compareTo
的调用,这个函数需要返回 Int
值
属性委托操作符
provideDelegate
、 getValue
以及 setValue
操作符函数已在委托属性中描述。
命名函数的中缀调用
我们可以通过中缀函数的调用 来模拟自定义中缀操作符。
类型相等性
Kotlin 中有两种类型的相等性:
- 引用相等(两个引用指向同一对象)
- 结构相等(用 equals() 检查)
引用相等
引用相等由 ===
(以及其否定形式 !==
)操作判断。a === b
当且仅当 a 和 b 指向同一个对象时求值为 true。
结构相等
结构相等由 ==
(以及其否定形式 !=
)操作判断。按照惯例,像 a == b
这样的表达式会翻译成
a?.equals(b) ?: (b === null)
也就是说如果 a
不是 null
则调用 equals(Any?)
函数,否则(即 a
是 null
)检查 b
是否与 null
引用相等。
请注意,当与 null
显式比较时完全没必要优化你的代码:a == null
会被自动转换为 a=== null
。同类型才有可比性。
This表达式
为了表示当前的 接收者 我们使用 this 表达式:
- 在类的成员中,this 指的是该类的当前对象
- 在扩展函数或者带接收者的函数字面值中, this 表示在点左侧传递的 接收者 参数。
如果 this 没有限定符,它指的是最内层的包含它的作用域。要引用其他作用域中的 this,请使用 标签限定符:
1 | fun main(args: Array<String>) { |
Nothing 类型
如果用 null
来初始化一个要推断类型的值,而又没有其他信息可用于确定更具体的类型时,编译器会推断出 Nothing?
类型:
1 | val nothingInt/*: Nothing?*/ = null |
另外Kotlin 中 throw
是表达式, 表达式的类型是特殊类型 Nothing
。 该类型没有值,而是用于标记永远不能达到的代码位置。Nothing
可以用来标记一个永远不会返回的函数, 也可以作为 Elvis 表达式的一部分:
1 | val nothingInt/*: Nothing?*/ = null |
解构声明
解构声明是创建多个变量与对象componentN
函数对应起来。例如在上面的数据类中
1 | val (name, age) = KotlinDataClass.User("Lisa", 18) |
NOTE: componentN()
函数需要用 operator
关键字标记,以允许在解构声明中使用它们。它可以用for-循环、
map-映射, 以及 lambda 表达式中。
1 | fun main(args: Array<String>) { |
解构声明的好处, 如request 函数时要返回两个东西时,用它爽爆了。因为编译器始终会创建多个变量接收,效率并不比之前用对象的高。但实际上并不需要解析一个对象里的大量变量,否则通过对象 .
属性获取值。
1 | val (name, age) = person //编译器会生成如下两句代码 |
相等性
Kotlin 中有两种类型的相等性:
- 引用相等(两个引用指向同一对象)
- 结构相等(用
equals()
检查)
引用相等
引用相等由 ===
(以及其否定形式 !==
)操作判断。a === b
当且仅当 a
和 b
指向同一个对象时求值为 true。
结构相等
结构相等由 ==
(以及其否定形式 !=
)操作判断。按照惯例,像 a == b
这样的表达式会翻译成
1 | a?.equals(b) ?: (b === null) |
也就是说如果 a
不是 null
则调用 equals(Any?)
函数,否则(即 a
是 null
)检查 b 是否与 null
引用相等。
请注意,当与 null
显式比较时完全没必要优化你的代码:a == null
会被自动转换为 a=== null
。
浮点数相等性
当相等性检测的两个操作数都是静态已知的(可空或非空的)Float
或 Double
类型时,该检测遵循 IEEE 754 浮点数运算标准。
否则会使用不符合该标准的结构相等性检测,这会导致 NaN
等于其自身,而 -0.0
不等于 0.0
。
异常
异常类
Kotlin 中所有异常类都是 Throwable
类的子孙类。 每个异常都有消息、堆栈回溯信息和可选的原因。
使用 throw-表达式来抛出异常:
1 | throw MyException("Hi There!") |
使用 try-表达式来捕获异常:
1 | try { |
可以有零到多个 catch 块。finally 块可以省略。 但是 catch 和 finally 块至少应该存在一个。
Try 是一个表达式
try 是一个表达式,即它可以有一个返回值:
1 | val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null } |
try-表达式的返回值是 try 块中的最后一个表达式或者是(所有)catch 块中的最后一个表达式。 finally 块中的内容不会影响表达式的结果。
受检的异常
Kotlin 没有受检的异常。这其中有很多原因,但我们会提供一个简单的例子。
以下是 JDK 中 StringBuilder
类实现的一个示例接口:
1 | Appendable append(CharSequence csq) throws IOException; |
这个签名是什么意思? 它是说,每次我追加一个字符串到一些东西(一个 StringBuilder
、某种日志、一个控制台等)上时我就必须捕获那些 IOException
。 为什么?因为它可能正在执行 IO 操作(Writer
也实现了 Appendable
)…… 所以它导致这种代码随处可见的出现:
1 | try { |
这并不好,参见《Effective Java》 第 65 条:不要忽略异常。
Bruce Eckel 在《Java 是否需要受检的异常?》(Does Java need Checked Exceptions?) 中指出:
通过一些小程序测试得出的结论是异常规范会同时提高开发者的生产力和代码质量,但是大型软件项目的经验表明一个不同的结论——生产力降低、代码质量很少或没有提高。
其他相关引证:
- 《Java 的受检异常是一个错误》(Java’s checked exceptions were a mistake)(Rod Waldhoff)
- 《受检异常的烦恼》(The Trouble with Checked Exceptions)(Anders Hejlsberg)
注意:throw
表达式的类型是特殊类型 Nothing
。参见Nothing类型
反射
反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。 Kotlin 让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。
在 Java 平台上,使用反射功能所需的运行时组件作为单独的 JAR 文件(
kotlin-reflect.jar
)分发。这样做是为了减少不使用反射功能的应用程序所需的运行时库的大小。如果你需要使用反射,请确保该 .jar文件添加到项目的 classpath 中。
类引用
最基本的反射功能是获取 Kotlin 类的运行时引用。要获取对静态已知的 Kotlin 类的引用,可以使用 类字面值 语法:
1 | val c = MyClass::class |
该引用是 KClass 类型的值。
请注意,Kotlin 类引用与 Java 类引用不同。要获得 Java 类引用, 请在 KClass
实例上使用 .java
属性。
绑定的类引用(自 1.1 起)
通过使用对象作为接收者,可以用相同的 ::class
语法获取指定对象的类的引用:
1 | val widget: Widget = …… |
你可以获取对象的精确类的引用,例如 GoodWidget
或 BadWidget
,尽管接收者表达式的类型是 Widget
。
函数引用
当我们有一个命名函数声明如下:
1 | fun isOdd(x: Int) = x % 2 != 0 |
我们可以很容易地直接调用它(isOdd(5)
),但是我们也可以把它作为一个值传递。例如传给另一个函数。 为此,我们使用 ::
操作符:
1 | val numbers = listOf(1, 2, 3) |
这里 ::isOdd
是函数类型 (Int) -> Boolean
的一个值。
当上下文中已知函数期望的类型时,::
可以用于重载函数。 例如:
1 | fun isOdd(x: Int) = x % 2 != 0 |
或者,你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文:
1 | val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String) |
如果我们需要使用类的成员函数或扩展函数,它需要是限定的。 例如 String::toCharArray
为类型 String
提供了一个扩展函数:String.() -> CharArray
。
示例:函数组合
考虑以下函数:
1 | fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C { |
它返回一个传给它的两个函数的组合:compose(f, g) = f(g(*))
。 现在,你可以将其应用于可调用引用:
1 | fun length(s: String) = s.length |
属性引用
要把属性作为 Kotlin中 的一等对象来访问,我们也可以使用 ::
运算符:
1 | var x = 1 |
表达式 ::x
求值为 KProperty<Int>
类型的属性对象,它允许我们使用 get()
读取它的值,或者使用 name
属性来获取属性名。更多信息请参见关于 KProperty
类的文档。
对于可变属性,例如 var y = 1
,::y
返回 KMutableProperty
类型的一个值, 该类型有一个 set()
方法。
属性引用可以用在不需要参数的函数处:
1 | val strs = listOf("a", "bc", "def") |
要访问属于类的成员的属性,我们这样限定它:
1 | class A(val p: Int) |
对于扩展属性:
1 | val String.lastChar: Char |
与 Java 反射的互操作性
在Java平台上,标准库包含反射类的扩展,它提供了与 Java 反射对象之间映射(参见 kotlin.reflect.jvm
包)。 例如,要查找一个用作 Kotlin 属性 getter 的 幕后字段或 Java方法,可以这样写:
1 | import kotlin.reflect.jvm.* |
要获得对应于 Java 类的 Kotlin 类,请使用 .kotlin
扩展属性:
1 | fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin |
构造函数引用
构造函数可以像方法和属性那样引用。他们可以用于期待这样的函数类型对象的任何地方:它与该构造函数接受相同参数并且返回相应类型的对象。 通过使用 ::
操作符并添加类名来引用构造函数。考虑下面的函数, 它期待一个无参并返回 Foo
类型的函数参数:
1 | class Foo |
使用 ::Foo
,类 Foo 的零参数构造函数,我们可以这样简单地调用它:
1 | function(::Foo) |
绑定的函数与属性引用(自 1.1 起)
你可以引用特定对象的实例方法。
1 | val numberRegex = "\\d+".toRegex() |
取代直接调用方法 matches
的是我们存储其引用。 这样的引用会绑定到其接收者上。 它可以直接调用(如上例所示)或者用于任何期待一个函数类型表达式的时候:
1 | val strings = listOf("abc", "124", "a70") |
比较绑定的类型和相应的未绑定类型的引用。 绑定的可调用引用有其接收者“附加”到其上,因此接收者的类型不再是参数:
1 | val isNumber: (CharSequence) -> Boolean = numberRegex::matches |
属性引用也可以绑定:
1 | val prop = "abc"::length |
类型别名
类型别名为现有类型提供替代名称。 如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。可以为函数类型提供另外的别名,也可以为内部类和嵌套类创建新名称
类型别名不会引入新类型。 它们等效于相应的底层类型。 当你在代码中添加 typealias Predicate<T>
并使用 Predicate<Int>
时,Kotlin 编译器总是把它扩展为 (Int) -> Boolean
。
1 | fun main(args: Array<String>) { |