Kotlin 入门

前言

  • 2011年7月,JetBrains 推出 Kotlin 项目
  • 2012年2月,JetBrains 以 Apache 2.0 许可证开源此项目,github 地址 kotlin
  • 2016年2月,Kotlin v1.0(第一个官方稳定版本)发布
  • 2017 Google I/O 大会上,Kotlin 正式成为 Android 开发官方语言

什么是Kotlin

Kotlin 是由 JetBrains 开发团队设计的基于 JVM 的静态型别编程语言

  • JetBrains开发团队: 众所周知,他们开发的 Intellij IDEA 是开发 Java 最优秀的 ide。可能是 Java 的局限性迫使他们吸收各个语言的优点来创造出这门语言。IDEA(包括Android Studio)对 Kotlin 的支持是非常全面完善的,甚至对 Eclipse 也开发了插件兼容

  • 基于JVM: Kotlin 同 Java 一样,是编译成字节码,基于 Java Virtual Machine(Java 虚拟机)运行的语言。Kotlin 能做到和 Java 混合编译,这也意味着 Java 丰富的第三方库能被 Kotlin 无条件继承

  • 静态语言: 静态类型语言是在运行前编译时检查类型,而不是在运行期间检查数据的类型。Kotlin 必须清楚定义数据类型,否则无法编译通过,同 Java 一样,Kotlin 是强静态语言,类型转换必须显式表达

为什么我们要使用 Kotlin

我们可以在这个网站 https://fabiomsr.github.io/from-java-to-kotlin 上看到 Kotlin 和 Java 的基本语法比较,很明显 Kotlin 拥有比 Java 更简洁的语法

  • 相比较 Java,Kotlin 增加了许多特性,比如 When 表达式、扩展函数、高级类、委托、Java 8 之前没有的 Lambda 表达式、高阶函数、协程… 每一个都让人兴奋不已

  • 相比较 Java,Kotlin 去除了一些特性,比如 final 关键字、静态变量、静态方法等

  • 相比较 Java,Kotlin 封装了许多过程和表示方法,实现许多语法糖,比如解构声明、操作符重载、空安全… 可以大幅减少代码,提高效率

列举一下 Kotlin 纯语法上的的好处

  • 开源:开源的好处其实也不用多说,其带来的庞大的社区及活跃的用户会使这门语言充满了活力,会涌现出一大片优秀的项目和开源库

  • Java 的兼容性:Kotlin 的设计之初就考虑到了对 Java 代码的兼容性,现在版本基本上可以实现 100% 的代码兼容性,这意味着使用 kotlin 开发的项目可以无缝调用已有的 Java库和代码,设置可以再一个项目中使用 Java 和 Kotlin 混合编译。当然作为一个 IDE 开发公司,Intellij IDEA 和 Android Studio 对 Kotlin 的支持非常完善,设置可以一键转换代码

  • 多平台开发:JetBrains 开发 Kotlin 不仅仅想要取代 Java (Android开发 —— 官方宣布 Kotlin 正式成为 Android 开发的语言,Web开发 —— 对 Spring 框架的支持,以及可以编译生成 JavaScript 模块),更远大的目标使实现多平台的统一,甚至可以进行 Native 开发,基于 LLVM 底层虚拟机的实现,Kotlin 可以为各个平台编写原生应用,在不久的将来可以看到 iOS 开发也有 Kotlin 的一席之地

  • 简洁安全:Kotlin 的入门相当简单,当然你有 Java 基础的话,你可以十分清晰的感受到这门语言在 Java 的基础上做了多少优化提升,非常值得一试,语法的定义与苹果推出的 Swift 有些类似,http://nilhcem.com/swift-is-like-kotlin 上可以看到两者的比较

当然后面也会介绍到一小部分 Kotlin 的特性

kotlin 的语法糖

安全性

空安全

Java 的 NullPointException 一直以来都是导致程序崩溃的头号杀手。kotlin 则通过类型系统旨在代码中消除潜在的空指针安全问题。
在 kotlin 中类型系统会区分一个引用可以容纳 null (可空引用)还是不能容纳(非空引用),通过在类名后添加后缀 实现

1
2
3
var str1:String = "abc"
// str1 = null
// String是非空引用,不可以容纳 null,编译器会报错

如果需要允许赋值为空,那么我们需要声明该变量为可空引用,即 Srting?

1
2
var str2: String? = "abc"
str2 = null // 编译通过

对于前者,因为是非空引用,我们可以直接调用对象属性而不必考虑空指针的情形

1
val l = a.length

而对于后者,因为是可空引用,我们就不能直接去调用对象属性,因为很有可能会导致空指针的出现,因此 kotlin 直接杜绝了这种不安全的调用方式的出现

1
2
// val l2 = str2.length
// 编译错误:变量 str2 可能为空

安全调用操作符 ?.

针对可空引用,kotlin 提供了专门的空安全调用操作符 ?.

1
2
3
// var l2: Int = str2?.length
// 编译错误:变量 l2 可能为空
var l2: Int? = str2?.length

如果变量 b 为 null,那么返回的也将是 null, 反之则会返回 b.length 的数值,所以我们得到的结果类型也是将一个可空引用 Int?
这个操作符将在链式调用时发挥巨大的作用

1
a?.b?.c?.d // 这种形式的链式调用很有可能在一些复杂的数据结构中有用到

当链式调用的任一节点为 null 都会中断链子返回 null,而在 Java 代码中

1
2
3
4
5
6
7
8
if(a != null) {
if(b != null) {
if(c != null) {
return c.d;
}
}
}
return null;

这样的写法即不简洁也不美观,而且最重要的一点是部分开发者并不会想到这么做
当然要只对非空值执行某个操作,安全调用操作符可以与 let 一起使用

1
2
val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
   item?.let { println(it) } // 输出 A 并忽略了 null 
}

Elvis 操作符 ?:

当一个可空引用 b 需要如下操作,当 b 不为空时,我们使用 b,否则我们需要使用一个非空的默认值 c 时,我们就可以使用该操作符

1
val l = b?.length ?: -1 // 相当于 val l: Int = if (b != null) b.length else -1

如果 ?: 左侧表达式非空,就返回其左侧表达式,否则返回右侧表达式,并且只有当左侧的表达式为空时才会对右侧的表达式求值

可能导致空指针的操作符 !!

对于一个可空引用的变量 b 来说,使用该操作符相当于返回一个非空引用的变量 b!!,可以直接使用对象的属性,当然如果 b 为 null,那么就会抛出 NullPointException 了

1
val l = b!!.length // 虽然 b 是可空类型,但是可以调用获取 length 属性,但是可能会抛出空指针异常

在 kotlin 中除非是你显示的要求 NullPointException:

  • 显式调用 throw NullPointerException()
  • 使用 !! 操作符

否则它不会不期而至,当然还是有两个导致空指针异常的原因:

  • 外部 Java 代码导致
  • 对于初始化,有一些数据不一致(如一个未初始化的 this 用于构造函数的某个地方)

类型转换安全

如果你检查类型是正确的,编译器会为你做自动类型转换,kotlin 通过 is 操作符及其否定形式 !is 来检查对象是否符合给定的类型,相当于 Java 中的 instanceof 关键字

同时我们可以省略显式的转换操作,因为编辑器跟踪不可变值的 is 检查,并在需要是自动插入安全的转换

1
2
3
4
5
6
7
8
9
10
11
12
if(x is String){
println(x.length)// x会被编译器自动转化为 Kotlin.String 类型,在编译器上会有高亮提示
}
//安全转换也可对反向检查智能判定
if(x !is String){
return
}
println(x.length)

//安全转换还可以实现对 && 和 || 操作符的兼容
if(x is String && x.length > 0) return//对于 && 后面数据类型自动转换
if(x !is String || x.length == 0) return//对于 || 后面数据类型自动转换

kotlin 也有显式的转换操作符 as,如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException

1
2
3
4
//类型不匹配,会抛出异常
val x: String = y as String
//上述代码中,如果 y 为空,也会抛出异常,所以必须对空安全兼容
val x: String? = y as String?

当然,这种不安全的调用形式 kotlin 并不提倡,类似于空安全操作符,kotlin 提供了一种安全的转换操作符 as?

1
val x:String? = y as? String //如果类型不匹配,则返回 null,所以 x 必须是可空变量,此时强转类型就不必是可空类型了

简洁性

高级类

Kotlin 提供了许多高级方法类,目的就是为了简化编程复杂度

data class 数据类

Kotlin 将 Java 中专门用于存放数据的类用 data 关键字标记

1
data class User(val name: String, val age: Int)

数据类会由编译器自动从主构造函数中声明的所有属性导出以下成员

  • equals()/hashCode()

  • toString() 格式是 “User(name=John, age=42)”

  • componentN() 解构声明 按声明顺序对应于所有属性

    1
    2
    val name = person.component1()
    val age = person.component2()

componentN() 函数运用于解构声明,一种类似于 python 里元组的操作方式,每个对应的属性都有一个对应的函数,按顺序叠加,是 kotlin 广泛使用的约定原则

1
  val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // 输出 "Jane, 35 years of age"

在上述代码中,name 和 age 是调用 component1() 和 component2() 的返回值

  • copy() 函数

复制函数应用于快速生成只有部分属性不同的相似对象,其实现类似于

1
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

我们可以实现

1
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

当然,这种方式是浅拷贝

object 单例修饰关键字

单例类可以由关键字 object 修饰实现,直接调用即可
顺带一提,Kotlin 并没有提供静态方法和属性,我们需要通过伴生对象 companion object 实现静态调用

enum class 枚举类和 sealed class 密封类

枚举类(enum class)与 Java 中的枚举差距不大,密封类(sealed class)在某种程度上是枚举类的一种扩展,和枚举一样,密封类的值是有限集中的类型、而不能有任何其他类型,只是每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例

Type.extension 函数扩展和属性扩展

扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式。这通过叫做扩展的特殊声明完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 扩展方法不可以覆盖原有方法
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
// 扩展方法调用
val list = mutableListOf(1, 2, 3)
list.swap(0, 1)
println(list)
// 输出结果: [2, 1, 3]


// 扩展属性不可以设值操作
val Int.isOdd: Boolean
get() = this and 1 == 1
// 扩展属性调用
val n = 3
println(n.isOdd)
// 输出结果: true

对于传统的工具类完全可以是用扩展的方法代替,而且 ide 提供了智能联想功能,Android 开发中的 kotlin-android-extensions 库也是利用了这个原理

操作符重载

可以先从相等性说起,Kotlin 中有两种类型的相等性:

  • 引用相等(两个引用指向同一对象)

引用相等由 === (以及其否定形式 !== )操作判断。 a === b 当且仅当 a 和 b 指向同 一个对象时求值为 true

  • 结构相等(用 equals() 检查)

结构相等由 == (以及其否定形式 !=)操作判断。

1
2
a == b 相当于 a?.equals(b) ?: (b === null)
// 因为 == 可能会出现 null == null 的情况,所以会出现额外的空判断

当 a == null 这种判断会被直接翻译成 a === null

Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示(如 + 或 *)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或扩展函数。 重载操作符的函数需要用 operator 修饰符标记

比如说一元的操作符 ++ ,a++ 和 ++a 都可以翻译为 a.inc(),当然也保留了前后缀的区别。
后缀 a++ 的过程是

  • 把 a 的初始值存储到临时存储 a0 中
  • 把 a.inc() 结果赋值给 a
  • 把 a0 作为表达式的结果返回

而前缀 ++a 则是

  • 把 a.inc() 结果赋值给 a ,
  • 把 a 的新值作为表达式结果返回。

可见 kotlin 对特定操作符有做额外的判断,不仅仅是调用重载方法,就比如说对 == 操作符的 null 判断的额外处理

二元操作符 +,a + b 类似于 a.plus(b) …

这种重载方法在对象中以 operator 修饰。当然,如果对象没有实现重载方法怎么办?当然是可以实现函数扩展啊,只要记得带上 operator

在这里还有更多更详细的关于操作符重载的知识
https://www.kotlincn.net/docs/reference/operator-overloading.html

委托

类委托

委托模式是在 Android 源码中广泛使用的模式,认为是实现继承的一个很好的代替模式,当无法或不想直接访问某个对象时就可以通过一个代理对象来间接访问。Kotlin 可以零样板代码地原生支持 它。 类 Derived 可以继承一个接口 Base ,并将其所有共有的方法委托给一个指定的对象:

1
2
3
4
5
6
7
interface Base {
    fun print() 
}
class BaseImpl(val x: Int) : Base { override fun print() { print(x) } }
class Derived(b: Base) : Base by b
fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 输出 10 }

by 关键字声明了 Derived 中将存储 b 变量,并且编译器将会把所有的 Base 接口方法都转发到 b 中执行

属性委托

Kotlin 同时支持属性的委托

语法:val/var <property name>: <Type> by <expression>

  • val/var:属性类型(可变/只读)
  • name:属性名称
  • Type:属性的数据类型
  • expression:代理类

by 关键字后面的表达式是具体代理类, 因为属性对应的 getter ( 对于可变属性来说还有 setter 方法 ) 会被委托给它的 getValue() 和 setValue() 方法。 属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数( 和 setValue() )。 例如:

1
2
3
4
5
6
7
class Example {
    var p: String by Delegate()
}


class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" }
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name} in $thisRef.'") }
}

如此的话,p 变量的赋值与取值都会被代理了。
当我们从委托到一个 Delegate 实例的 p 读取时,将调用 Delegate 中的 getValue() 函 数, 所以它第一个参数是读出 p 的对象、第二个参数保存了对 p 自身的描述 (例如你可以取它的名字)

1
2
3
4
val e = Example()
println(e.p)

输出结果:
Example@33a17727, thank you for delegating ‘p’ to me!

类似地,当我们给 p 赋值时,将调用 setValue() 函数。前两个参数相同,第三个参数保存 将要被赋 的值

1
2
3
4
e.p = "NEW"

输出结果:
NEW has been assigned to ‘p’ in Example@33a17727.

当然 kotlin 中标准库中包含了可以实现包含所需 operator 方法的 ReadOnlyProperty(val) 或 ReadWriteProperty(var) 接口,我们只需要继承实现对应方法即可

1
2
interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> { operator fun getValue(thisRef: R, property: KProperty<*>): T operator fun setValue(thisRef: R, property: KProperty<*>, value: T) }

另外 kotlin 标准库提供了几种常用方法的工厂方法

  • 延迟属性(lazy properties): 其值只在首次访问时计算
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

用过的人都说“真TM爽”

如果你想从 Java 的角度去看 kotlin 的语法糖,那么你可以通过以下方法:

在 Intellij IDEA 或者 Android Studio 中选择 .kt 文件,然后通过 tools -> Kotlin -> Show Koltin ByteCode 弹窗查看编译后的字节码,你可以通过 Decompile 按钮查看 Java 代码

kotlin 语言中文站
Kotlin 学习之路