kotlin基础知识

参考:https://www.jianshu.com/p/769cbb6eba38

1. Kotlin 语言简介

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

  • 编译型:编译器将编写的源代码一次性地编译成计算机可识别的二进制文件,然后计算机直接执行,如 C、C++。
  • 解释型:程序运行时,解释器会一行行读取编写的源代码,然后实时地将这些源代码解释成计算机可识别的二进制数据后再执行,如 Python、JavaScript(解释型语言效率会差些)。

Java 先编译再运行,但 Java 代码编译之后生成的是 class 文件,只有 Java 虚拟机才能识别,Java 虚拟机将编译后的 class 文件解释成二进制数据后再执行,因而 Java 属于解释型语言。

Kotlin 也是通过编译器编译成 class 文件,从而 Java 虚拟机可以识别。

2. 变量和函数

2.1 变量

Kotlin 定义一个变量,只允许在变量前声明两种关键字:valvar

val (value的简写):声明不可变的变量,对应 java 中的 final 变量。

var (variable的简写):声明可变的变量,对应 java 中的非 final变量。

使用技巧:编程时优先使用 val 来声明变量,当 va l无法满足需求时再使用 var

2.2 函数

fun (function的简写) 是定义函数的关键字,如定义个 返回两个数中较大的数 的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main() {
val a = 10
val b = 20
val value = largerNum(a, b)
print("large number is $value")
}

fun largerNum(num1: Int, num2: Int): Int {
return max(num1, num2)
}

当函数中只有一行代码时,可以不写函数体,用等号连接即可,return也可以省略,等号足以表达返回的意思
fun largerNum(num1: Int, num2: Int): Int = max(num1, num2)
由于类型推导机制,max函数返回的是Int值,所以可以不用显式的声明返回值的类型
fun largerNum(num1: Int, num2: Int) = max(num1, num2)

3. 程序的逻辑控制

程序的执行语句主要分 3 种:顺序条件循环语句

  • 条件:ifwhen
  • 循环语句:whilefor

3.1 if

Kotlin 中的 if 语句和 java 中 if 语句没啥区别,如下:

1
2
3
4
5
6
7
8
9
fun largerNum(num1: Int, num2: Int): Int {
var value = 0
if (num1 > num2) {
value = num1
} else {
value = num2
}
return value
}

==不过 Kotlin 中 if 语句可以有返回值==,返回值是 if 语句每一个条件中最后一行代码的返回值。上述函数可以简化如下:

1
2
3
4
5
6
7
8
fun largerNum(num1: Int, num2: Int): Int {
val value = if (num1 > num2) {
num1
} else {
num2
}
return value
}

if 语句直接返回,继续简化:

1
2
3
4
5
6
7
fun largerNum(num1: Int, num2: Int): Int {
return if (num1 > num2) {
num1
} else {
num2
}
}

当一个函数只有一行代码时,可以省略函数体部分,直接将这一行代码使用等号串连在函数定义的尾部。上述函数和一行代码的作用是相同的,从而可以进一步精简:

1
2
3
4
5
fun largerNum(num1: Int, num2: Int): Int = if (num1 > num2) {
num1
} else {
num2
}

当然也可以直接压缩成一行代码:

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

3.2 when

Kotlin 中的 when 语句有点类似于 java 中的 switch 语句,但强大得多。

if 语句实现个 输入学生名字返回该学生的分数 的函数如下:

1
2
3
4
5
6
7
8
9
fun getScore(name: String) = if (name == "Wonderful") {
100
} else if (name == "Tome") {
86
} else if (name == "Jack") {
60
} else {
0
}

when 语句允许传入一个任意类型的参数,然后可以在 when 的结构体中定义一系列的条件,格式是:

匹配值 -> { 执行逻辑 },当执行逻辑只有一行代码时,{}可以省略:匹配值->执行逻辑

1
2
3
4
5
6
fun getScore(name: String) = when (name) {
"Wonderful" -> 100
"Tome" -> 86
"Jack" -> 60
else -> 0
}

在某些场景,比如 所有名字以Won开头的学生分数都是100分,则上述函数可以用不带参数的 when 语句实现:

1
2
3
4
5
6
fun getScore(name: String) = when {
name.startsWith("Won") -> 100
name == "Tome" -> 86
name == "Jack" -> 60
else -> 0
}

注:when语句不带参数的用法不太常用

除此之外,when 语句还可以进行类型匹配,如:

1
2
3
4
5
6
7
fun checkNumber(num: Number) {
when (num) {
is Int -> print("整数") // is 关键字相当于 Java 中的 instanceof 关键字
is Double -> print("Double")
else -> print("number not support")
}
}

3.3 循环

whilejava相似,而for有很大的差别

Java 中常用的 for-i 循环在 Kotlin 中被舍弃了,Java 中的 for-each 循环在 Kotlin 中变成了 for-in 循环。

Kotlin 用 .. 创建闭区间,用 until 关键字创建左闭右开的区间,如:

1
2
val range = 0..10 // 数学中的[0, 10]
val range = 0 until 10 // 数学中的[0, 10)

Kotlin 中 for 循环用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() {
// 遍历[0, 10]中的每一个元素
for (i in 0..10){
println(i)
}
// 遍历[0, 10)的时候,每次循环会在区间范围内递增2,相当于 for-i 中的 i = i + 2 效果
// step 关键字可以跳过其中一些元素
for (i in 0 until 10 step 2){
println(i)
}
// 降序遍历[0, 10]中的每一个元素
// downTo 关键字用来创建降序的空间
for (i in 10 downTo 1){
println(i)
}
}

4. 面向对象编程

:是对事物的一种封装,而面向对象编程最基本的思想就是通过这种类的封装,在适当的时候创建该类的对象,然后调用对象中的字段和函数来满足实际编程的需求。

4.1 类和对象

在kotlin中,使用class来创建类,下面为Person类:

1
2
3
4
5
6
7
class Person{
var name = ""
var age = 0
fun eat(){
println("$name is eating,He is $age years old")
}
}

而对象在kotlin中的创建方式,省略了new,如下:

1
2
3
4
5
6
fun main{
var p = Person()
p.name = zlw
p.age = 25
p.eat()
}

4.2 继承与构造函数

对于kotlin中的继承和实现使用:,与java相同的是,只能继承一个父类,但可以实现多个接口

1
2
3
4
class student:Person(){
var son = ""
var grade = 0
}

如果要让 Student 类继承 Person 类,使 Person 类可以被继承(注:Kotlin 中任何一个非抽象类默认是不可被继承的),在 Person 类前面加上关键字 open

1
2
3
4
5
6
7
open class Person {
var name = ""
var age = 0
fun eat(){
println("$name is eating. He is $age years old")
}
}

++++++++++++++

Kotlin 的构造函数有两种:主构造函数次构造函数

主构造函数 没有函数体,每个类默认会有一个不带参数的主构造函数,也可以在类名后面直接定义来显式指明参数,如:

1
class Student(val sno: String, val grade: Int) : Person() { }

这样,实例化 Student 类时需要传入构造函数中的参数:

1
val student = Student("no123", 6) // 学号 no123,年级 6

如果想在实例化类时在主构造函数中实现一些逻辑,则可以将逻辑写在 Kotlin 提供的 init 结构体中:

1
2
3
4
5
6
7
class Student(val sno: String, val grade: Int) : Person() {
init {
// 实例化时打印学号和年级
println("sno is $sno")
println("grade is $grade")
}
}

子类的主构造函数调用父类中的哪个构造函数,在继承时通过括号来指定。

Person 类的姓名和年龄放到主构造函数中,如下:

1
2
3
4
5
open class Person(val name: String, val age: Int) {
fun eat() {
println("$name is eating. He is $age years old")
}
}

此时,Person 类已经没有无参构造函数了,Student 类要继承 Person 类也要在主构造函数中加上姓名和年龄这两个参数,如下:

1
2
3
4
5
6
7
8
// 这边增加的 name 和 age 字段不能声明成 val,因为在主构造函数中声明 val 或 var 的参数会将自动
// 成为该类的字段,这会导致和父类中同名的字段冲突
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
init {
println("sno is $sno")
println("grade is $grade")
}
}

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

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

次构造函数 是通过 constructor 关键字来定义的,如定义 Student 类的次构造函数如下:

1
2
3
4
5
6
7
8
9
10
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {
init {
println("sno is $sno")
println("grade is $grade")
}

constructor(name: String, age: Int) : this("", 0, name, age){ }

constructor() : this("", 0){ }
}

此时就可以有 3 种方式来实例化 Student 类:

1
2
3
val student1 = Student()
val student2 = Student("Wonderful", 18)
val student3 = Student("no123", 6, "Wonderful", 18)

还有种特殊情况,类中只有次构造函数,没有主构造函数(当一个类没有显式定义主构造函数且定义了次构造函数时,它就是没有主构造函数的),此时继承类时就不需要再加上括号了,如下:

1
2
3
class SpecialStudent : Person {
constructor(name: String, age: Int) : super(name, age) { }
}

4.3 接口

接口是用于实现多态编程的重要组成部分,Kotlin 和 Java 一样也是一个类只能继承一个父类,却可以实现多个接口。

定义个 Study 接口,接口中的函数不要求有函数体,如下:

1
2
3
4
interface Study {
fun readBooks()
fun doHomework()
}

在 Kotlin 中,统一用冒号,中间用逗号分隔,来继承类或实现接口,如在 Student 类中实现 Study 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age), Study {
init {
println("sno is $sno")
println("grade is $grade")
}

override fun readBooks() {
println("$name is reading")
}

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

constructor(name: String, age: Int) : this("", 0, name, age){ }

constructor() : this("", 0){ }
}

mian() 中调用这两个接口函数如下:

1
2
3
4
5
6
7
8
9
fun main() {
val student = Student("no123", 6, "Wonderful", 18)
doStudy(student)
}

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

上面由于 Student 类实现了 Study 接口,从而可以把 Student 类的实例传递给 doStudy 函数,这种面向接口编程也可以称为多态。

Kotlin 还允许对接口中定义的函数进行默认实现,如:

1
2
3
4
5
6
7
8
// 当一个类实现 Sduty 接口时,只会强制要求实现 readBooks() 函数,
// 而 doHomework() 函数可以自由选择是否实现
interface Study {
fun readBooks()
fun doHomework() {
println("do homework default implementation")
}
}

4.4 数据类与单例类

在 Java 中数据类通常需要重写 equals()hashCode()toString() 几个方法,如果用 Kotlin 只需在数据类前面声明关键字 data 就可以了,如下:

1
data class CellPhone(val brand: String, val price: Double)

kotlin中的单例模式:

1
object Singleton { }

5. lambda编程

5.1 集合的创建与遍历

一般的集合主要就是 ListSetMapList 的主要实现类是 ArrayListLinkedListSet 的主要实现类是 HashSetMap 的主要实现类是 HashMap

传统写法:

1
2
3
4
val list = ArrayList<String>()
list.add("apple")
list.add("orange")
list.add("pear")

上面这种方式比较繁琐,Kotlin 专门提供了一个内置的 listOf() 函数来简化初始化集合的写法,如下:

1
val list = listOf("apple", "orange", "pear")

不过 listOf() 函数创建的是一个不可变集合,创建可变集合用 mutableListOf() 函数。

Set 集合也差不多,将创建集合的方式变成 setOf()mutableSetOf() 函数而已。

注:和 List 集合不同的是,Set 集合底层使用 hash 映射机制来存放数据,因而集合中的元素无法保证有序。

Map 集合创建一个包含许多水果名称和对应编号的集合,传统的写法如下:

1
2
3
4
val map = HashMap<String, Int>()
map.put("apple", 1)
map.put("orange", 2)
map.put("pear", 3)

但在 Kotlin 中不建议用 put()get() 方法来对 Map 进行数据操作,而推荐使用一种类似于数组下标的语法结构,如添加 map["apple] = 1,读取 val number = map["apple"],因此上面代码可改为:

1
2
3
4
val map = HashMap<String, Int>()
map["apple"] = 1
map["orange"] = 2
map["pear"] = 3

或者使用 mapOf()mutableMapOf() 来简化:

1
val map = mapOf("apple" to 1, "orange" to 2, "pear" to 3)

5.2 集合的函数式 API

要在一个水果集合里找到单词最长的那个水果,可以用如下代码实现:

1
2
3
4
5
6
7
8
val list = listOf("apple", "orange", "pear")
var maxLengthFruit = ""
for (fruit in list){
if (fruit.length > maxLengthFruit.length){
maxLengthFruit = fruit
}
}
println("max length fruit is $maxLengthFruit")

但如果使用集合的函数式 API,就可以简化为:

1
2
3
val list = listOf("apple", "orange", "pear")
val maxLengthFruit = list.maxBy { it.length }
println("max length fruit is $maxLengthFruit")

Lambda 就是一小段可以作为参数传递的代码,它的语法结构如下:

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

最外层是一对大括号,若有参数传入到 Lambda 表达式,需要声明参数列表,参数列表结尾用符号 -> 表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码会自动作为返回值。

多数情况下我们写的更多的是简化的写法,以上面例子为例:

1
2
3
val list = listOf("apple", "orange", "pear")
val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy(lambda) // maxBy 函数实质上是接收了一个 Lambda 参数

由于可以直接将 lambda 表达式传入 maxBy 函数中,因此可简化为:

1
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })

Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可将 Lambda 表达式移到函数括号外面,如下:

1
val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }

如果 Lambda 参数是函数的唯一一个参数的话,可将函数的括号省略:

1
val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }

由于 Kotlin 拥有类型推导机制,Lambda 表达式中的参数列表大多数情况下可不必声明参数类型,从而进一步简化为:

1
val maxLengthFruit = list.maxBy { fruit -> fruit.length }

最后,当 Lambda 表达式的参数列表只有一个参数时,也不必声明参数名,可用 it 关键字代替:

1
val maxLengthFruit = list.maxBy { it.length }

++++++++++++

下面介绍几个比较常用的API

  • map 函数

集合中的 map 函数用于将集合中的每个元素都映射成一个另外的值,映射的规则在 Lambda 表达式中指定,最终生成一个新的集合。

如把所有水果名变成大写:

1
2
val list = listOf("apple", "orange", "pear")
val newList = list.map { it.toUpperCase(Locale.ROOT) } // 新的列表水果名都是大写的
  • filter 函数

filter 函数是用来过滤集合中的数据的,可单独使用,也可配合 map 一起使用。

如只保留 5 个字母以内的水果且所有水果名大写:

1
2
val list = listOf("apple", "orange", "pear")
val newList = list.filter { it.length <= 5 }.map { it.toUpperCase(Locale.ROOT) }

注:上面若改成先调用 map 再调用 filter 函数,效率会差很多,因为这相当于对集合的所有元素进行一次映射转换后再过滤。

  • any 和 all 函数

any 函数用于判断集合中是否至少存在一个元素满足指定条件。

all 函数用于判断集合中是否所有元素都满足指定条件。

1
2
3
4
val list = listOf("apple", "orange", "pear")
val anyResult = list.any { it.length <= 5 } // 集合中是否存在5个字母以内的单词,返回 true
val allResult = list.all { it.length <= 5 } // 集合中是否所有单词都在5个字母内,返回 false
println("anyResult is $anyResult , allResult is $allResult")

5.3 Java 函数式 API 使用

在 Kotlin 代码中调用 Java 方法,==若该方法接收一个 Java 单抽象方法接口(接口中只有一个待实现方法)参数,就可以使用函数式 API。==

如 Java 原生 API 中的 Runnable 接口就是一个单抽象方法接口:

1
2
3
4
public interface Runnable {
// 这个接口中只有一个待实现的 run() 方法
void run();
}

以 Java 的线程类 Thread 为例,Thread 类的构造方法中接收一个 Runnable 参数,Java 代码创建并执行一个子线程:

1
2
3
4
5
6
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running");
}
}).start();

上面代码用 Kotlin 实现如下:

1
2
3
4
5
Thread(object : Runnable {
override fun run() {
println("Thread is running")
}
}).start()

上面 Thread 类的构造方法是符合 Java 函数式 API 使用条件的,因此可简化为:

1
Thread(Runnable { println("Thread is running") }).start()

若一个 Java 方法的参数列表只有唯一一个单抽象方法接口参数,可把接口名省略:

1
Thread({ println("Thread is running") }).start()

当 Lambda 表达式是方法的最后一个参数时,可把它移到方法括号外面,同时如果它还是方法的唯一一个参数,可把方法的括号省略:

1
Thread { println("Thread is running") }.start()

注:以上 Java 函数式 API 的使用都限定与从 Kotlin 中调用 Java 方法,并且单抽象方法接口也必须是 Java 语言定义的。

6. 空指针检查

  • 如果希望传入的参数可为空,Kotlin 中在类名后面加一个问号就可以了,比如 Int 表示不可为空的整型,而 Int? 就表示可为空的整形。

    1
    2
    3
    4
    5
    6
    fun doStudy(study: Study?) {
    if (study != null) {
    study.readBooks()
    study.doHomework()
    }
    }
  • ?.

    操作符 ?. 的作用是当对象不为空时正常调用相应的方法,为空时则什么都不做。

    1
    2
    3
    4
    fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomework()
    }
  • ?:

    操作符 ?: 的左右两边都接收一个表达式,若左边表达式的结果不为空则返回左边表达式的结果,否则返回右边表达式的结果

    1
    val c = a ?: b
  • !!

    操作符 !! 的作用是告诉 Kotlin 非常确信对象不会为空,不需要 Kotlin 帮忙做空指针检查,若出现问题再直接抛出空指针异常。

    1
    2
    3
    4
    fun printUpperCase(){
    val upperCase = content!!.toUpperCase(Locale.ROOT)
    println(upperCase)
    }

7. let函数

let 函数提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中,如下:

1
2
3
obj.let { obj2 ->  // 这里的 obj2 和 obj 是同一个对象
// 编写具体的业务逻辑
}

let 函数属于 Kotlin 中的标准函数,可以处理全局变量的判空问题(if 判断语句无法做到这一点),它配合操作符 ?. 可以在做空指针检查时起到很大作用。

如上述的 doStudy() 函数代码可用 let 函数进行优化,如下:

1
2
3
4
5
6
7
fun doStudy(study: Study?) {
// study 对象不为空时就调用 let 函数,let 函数会将 study 对象本身作为参数传递到 Lambda 表达式中
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}

当 Lambda 表达式的参数列表只有一个参数时,可不声明参数名,用 it 关键字代替即可,从而可简化为:

1
2
3
4
5
6
fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}

8. Kotlin 中的小技巧

8.1 字符串内嵌表达式

Kotlin 允许在字符串里嵌入 ${} 这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容,大大提升了易读性和易用性:

1
"hello, ${obj.name}, nice to meet you"

当表达式仅有一个变量时,可将两边的大括号省略:

1
"hello, $name, nice to meet you"

举个例子:

1
2
3
val name = "Wonderful"
val age = 18
println("My name is " + name + ", " + age + "years old")

用字符串内嵌表达式的写法可简化为:

1
2
3
val name = "Wonderful"
val age = 18
println("My name is $name, $age years old")

8.2 函数的参数默认值

Kotlin 中,定义函数时给任意参数设定一个默认值,调用时就不会强制为此参数传值,在此参数没传值的情况下使用设定的默认值。如:

1
2
3
4
// 这里给第二个参数 str 设定了个默认值 “hello”
fun printParams(num: Int, str: String = "hello"){
println("num is $num , str is $str")
}

这样调用时可不用给第二个参数传值,如printParams(123)。但如果改成给第一个参数设定默认值的话:

1
2
3
4
// 这里给第一个参数 num 设定了个默认值 100
fun printParams(num: Int = 100, str: String){
println("num is $num , str is $str")
}

此时再调用诸如 printParams("world") 就会报类型匹配错误了,这时需要通过键值对的方式来传参,从而不必按照参数定义的顺序来传参,如 printParams(str = "world")

给函数设定参数默认值这个功能,使得主构造函数很大程度上替代了次构造函数,从而次构造函数比较少使用到。

9.标准函数 with、run 和 apply

Kotlin 的标准函数指的是 Standard.kt 文件中定义的函数,任何 Kotlin 代码都可以自由的调用所有的标准函数。

  • with 函数

with 函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个 Lambda 表达式。

with 函数会在 Lambda 表达式中提供第一个参数对象的上下文,并使用 Lambda 表达式中的最后一行代码作为返回值返回。示例代码如下:

1
2
3
4
val result = with(obj){
// 这里是obj的上下文
"value" // with 函数的返回值
}

with 函数可以在连续调用同一个对象的多个方法时让代码变得更加精简。

  • run 函数

run 函数的用法和使用场景和 with 函数非常类似,只是语法有些不同。

run 函数不能直接调用,一定要调用某个对象的 run 函数才行。

run 函数只接收一个 Lambda 参数,并且会在 Lambda 表达式中提供调用对象的上下文,用最后一行代码作为返回值。示例代码如下:

1
2
3
4
val result = obj.run{
// 这里是obj的上下文
"value" // run 函数的返回值
}
  • apply 函数

apply 函数和 run 函数也极其相似,都要在某个对象上调用,并且只接收一个 Lambda 参数,会在 Lambda 表达式中提供调用对象的上下文。

不过, apply 函数无法指定返回值,而是会自动返回调用对象本身。示例代码如下:

1
2
3
4
val result = obj.apply{
// 这里是obj的上下文
}
// result == obj

举个例子,比如有一个水果列表,吃完所有水果并把结果打印出来,有如下代码:

1
2
3
4
5
6
7
8
9
val list = listOf("apple", "orange", "pear")
val builder = StringBuilder()
builder.append("开始吃水果:\n")
for (fruit in list){
builder.append(fruit).append("\n")
}
builder.append("吃完全部水果!")
val result = builder.toString()
println(result)

上面代码连续调用了很多次 builder 对象的方法,若用 withrunapply 函数可以更加精简:

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 main() {
val list = listOf("apple", "orange", "pear")

// with 函数第一个参数传入 StringBuilder 对象,
// 下面整个 Lambda 表达式的上下文都是这个 StringBuilder 对象
val withResult = with(StringBuilder()) {
append("开始吃水果:\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("吃完全部水果!")
toString()
}

// run 函数
val runResult = StringBuilder().run {
append("开始吃水果:\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("吃完全部水果!")
toString()
}

// apply 函数返回的是 StringBuilder 对象
val applyResult = StringBuilder().apply {
append("开始吃水果:\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("吃完全部水果!")
}

println(withResult)
println(runResult)
println(applyResult.toString())
}

小结:withrunapply 函数的用法和使用场景非常类似,大多数情况下可以相互转换,编程时选择合适的即可。

10.静态方法

静态方法是那种不需要创建实例就能调用的方法(某些编程语言里也叫类方法),在 Java 中定义一个静态方法只需要在方法上声明一个 static 关键字即可:

1
2
3
4
5
6
public class Util {
public static void doAction() {
// do something
...
}
}

调用静态方法无需创建类的实例,直接用 Util.doAction() 即可,因而静态方法适合编写全局通用、无需创建实例的工具类。

不过,Kotlin 却极度弱化了静态方法这个概念,它提供了比静态方法更好的语法特性:单例类

像工具类这种功能,Kotlin 推荐用单例类来实现,如上述的代码用 Kotlin 实现:

1
2
3
4
5
6
object Util {
fun doAction() {
// do something
...
}
}

使用单例类会将整个类中的所有方法变成类似静态方法的调用方式,若我们只想让类中的某一个方法变成静态方法的调用方式,则可以用 companion object,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Util {
// doAction1 方法一定要先创建 Util 类的实例才能调用
fun doAction1() {
// do something
...
}

companion object {
// doAction2 方法直接使用 Util.doAction2() 即可
fun doAction2() {
// do something
...
}
}
}

上面 doAction2() 方法并不是静态方法,关键字 companion object 会在 Util 类的内部创建一个伴生类,而 doAction2() 方法就是定义在这个伴生类里的实例方法。

当然,Kotlin 也提供了两种实现静态方法的方式:注解和顶层方法。

  • 注解

在单例类或 companion object 中的方法加上 @JvmStatic 注解,Kotlin 编译器就会将这些方法编译成真正的静态方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Util {
fun doAction1() {
println("do action1")
}

// @JvmStatic 注解只能加在单例类或者 companion object 中的方法上
companion object {
@JvmStatic
fun doAction2() {
println("do action2")
}
}
}
  • 顶层方法

顶层方法是指那些没有定义在任何类中的方法,如上面的 main() 方法。

Kotlin 编译器会把所有的顶层方法编译成静态方法。

定义顶层方法,首先要创建一个 Kotlin 文件,创建类型选择 File,比如创建了个 Helper.kt 文件,在此文件中定义的任何方法都是顶层方法:

1
2
3
4
// 在 Helper.kt 中定义了个 doSomething 顶层方法
fun doSomething() {
println("do something")
}

所有的顶层方法在 Kotlin 代码中可以在任何位置被直接调用,不用管包名路径,也不用创建实例,如调用上面定义的顶层方法直接键入 doSomething() 即可。

不过,在 Java 中没有顶层方法这个概念,所有的方法必须定义在类中。上面创建的 Kotlin 文件名是 Helper.kt,Kotlin 编译器会自动创建一个叫 HelperKt 的类,在 Java 中调用上面定义的顶层方法使用 HelperKt.doSomething() 即可。

小结:除了 @JvmStatic 注解不太常用,其他像单例类、companion object、顶层方法都是 Kotlin 中比较常用的技巧。

11.延迟初始化和密封类

(1)延迟初始化

关键字:lateinit,查看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainActivity : AppCompatActivity(), View.OnClickListener {

private var adapter: MsgAdapter? = null

override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = MsgAdapter(msgList)
...
}

override fun onClick(v: View?) {
...
adapter?.notifyItemInserted(msgList.size - 1)
...
}

}

这里将adapter设置为了全局变量,但是它的初始化工作是在onCreate()方法中进行的,因此不得不先将adapter赋值为null,同时把它的类型声明成MsgAdapter?

但是我们在onClick()方法中调用adapter的任何方法时仍然要进行判空处理才行,否则编译肯定无法通过。

如果程序中有大量这种全局变量,则必须编写大量额外的判空处理代码。因此,对全局变量进行延迟初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MainActivity : AppCompatActivity(), View.OnClickListener {

private lateinit var adapter: MsgAdapter

override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = MsgAdapter(msgList)
...
}

override fun onClick(v: View?) {
...
adapter.notifyItemInserted(msgList.size - 1)
...
}

}

使用lateinit关键字也不是没有任何风险,如果我们在adapter变量还没有初始化的情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个UninitializedPropertyAccessException异常。

还可以通过代码来判断一个全局变量是否已经完成了初始化,这样在某些时候能够有效地避免重复对某一个变量进行初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainActivity : AppCompatActivity(), View.OnClickListener {

private lateinit var adapter: MsgAdapter

override fun onCreate(savedInstanceState: Bundle?) {
...
//::adapter.isInitialized可用于判断adapter变量是否已经初始化。
//虽然语法看上去有点奇怪,但这是固定的写法
if (!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}
...
}

}

(2)密封类

密封类的关键字是sealed class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//之前不使用密封类的写法
interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result

fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error.message
else -> throw IllegalArgumentException()
}

//使用密封类的写法
sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()

fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> "Error is ${result.error.message}"
}

这是因为当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。而如果我们现在新增一个Unknown类,并也让它继承自Result,此时getResultMsg()方法就一定会报错,必须增加一个Unknown的条件分支才能让代码编译通过。

注:另外再多说一句,密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

例子:

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
//不使用密封类
class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}

inner class RightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
...
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
}

//使用密封类
class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

sealed class MsgViewHolder(view: View) : RecyclerView.ViewHolder(view)

class LeftViewHolder(view: View) : MsgViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.leftMsg)
}

class RightViewHolder(view: View) : MsgViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.rightMsg)
}
...
override fun onBindViewHolder(holder: MsgViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
}
}
}

12.扩展函数、运算符重载

(1)扩展函数

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。

定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。

统计字符串中字母的数量:

1
2
3
4
5
6
7
8
9
10
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
val count = "ABC123xyz!@#".lettersCount()

建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,这样便于你以后查找。最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。

除了String类之外,你还可以向任何类中添加扩展函数,Kotlin对此基本没有限制。如果你能利用好扩展函数这个功能,将会大幅度地提升你的代码质量和开发效率。

(2)运算符重载

运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。

例如让两个money类相加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Money(val value: Int) {

operator fun plus(money: Money): Money {
val sum = value + money.value
return Money(sum)
}

operator fun plus(newValue: Int): Money {
val sum = value + newValue
return Money(sum)
}

}

val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
val money4 = money3 + 20
println(money4.value)
下面两个是相同的意思,推荐使用第二种
if ("hello".contains("he")) {
}
if ("he" in "hello") {
}

随机生成字符串长度的函数,代码如下所示:

1
2
3
4
5
6
7
8
fun getRandomLengthString(str: String): String {
val n = (1..20).random()
val builder = StringBuilder()
repeat(n) {
builder.append(str)
}
return builder.toString()
}

使用运算符重载和扩展函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
operator fun String.times(n: Int): String {
val builder = StringBuilder()
repeat(n) {
builder.append(this)
}
return builder.toString()
}

val str = "abc" * 3
println(str)

其实Kotlin的String类中已经提供了一个用于将字符串重复n遍的repeat()函数,因此times()函数还可以进一步精简成如下形式:
operator fun String.times(n: Int) = repeat(n)
getRandomLengthString函数简化为:
fun getRandomLengthString(str: String) = str * (1..20).random()

image-20240317001558229

13.高阶函数、内联函数

(1)高阶函数

==如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。==

Kotlin又增加了一个函数类型的概念。如果我们将这种函数类型添加到一个函数的参数声明或者返回值声明当中,那么这就是一个高阶函数了。

函数类型语法:(String, Int) -> Unit

高阶函数使用:

1
2
3
fun example(func: (String, Int) -> Unit) {
func("hello", 123)
}

这里我准备定义一个叫作num1AndNum2()的高阶函数,并让它接收两个整型和一个函数类型的参数。我们会在num1AndNum2()函数中对传入的两个整型参数进行某种运算,并返回最终的运算结果,但是具体进行什么运算是由传入的函数类型参数决定的。

1
2
3
4
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}

现在高阶函数已经定义好了,那么我们该如何调用它呢?由于num1AndNum2()函数接收一个函数类型的参数,因此我们还得先定义与其函数类型相匹配的函数才行。在HigherOrderFunction.kt文件中添加如下代码:

1
2
3
4
5
6
7
fun plus(num1: Int, num2: Int): Int {
return num1 + num2
}

fun minus(num1: Int, num2: Int): Int {
return num1 - num2
}

有了上述函数之后,我们就可以调用num1AndNum2()函数了,在main()函数中编写如下代码:

1
2
3
4
5
6
7
8
fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2, ::plus)
val result2 = num1AndNum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")
}

使用这种函数引用的写法虽然能够正常工作,但是如果每次调用任何高阶函数的时候都还得先定义一个与其函数类型参数相匹配的函数,这是不是有些太复杂了?

Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用等。其中,Lambda表达式是最常见也是最普遍的高阶函数调用方式,下面这种方式就不会复杂了,plus和minus函数就不用了。

1
2
3
4
5
6
7
8
9
10
11
12
fun main() {
val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { n1, n2 ->
n1 + n2
}
val result2 = num1AndNum2(num1, num2) { n1, n2 ->
n1 - n2
}
println("result1 is $result1")
println("result2 is $result2")
}

高阶函数也可以模仿apply函数(apply函数,它可以用于给Lambda表达式提供一个指定的上下文,当需要连续调用同一个对象的多个方法时,apply函数可以让代码变得更加精简)

使用Stringbuilder举例:

1
2
3
4
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}

在函数类型的前面加上了一个StringBuilder. 的语法结构。这是什么意思呢?其实这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName. 就表示这个函数类型是定义在哪个类当中的。

那么这里将函数类型定义到StringBuilder类当中有什么好处呢?好处就是当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply函数的实现方式。

现在我们就可以使用自己创建的build函数来简化StringBuilder构建字符串的方式了。

1
2
3
4
5
6
7
8
9
10
11
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list) {
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

高阶函数可以应用:简化SharedPreferences的用法 和 简化ContentValues的用法,在这两个部分 分别有介绍

(2)内联函数

高阶函数的原理是:原来我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。

为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除。

使用:只需在定义高阶函数时加上inline关键字

内联函数的工作原理:

  • 首先,Kotlin编译器会将Lambda表达式中的代码替换到函数类型参数调用的地方:

image-20240317001438573

  • 接下来,再将内联函数中的全部代码替换到函数调用的地方:

image-20240317001449882

  • 最终的代码就被替换成了:

image-20240317001500525

如果一个高阶函数接收了两个或更多的函数参数,这时给函数加上inline关键字,那么kotlin会自动将所有引用的Lambda表达式全部内联。要想内联其中一个Lambda表达式,将不内联的函数类型前加上关键字:noinline

那么为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。

举例:可以通过内链和非内联的原理考虑下面的结果

非内联:局部返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}

fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return@printString
println(s)
println("lambda end")
}
println("main end")
}

结果:

image-20240317001522721

内联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}

fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("lambda start")
if (s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")
}

结果:

image-20240317001531892

如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。例如:

1
2
3
4
5
6
7
//内联函数所引用的表达式允许使用return关键字进行函数返回
inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()//在匿名类中调用
}
runnable.run()
}

就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。

1
2
3
4
5
6
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}

14.泛型和委托

(1)泛型

泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。

举个例子,List是一个可以存放数据的列表,但是List并没有限制我们只能存放整型数据或字符串数据,因为它没有指定一个具体的类型,而是使用泛型来实现的。也正是如此,我们才可以使用List、List之类的语法来构建具体类型的列表。

定义泛型类:

1
2
3
4
5
6
7
8
9
10
11
class MyClass<T> {

fun method(param: T): T {
return param
}

}

//调用
val myClass = MyClass<Int>()
val result = myClass.method(123)

不想定义一个泛型类,只想定义一个泛型方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {

fun <T> method(param: T): T {
return param
}

}

//调用
val myClass = MyClass()
val result = myClass.method<Int>(123)

//Kotlin还拥有非常出色的类型推导机制,例如我们传入了一个Int类型的参数,
//它能够自动推导出泛型的类型就是Int型,因此这里也可以直接省略泛型的指定:
val myClass = MyClass()
val result = myClass.method(123)

对泛型进行限制,例如只想要方法指定成数字类型(int、float、double),默认是Any?,如果不可以为空可指定Any

1
2
3
4
5
6
7
class MyClass {

fun <T : Number> method(param: T): T {
return param
}

}

对一下面这个代码进行改写,之前只能作用在StringBuilder类上面,而apply可以作用在所有类上面,用泛型进行扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//只能作用在StringBuilder类上面
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}

//泛型扩展
fun <T> T.build(block: T.() -> Unit): T {
block()
return this
}

//完全可以像使用apply函数一样去使用build函数了
contentResolver.query(uri, null, null, null, null)?.build {
while (moveToNext()) {
...
}
close()
}

(2)类委托和委托属性

Kotlin中委托使用的关键字是by

  • 类委托

将一个类的具体实现委托给另一个类去完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MySet<T>(val helperSet: HashSet<T>) : Set<T> {

override val size: Int
get() = helperSet.size

override fun contains(element: T) = helperSet.contains(element)

override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)

override fun isEmpty() = helperSet.isEmpty()

override fun iterator() = helperSet.iterator()

}

MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后在Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。

为什么不直接使用辅助对象?因为有时候我们要进行一些重写,或加入独有的方法

但这么写也有一定的弊端:如果接口中的方法少还好,但如果有几十甚至上百的话,那可真是要写哭了。

解决:使用类委托的功能来解决

1
2
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
}

就下面这简短的一行代码就满足了上面那些代码。如果我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利

1
2
3
4
5
6
7
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {

fun helloWorld() = println("Hello World")

override fun isEmpty() = false

}
  • 委托属性

将一个属性(字段)的具体实现委托给另一个类去完成

1
2
3
4
5
class MyClass {

var p by Delegate()

}

将p属性的具体实现委托给了Delegate类去完成,当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。

因此还得对Delegate类进行两个方法的具体实现才行

1
2
3
4
5
6
7
8
9
10
11
12
13
class Delegate {

var propValue: Any? = null

operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
return propValue
}

operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
propValue = value
}

}

getValue():第一个参数用于声明该Delegate类的委托功能可以在什么类中使用,第二个参数KProperty<*>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值,,<*>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已

setValue():前两个参数和getValue()方法是相同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法返回值的类型保持一致。


kotlin基础知识
http://example.com/2023/08/29/kotlin基础知识/
作者
zlw
发布于
2023年8月29日
许可协议