Kotlin 语言入门

语言简介

编程语言大致可以分为两类:编译型语言和解释型语言。

编译型语言的特点是编译器会将我们编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,像 C/C++ 都属于编译型语言。

解释型语言的特点是它有一个解释器,在程序运行时,解释器会一行行地读取我们编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,因此解释型语言通常效率会差一些,像 Python 和 JavaScript 都属于解释性语言。

Java 属于解释性语言。Java 代码编译之后生成的并不是计算机可识别的二进制文件,而是一种特殊的 class 文件,这种 class 文件只有 Java 虚拟机(Android 中叫 ART,一种移动优化版的虚拟机)才能识别,而这个 Java 虚拟机担当的其实就是解释器的角色,它会在程序运行时将编译后的 class 文件解释成计算机可识别的二进制数据后再执行。

Kotlin 的工作原理也是基于 Java 虚拟机之上的,因为 JVM 识别的是符合规格的 class 文件,所以 Kotlin 只需要用自己的编译器将代码编译成同样规格的 class 文件即可。

Kotlin 的优势在于语法更加简洁,代码量更少,语法更加高级,安全性提高(比如几乎杜绝了空指针),并且与 Java 完全兼容

Kotlin 语言文件格式为 .kt


变量和函数

1
2
3
4
5
6
7
8
9
package com.example.myhelper

/**
* File 文件通常是用来编写 Kotlin 顶层函数和扩展函数的。
* man 函数,程序的入口函数。
*/
fun main(){
println("Hello Kotlin")
}

变量

因为 Kotlin 拥有出色的类型推导机制,所以只有两种关键字:val 和 var。

  • val(value 的简写):用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值。
  • var(variable 的简写):用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 区别于 Java,末尾不需要加分号
fun main(){
val a = 10
println("a = " + a)

// 一些情况下,比如说对一个变量延迟赋值时,类型推导机制则无法正常工作,需要显式地声明变量类型
// Int 代表一个类,它有自己的方法和继承结构,是对象数据类型。不同于 Java 的 int 为基本数据类型,int 是关键字。
val b : Int = 20
println("b = " + b)
}

// 可为 null
// var ss:String?=null
// 不能 null,必须引用类型
// lateinit var str:String
// 懒加载
// val str:String by lazy("by lazy string")

在 Java 中,一个好的编程习惯是,除非一个变量明确允许被修改,否则都应该给它加上 final 关键字。而在 Kotlin 中,可以永远优先使用 val 来声明一个变量,而当 val 没办法满足需求时再使用 var。

函数

函数是用来运行代码的载体。对于函数(function)和方法(method),是同一个概念。只不过 Java 中方法的叫法更普遍一些,Kotlin 中函数的叫法更普遍一些。

1
2
3
4
5
6
7
8
// fun(function 的简写),定义函数的关键字。
// param 参数名,可以没有。
// 最后的 Int 代表该函数会返回什么类型的数据。可以没有,代表不返回数据。
fun methodName(param1: Int,param2: Int): Int{
// 函数体
// AS 的代码补全,会自动导包。
return max(param1,param2)
}

语法糖

1
2
3
4
5
// 语法糖
// 当函数中只有一行代码时,可不必写函数体,直接写在函数定义的尾部,用等号连接
fun largerNumber(num1: Int,num2: Int): Int = max(num1,num2)
// 因为类型推导机制,所以可不必显示地声明返回值类型。
fun largerNumber2(num1: Int,num2: Int) = max(num1,num2)

逻辑控制

Kotlin 中的条件语句主要有两种实现方式:if 和 when。

if 条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun largerNumber3(num1: Int,num2: Int): Int {
// 相较于 Java,这里的 if 语句是可以有返回值的。
// 因为不涉及重新赋值的情况,可以用 val 关键字。
// val value = if (num1 > num2){
// num1
// }else{
// num2
// }
// return value

return if (num1 > num2){
num1
}else{
num2
}
}

fun largerNumber4(num1: Int,num2: Int): Int = if (num1 > num2){
num1
}else{
num2
}

fun largerNumber5(num1: Int,num2: Int): Int = if (num1 > num2) num1 else num2

when 语句

类似于 Java 中的 switch 语句,但要更灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
fun getScore(name: String) = if (name == "Tom"){
86
}else if (name == "Jim"){
77
}else if (name == "Jack"){
95
}else if (name == "Lily"){
100
}else{
0
}

/**
* when 语句允许传入任意类型的参数。
* 然后在结构体中定义一系列的条件,格式是:
* 匹配值 -> { 执行逻辑 }
* 当执行逻辑只有一行代码时,{} 可以省略。
*/
fun getScore2(name: String) = when(name){
"Tom" -> 86
"Jim" -> 77
"Jack" -> 95
"Lily" -> 100
else -> 0
}

/**
* 除了精确匹配之外,还允许类型匹配
* is 关键字相当于 Java 中的 instanceof 关键字
*/
fun checkNumber(num: Number){
when(num){
is Int -> println("number is Int")
is Double -> println("number is Double")
else -> println("number is support")
}
}

/**
* when 不带参数的用法,不常用。
*/
fun checkNumber(name: String) = when{
// 所有以 Tom 开头的,都是 86
name.startsWith("Tom") -> 86
// Kotlin 中判断字符串或对象是否相等,可直接使用 == 关键字
name == "Jim" -> 77
name == "Jack" -> 95
name == "Lily" -> 100
else -> 0
}

循环语句

Kotlin 中提供了 while 和 for 循环。while 的使用与 Java 一致。(while 的条件判断中不能执行赋值操作?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fun main(){
// for-in 循环
// 区间的概念:.. 是创建两端闭区间的关键字
// var range = 0..10 ,表示 0 到 10 的闭区间。
// 打印出来的结果:0,1,2,3,4,5,6,7,8,9,10
for (i in 0..10){
// sout 快速输入 println()
println(i)
}

// until 表示 左闭右开 的区间
// 更加常用,比如长度为 10 的数组,它的下标区间范围是 0 到 9.
// 打印出来的结果:0,1,2,3,4,5,6,7,8,9
for (i in 0 until 10){
println(i)
}

// step 关键字:每次执行循环都会在区间范围内递增 2
// 相当于 Java for-i 循环中 i = i + 2 的效果
// 打印出来的结果:0,2,4,6,8
for (i in 0 until 10 step 2){
println(i)
}

// downTo:降序的两端闭区间
// 打印出来的结果:10,9,8,7,6,5,4,3,2,1,0
for (i in 10 downTo 0){
println(i)
}
}

面向对象编程

不同于面向过程的语言(比如 C 语言),面向对象的语言是可以用来创建类的。类就是对事物的一种封装,万物皆对象,都可被封装。类中又包括类名(通常名词),字段(通常名词),函数(通常动词)等,代表这个类的名称,拥有的属性,和有哪些行为等。

类与对象

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
/**
* 因为需要在创建对象之后再指定具体的值,所以使用 var 关键字。
* 使用 val 关键字之后,初始化后就不能再重新赋值了。
*/
var name = ""
var age = 0

fun eat(){
println(name + " is eating。He is " + age + " years old。")
}

}
1
2
3
4
5
6
7
8
9
fun main(){
// Kotlin 本着最简化的设计原则,诸如 new、行尾分号这种不必要的语法结构都取消了。
// 将实例化后的类赋值到了 p 这个变量上面
// p 就可以称为 Person 类的一个实例,也可以称为一个对象。
val p = Person()
p.name = "Jack"
p.age = 19
p.eat()
}

继承

1
2
3
4
5
6
7
8
9
/**
* Kotlin 中任何一个非抽象类默认都是不可以被继承的,原因和 val 关键字差不多。
* 抽象类本身无法创建实例,一定要由子类去继承它才能创建实例,因此必须可被继承才有意义。
*
* open 关键字则代表这个类是专门为继承而设计的。
*/
open class Person {

}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 类的继承有两点:
* 1.Person 类加 open 关键字
* 2.Student 类使用冒号 : 继承
*
* () 涉及了主构造函数和次构造函数等方面知识
*/
class Student : Person() {

var sno = ""
var grade = 0
}

构造函数

Kotlin 将构造函数分成了两种:主构造函数、次构造函数。

每个类默认会有一个不带参数的主构造函数,主构造函数的特点是没有函数体,直接定义在类名的后面。

1
2
3
4
5
6
7
class Student(val sno: String,val grade: Int) : Person() {

// 先构造函数,后 init 函数。
init {
// 构造函数的逻辑
}
}
1
2
3
fun main(){
val student = Student("a123",5)
}

涉及 Java 中继承特性的一个规定,子类中的构造函数必须调用父类中的构造函数。

在 Kotlin 中也要遵守,用括号()来指定调用父类的哪个构造函数。比如 : Person() 指的是调用父类 Persion 的无参构造函数。

1
2
3
open class Person(val name:String,val age:Int) {

}
1
2
3
4
5
6
7
8
9
/**
* 这里的 name 和 age 没有用关键字声明。
* 因为主构造函数中声明 val 或者 var 的参数将自动成为该类的字段,
* 这就会导致和父类中同名的 name 和 age 字段造成冲突。
* 因此这里不用加任何关键字,让它的作用域仅限定在主构造函数当中即可。
*/
class Student(val sno: String,val grade: Int,name:String,age:Int) : Person(name,age) {

}
1
2
3
fun main(){
val student = Student("a123",5,"Jack",19)
}

次构造函数几乎用不到,Kotlin 提供了一个给函数设定参数默认值的功能,基本可以替代次构造函数的作用。

任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,与主构造函数的区别在于它是有函数体的。

Kotlin 规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* constructor 关键字定义次构造函数
*/
class Student(val sno: String,val grade: Int,name:String,age:Int) : Person(name,age) {

/**
* 通过 this 关键字调用了主构造函数
* 并将 sno 和 grade 两个参数赋值成初始值
*/
constructor(name: String,age: Int):this("",0,name,age){

}

/**
* 通过 this 关键字调用了上面定义的第一个次构造函数
* 并将 name 和 age 也赋值成初始值
* 由于这个次构造函数间接调用了主构造函数,也是合法的次构造函数。
*/
constructor():this("",0){

}
}
1
2
3
4
5
fun main(){
val student1 = Student()
val student2 = Student("Jack",19)
val student3 = Student("a123",5,"Jack",19)
}

一种非常特殊的情况,类中只有次构造函数,没有主构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 当一个类没有显式地定义主构造函数,且定义了次构造函数时,它就是没有主构造函数的。
* 而且,既然没有主构造函数了,继承 Person 类时也就不需要再加上括号了。
*/
class Student : Person {

/**
* 由于没有主构造函数,次构造函数只能直接调用父类的构造函数,
* 所以将 this 关键字换成了 super 关键字
*/
constructor(name:String,age:Int):super(name,age){

}
}

接口

接口是用于实现多态编程的重要组成部分,如 Java 一样,Kotlin 类也是单继承,多接口结构。

可以在接口中定义一系列的抽象行为,然后由具体的类去实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 接口中的函数不要求有函数体
*/
interface Study {

fun readBooks()
fun doHomework()

/**
* 对接口中定义的函数进行默认实现
* 如果接口中的函数拥有了函数体,这个函数体中的内容就是它的默认实现。
* 此函数不会被具体类强制实现,可自由选择,不实现则自动使用默认的实现逻辑。
*/
fun eat(){
println("eat")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Kotlin 中继承和接口统一用冒号,中间用逗号分隔。
* 接口的后面不用加括号,因为它没有构造函数可以去调用。
*/
class Student(name:String, age:Int) : Person(name,age), Study {
/**
* 使用 override 关键字重写父类或实现接口中的函数。
*/
override fun readBooks() {
println(name + "is reading.")
}

override fun doHomework() {
println(name + "is doing homework.")
}

}
1
2
3
4
5
6
7
8
9
10
11
12
fun main(){
val student = Student("Jack",19)
// 多态编程特性
// 由于 Student 类实现了 Study 接口,因此 Student 类的实例可以传递给这个函数。
doStudy(student)
}

fun doStudy(study: Study){
study.readBooks()
study.doHomework()
study.eat()
}

函数的可见性修饰符

修饰符 Java Kotlin
public 所有类可见 所有类可见(默认)
private 当前类可见 当前类可见
protected 当前类、子类、同一包路径下的类可见 当前类、子类可见
default 同一包路径下的类可见(默认)
internal 同一模块中的类可见

数据类

在类前面声明 data 关键字,表明这个类是一个数据类。Kotlin 会根据主构造函数中的参数将 equals()、hashCode()、toString()等固定且无实际逻辑意义的方法自动生成。

注:equals() 用于判断两个数据类是否相等。hashCode() 作为 equals() 的配套方法,也需要重写,否则会导致 HashMap、HashSet 等 hash 相关的系统类无法正常工作。toString() 用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。

1
2
3
4
/**
* 当类中没有任何代码时,可将尾部的大括号省略。
*/
data class Cellphone(val brand:String,val price:Double)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main(){
val cellphone1 = Cellphone("Samsuing",1299.99)
val cellphone2 = Cellphone("Samsuing",1299.99)
println(cellphone1)
println("cellphone1 equals cellphone2 "+(cellphone1 == cellphone2))

// 有 data 关键字的结果:
// Cellphone(brand=Samsuing, price=1299.99)
// cellphone1 equals cellphone2 true

// 没有 data 关键字的结果:
// com.example.myhelper.Cellphone@5e2de80c
// cellphone1 equals cellphone2 false
}

单例类

1
2
3
4
5
6
7
8
9
10
11
/**
* 在 Kotlin 中,只需要将 class 换成 object 关键字。
* 这样,一个单例类就完成了。
*/
object Singleton {

fun singletonTest(){
println("singletonTest is called")
}

}
1
2
3
4
5
fun main(){
// 调用单例类中的函数
// 看似像是静态方法的调用,但其实是背后自动帮我们创建了一个实例,并保证全局唯一。
Singleton.singletonTest()
}

Lambda 编程

集合的创建与遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
fun main(){
// 关于 List 的用法:
// 比较繁琐的方式
val list = ArrayList<String>()
list.add("Apple")
list.add("Orange")

// Kotlin 中专门提供了一个内置的 listOf() 函数来简化初始化集合的写法
// listOf() 函数创建的是一个不可变的集合,也就是只能用于读取。

val list2 = listOf("Apple","Orange")

// 使用 for-in 循环遍历集合
for (fruit in list2){
println(fruit)
}

// mutableListOf() 函数创建的是可变的集合
val list3 = mutableListOf("Apple","Banana")
list3.add("Pear")
for (fruit in list3){
println(fruit)
}


// 关于 Set 集合,为 SetOf() 函数,和 mutableSetOf() 函数。
// Set 集合底层是使用 hash 映射机制来存放数据的,因此集合中的元素无法保证有序。


// 关于 Map 的用法:
// 广泛一点,Map 这种键值对数据结构也可算是集合。
// 传统的用法
val map = HashMap<String,Int>()
map.put("Apple",1)
map.put("Pear",2)
// Kotlin 中不建议使用 put() 和 get() 操作数据,更加推荐类似数组下标的语法结构。
map["Banana"] = 3
// 读取数据
val number = map["Banana"]
println(number)

// Kotlin 中提供了更加简单的函数
// 这里的 to 并不是关键字,而是一个 infix 函数。
val map2 = mapOf("Apple" to 1,"Banana" to 2)
// 将 Map 的键值对变量声明到了一对括号里
for ((fruit,number) in map2){
println("fruit is " + fruit + ", number is " + number)
}
}

集合的函数式 API

集合的函数式 API 是用来入门 Lambda 编程的绝佳示例。

直白的讲,Lambda 就是一小段可以作为参数传递的代码,Kotlin 对代码量并没有进行限制,但通常不建议在表达式中编写太长的代码,否则可能会影响代码的可读性。Lambda 表达式最完整的语法结构定义如下:

{ 参数 1:参数类型,参数 2:参数类型 -> 函数体}

最外层是一个大括号,如果有参数传入到表达式中的话,还需要声明参数列表,参数列表的结尾使用一个 -> 符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为表达式的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main(){
// Lambda 表达式最完整的语法结构
// { 参数 1:参数类型,参数 2:参数类型 -> 函数体}
val list2 = listOf("Apple","Orange")
val lambda = {fruit: String -> fruit.length}
val maxLengthFruit3 = list2.maxBy(lambda)
// 简化一:不必专门定义一个变量
val maxLengthFruit4 = list2.maxBy({fruit: String -> fruit.length})
// 简化二:Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可将表达式移到函数括号的外面
val maxLengthFruit5 = list2.maxBy(){fruit: String -> fruit.length}
// 简化三:如果 Lambda 参数是函数的唯一参数,可将函数的括号省略。
val maxLengthFruit6 = list2.maxBy{fruit: String -> fruit.length}
// 简化四:由于类型推导机制,表达式中的参数列表在大多情况下不必声明参数类型。
val maxLengthFruit7 = list2.maxBy{fruit -> fruit.length}
// 简化五:当表达式的参数列表中只有一个参数时,也不必声明参数名,可用 it 关键字代替。
val maxLengthFruit8 = list2.maxBy{it.length}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
fun main(){
// 找到集合中单词最长的
val list = listOf("Apple","Orange","Banana","Pear","Grape","Watermelon")
var maxLengthFruit = ""
for (fruit in list) {
if (fruit.length > maxLengthFruit.length){
maxLengthFruit = fruit
}
}
println("max length fruit is " + maxLengthFruit)

// 使用集合的函数式 API 来简化代码
// maxBy() 是一个普通的函数,接收的是一个 Lambda 类型的参数,
// 并且会在遍历集合时将每次遍历的值作为参数传递 Lambda 表达式。
// maxBy() 的工作原理是根据传入的条件来遍历集合,从而找到该条件下的最大值。
val maxLengthFruit2 = list.maxBy { it.length }
println("max length fruit is " + maxLengthFruit)

// map 函数是最常用的一种函数式 API
// 它用于将集合中的每个元素都映射成另外的值,
// 映射的规则在 Lambda 表示中指定,最终生成一个新的集合。
// 比如变成大写
val newList = list.map { it.toUpperCase() }
for (fruit in newList){
println(fruit)
}
println("------")

// filter 函数用于过滤集合中数据,可单独使用,也可配合 map 函数一起使用。
// 注意函数的调用顺序,先过滤再映射,会提高效率。
// 只保留长度在 5 以内的
val newList2 = list.filter { it.length <= 5 }.map { it.toUpperCase() }
for (fruit in newList2){
println(fruit)
}
println("------")

// any 函数用于判断集合中是否至少存在一个元素满足指定条件
// all 函数用于判断集合中是否所有元素都满足指定条件
val anyResult = list.any{it.length <= 5}
val allResult = list.all{it.length <= 5}
println("anyResult is " + anyResult + ",allResult is " + allResult)

//......
}

Java 函数式 API 的使用
在Kotlin 中调用 Java 方法时也可以使用函数式 API,只不过是有一定限制的。

在 kotlin 代码中调用 Java 方法,该方法如果只接收一个 Java 单抽象方法接口参数,就可以使用函数式 API。单抽象方法接口指的是接口中只有一个待实现的方法,比如 Runnable 接口,其中只有一个待实现的 run() 方法。 所以对于任何一个 Java 方法,只要它接收 Runnable 参数,就可以使用函数式 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main(){
// Kotlin 中使用匿名类的方式,创建并执行一个子线程。
Thread(object : Runnable{
override fun run() {
println("Thread is running")
}
}).start()

// 函数式 API 的简化写法
// 因为 Runnable 类中只有一个待实现的方法,所以这里即使不显式的重写 run() 也可以
Thread(Runnable { println("Thread is running") }).start()
// 如果一个 Java 方法的参数列表中不存在一个以上 Java 单抽象方法接口参数,可将接口名省略。
// 勘误:如果一个 Java 方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将接口名进行省略
Thread({ println("Thread is running")}).start()
// 当 Lambda 表达式是方法的最后一个参数时,可将表达式移到括号外,
// 同时,表达式还是方法的唯一一个参数,还可将括号省略
Thread{ println("Thread is running")}.start()
}

空指针检查

Kotlin 利用编译时判空检查的机制几乎杜绝了空指针异常。

可空类型系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main(){
doStudy(null)
}

/**
* Kotlin 默认所有的参数和变量都不可为空
* 所以这里传入的 Study 参数也一定不会为空
*
* 如果需要为空的参数或变量,就在类名的后面加个问号 ?
*/
fun doStudy(study: Study?) {
// 因为 Study 可为空了,所以需要处理
if (study != null){
study.readBooks()
study.doHomework()
}
}

判空辅助工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
fun doStudy(study: Study?) {
// 使用问号 ? 操作符
// 表示当对象不为空时正常调用相应的方法,当对象为空时则什么都不做。
study?.readBooks()
study?.doHomework()

// 使用 ?: 操作符
// 此操作符两边都接收一个表达式,如果左边表达式的结果不为空就返回左边的结果,
// 否则就返回右边表达式的结果。
getTextLength(null)

// 非空断言工具,在对象后面加上 !!
// 指非常确定这里对象不会为空,不需做空指针检查。
// 因为有些情况下,即使从逻辑上已经处理好了空指针异常,但编译器并不知道。
// 使用需谨慎。
study!!.readBooks()

// 使用 let,它是一个函数。
// 这里调用了 obj 对象的 let 函数,然后 Lambda 表达式中的代码会立即执行,
// 并且这个 obj 对象本身还会作为参数传递到表达式中。这里的 obj 和 obj2 实际上是同一个对象。
study.let { stu ->
stu.readBooks()
stu.doHomework()
}
// 当表达式的参数列表中只有一个参数时,可不用声明参数名,直接用 it 关键字代替。
// 并且 let 函数可以处理全局变量的判空问题,
// 而 if 不可以,因为全局变量的值随时可能被其他线程所修改。
study.let {
it.readBooks()
it.doHomework()
}
}

/**
* 获取一段文本长度
*/
fun getTextLength(text:String?) = text?.length ?:0

a.b(), a?.b(), a!!.b() 区别?

在Kotlin中定义变量如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Temp {
var a: A? = null
val a2: A = A()

fun test() {
a?.b()
}

fun test2() {
a!!.b()
}

fun test3(){
a2.b()
}
}

反编译后结果如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public final class Temp {
@Nullable
private A a;
@NotNull
private final A a2 = new A();

@Nullable
public final A getA() {
return this.a;
}

public final void setA(@Nullable A var1) {
this.a = var1;
}

@NotNull
public final A getA2() {
return this.a2;
}

public final void test() {
A var10000 = this.a;
if (var10000 != null) {
var10000.b();
}
}

public final void test2() {
A var10000 = this.a;
Intrinsics.checkNotNull(var10000);
var10000.b();
}

public final void test3() {
this.a2.b();
}
}

kotlin的可空与非可空,在字节码上是同一种类型,判断只是在对象调用层面,而不是类型层面。主要就是通过@Nullable@NotNull注解来区分可空与不可空。

a?.b()在a对象为可空时,不会编译错误,但是在a对象为空时代码并不会执行;a!!.b()在a对象为可空时,不会编译错误,但是在a对象为空时会抛出异常;a.b()在a对象为可空时,会编译错误。(如果是自定义类型,则是在a.b() 之前会调用一次 checkNull)


备注

参考资料

第一行代码(第3版)
官方文档
官方中文翻译站

传送门GitHub

欢迎关注微信公众号:非也缘也