Kotlin Learn Note

Welcome to KLN,here are base kownledge of Kotlin.Kotlin language is the first android developer language after Google declared years ago.

写在前言

1. kotlin 概述

1.1. 程序结构
如果 Kotlin 文件包含单个类(其他的顶层声明不影响),那么文件名应该与该类的名称相同,
并用.kt 扩展名。如果文件包含多个类或只包含顶层声明,那不要求与类相同。
建议多个相关的类、顶级函数或者属性等内容放在同一个文件中。特别是为类定义与类相关
的扩展函数时,将它们放在与类自身定义相同的地方。
1.2. 程序入口点
Kotlin 应用程序的入口点是 main 函数,例如:
fun main(){
println(“Hello world!”)
}
1.3. 标签
kotlin 中任何表达式都可以用标签(label)来标记,格式:
labelname@
应用:
loop@ for( … ){}
在程序中,跳转指令就可以跳转到标签所在位置,例如:break@loop。
1.4. 编码规则
 区分大小写
 单行语句不需要分号结束,若一行有多个语句,需要用分号隔开
 字符串不能分开在两行中书写,若要分开,可以将之拆分成两个字符,用加号(+)连接
 注释:单行:// ,多行: /* / ,文档注释:/* */,且注释可以嵌套
1.5. 常用符号
? 空值处理之可空类型变量
?. 空值处理之安全调用符
?: 空值处理之 Elvis 操作符
!!. 空值处理之非空断言
! 空值处理之平台类型标记
类型声明,继承、接口
:: 函数类型之函数参数、反射
== 比较两个值大小,即 java 中的 equals 方法
=== 比较对象地址,即比较两个对象是否相同””” “”” 字符串之原生符号
$ 字符串之模板表达式

2. 基本语法概述

2.1. 包 Package
源文件通常以包声明开都,所以包的声明应处于源文件顶部,源文件的所有内容,都包含在
声明的包内。如果没有指明包,该文件的内容属于无名字的默认包。格式为:
package com.xx
有多个包会默认导入到每个 kotlin 文件中,例如:
kotlin.*
kotlin.annotation.*
kotlin.collections.*
kotlin.comparisons.*
kotlin.io.*
kotlin.ranges.*
kotlin.sequences.*
kotlin.text.*
2.2. 导入 import
除了默认导入之外,每个文件可以包含它自己的导入指令。
可以导入一个单独的名字,如:
import org.example.Message // 现在 Message 可以不用限定符访问
也可以导入一个作用域下的所有内容(包、类、对象等):
import org.example.* //“org.example”中的一切都可访问
如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义:
import org.example.Message //Message可访问
import org.test.Message as testMessage //testMessage 代表“org.test.Message”
import 并不仅限于导入类,也可用它来导入其他声明:
 顶层函数及属性;
 在对象声明中声明的函数和属性;
 枚举常量。
2.3. 注解

1) 概述
一种描述数据的数据,可以说注解就是源代码的元数据。它作用的对象是代码。它可以给特定的注解代码标注一些额外的信息。例如实现接口中的方法,都会在方法前面加上:@override
的注解,这样,此方法的特性就是一个重写的方法,必须满足一定的要求。具体如何处理注解的
含义,需要注解处理工具来执行,所以针对一些框架的注解,需要添加注解处理工具插件,例如
kotlin 中的 kapt(kotlin Annotation processing tool)。
注解以@字符作为名字的前缀,并放在要注解元素的前面。
元注解为注解的注解。元注解也是一个标签,但是它是一个特殊的标签,它的作用和目的就
是给其他普通的标签进行解释说明的。
Kotlin 元注解
 @Target
目标注解,对应的注解类是 kotlin.annotation.Target,功能是指定一个注解的适用目标
 @Retention
保留期注解 ,指定一个新注解的有效范围
 @Repeatable
可重复注解,允许在单个元素上多次使用同一个注解
 @MustBeDocumented
文档注解,该注解可以修饰代码元素(类,接口,函数,属性等)
2) 注解的应用
//注解声明:和类的声明很类似,只是在 class 前面加上了 annotation 修饰符
@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: Int)
class Test {
//给 test 函数贴上 TestAnnotation 标签(添加 TestAnnotation 注解)
@TestAnnotation(value = 1000)
fun test() {

}
}
3) 在框架中使用注解
注解是给机器看的,注释是给程序员看的,这是两者的区别。现在各大框架都在使用注解,
程序员需要做的就是知道如何使用注解。
注解的使用大体分为三部分: 定义注解、使用注解、解析注解。在框架中定义与解析框架都已
经为我们做好了,所以开发的时候定义注解以及解析注解一般不用我们写,只需要学会使用框架
提供的注解即可。
解析注解需要注解处理工具处理,例如 Room 中自动生成代码等。此时需要添加注解处理工
具插件和添加依赖。2.4. 常量
声明常量的关键字:const
声明常量的方式:
const val 常量名 = 值
const 只能修饰 val,不能修饰 var
声明常量的三种正确方式
 在顶层声明
 在 object 修饰的类中声明,在 kotlin 中称为对象声明,它相当于 Java 中一种形式的单
例类
 在伴生对象中声明
例如:
// 1. 顶层声明
const val NUM_A : String = “顶层声明”
// 2. 在 object 修饰的类中
object TestConst{
const val NUM_B = “object 修饰的类中”
}
// 3. 伴生对象中
class TestClass{
companion object {
const val NUM_C = “伴生对象中声明”
}
}
fun main(args: Array) {
println(“NUM_A: $NUM_A”)
println(“NUM_B: ${TestConst.NUM_B}”)
println(“NUM_C: ${TestClass.NUM_C}”)
}
2.5. 变量
kotlin 中,变量的声明必须使用 var 或 val 关键字。定义格式:
val/var 变量名:数据类型 = xxx
在定义时直接赋值,编译器可以推导其类型,所以可以省略类型,格式简化为:
val/var 变量名= xxx
 val(value)用此关键字声明的变量表示不可变变量,即只读变量。相当于 Java 中的 final 变量。
注意:若 val 修饰的是一个对象,例如:val myUser = User(),那表示定义了一个 myUser
的引用,指向了一个 User 对象,且一旦定义后,此引用不能再指向别的对象。但是,User 对象
本身的值是可以改变的。
 var(variable)
用此关键字声明的变量表示可变变量,即可读且可写。相当于 Java 中普通变量。
注意:kotlin 中不可以一次定义多个变量,例如:var a,b,c 这是错误的
例如:
//完整的定义格式
val a:Int=8
var var_a: Int = 10
//自动推断出 Int 类型
val b=2
var var_b = 5
//如果没有初始值,类型不能省略
val c:Int
c=3 //明确赋值,后面不能再次修改,例如: c += 1,这是错误的
var var_c: Float
var_c = 12.3f
var_c += 1 //可变变量,正确
注意:在不赋值的情况下,只声明变量,没有类型,程序会报错。因为不赋值就无法推导出
变量类型,此时就需要显示声明变量类型。例如:
var c //错误
var c:int //正确
val、var 如何选择呢?在程序设计中,优先选择 val,只有当 val 无法满足需求,再使用 var,
这样就能保证变量不会被胡乱修改,从而提高了程序的健壮性。

3. 空值处理

在程序开发过程中,经常会遇到空指针异常的问题,如果对这个问题处理不当,还可能引起
程序的崩溃(crash) ,因此在 Kotlin 中,为了避免出现空指针异常的问题,引入了 Null 机制。
3.1. 可空类型变量(?)
Kotlin 把变量分成两种类型,一种是可空类型的变量,一种是非空类型的变量。一般情况下,
一个变量默认是非空类型。当某个变量的值可以为空时,必须在声明处的数据类型后添加“?”来
标识该引用可为空。例如:
var name:String //声明非空变量
var age:Int? //声明可空变量注意:在使用可空变量时,若不赋初值,则必须要将其赋值为 null,否则会报 “variable ‘xxx’
must be initialized”异常。
例如:
package com.itheima.chapter02.variable
fun main(args: Array) {
var name: String = “Tom” //非空变量
var telephone: String? = null //可空变量
if (telephone != null) {
print(telephone.length)
} else {
telephone = “18800008888”
print(“telephone =” + telephone)
}
}
上述代码中,在使用可空变量时,必须先对可空变量进行判断,如果 telephone 不为空则输
出电话号码的长度,否则赋值为 18800008888 并输出。若不判断,直接输出,会报异常。例如:
fun main() {
var name:String?=null
var result=name.length
println(result)
}
程序会抛出异常,表示只有非空变量或者采用安全调用符才可以调用可空变量。
3.2. 安全调用符(?.)
上一节中,可空变量在使用时需要通过 if…else 语句进行判断,然后再进行相应的操作。这
样的使用方式还是比较复杂,为此 Kotlin 提供了一个安全调用符“?.” ,专门用于调用可空类
型变量中的成员方法或属性,其语法格式为“变量?.成员” 。其作用是判断变量是否为 null,如
果不为 null 才调用变量的成员方法或者属性。
例:
fun main(args: Array) {
var telephone: String? = null //可空变量
var result=telephont?.length //安全调用可空变量
println(result)
}
运行结果为 null,从运行结果可以看到,在使用“?.”调用可空变量的属性时,如果当前变量
为空,则程序编译也不会报错,而是返回一个 null 值。所以,非空变量调用是,最简单的方法就是采用安全调用符。
3.3. Elvis 操作符(?:)
在使用安全调用符调用可空变量中的成员方法或属性时,如果当前变量为空,则会返回一个
null 值,但有时即使当前变量为 null,也不想返回一个 null 值,而是指定一个默认值,通过 Elvis
操作符(?:)可以指定可空变量为 null 时, 调用该变量中的成员方法或属性的返回值, 其语法
格式为 :
表达式?: 表达式
如果左侧表达式非空,则返回左侧表达式的值,否则返回右侧表达式的值。当且仅当左侧为
空时,才会对右侧表达式求值(短路现象)。
例如:
fun main(args: Array) {
var telephone: String? = null //可空变量
var result=telephont?.length?:188000888 //telephone 为空时,使用?:操作符会返
回指定的默认值,而非 null。
println(result)
}
运行结果为:188000888
3.4. 非空断言( !!. )
除了通过使用安全调用符(?.)来使用可空类型的变量之外,还可以通过非空断言(!!.)来调
用可空类型变量的成员方法或属性。使用非空断言时,语法结构为:
变量!!.成员
非空断言(!!.)会将任何变量(可空类型变量或者非空类型变量)转换为非空类型的变量,
若该变量为空则抛出异常。例如:
fun main(args: Array) {
var telephone: String? = null //可空变量
var result=telephont!!.length //使用非空断言
println(result)
}
运行结果:
Exception in thread “main” java.lang.NullPointerException
……
从运行结果可以看出,程序抛出了空指针异常,如果变量 telephone 赋值不为空,则程序可
正常运行。
安全调用符与非空断言运算符都可以调用可空变量的方法,但是在使用时有一定的差别,如
下表所示。3.5. 平台类型标记
Java 中任何引用都可能为 null,而 Kotlin 中只有可空和非空类型,所以 Java 在转换为 Kotlin
时,对于部分无法标注的类型,会转换为 Kotlin 中的平台类型。
平台类型:其实就是 Kotlin 不知道可空性信息的类型,既可以把它当作可空类型处理,也可
以当作非空类型处理,这意味着要像在 java 中对这个数据做 null 判断处理,否则数据为 null 将
会抛出空指针异常。
平台类型无法在程序中显示标记,所以语言中没有相关的语法。不过,编译器和 IDE 有时需
要显示这些类型(错误信息,参数信息等等),所以 Kotlin 提供了“!”助记符,例如:
 T!表示 T 或 T?
 Array<(out) T>!表示类型为 T 或 T 的子类的 Java 集合,可以为空或不为空

4. 基本类型

在 kotlin 中,所有的东西都是对象,在这个意义上,可以在任何变量上调用成员函数与属性。
一些类型可以有特殊的内部表示,例如:数字,字符以及布尔值可以在运行时表示为原生类
型值。4.1. 类型推断
类型推断:编译器可以从上下文推断类型(赋予某个变量的表达式值)。
当变量声明和初始化工作一起执行时,即可省略类型声明。
4.2. 数字
Kotlin 中的数字的内置类型,其关键字为:
Byte:字节,8 位
Short:短整型,16 位
Int:整型,32 位
Long:长整型,64 位
Float:浮点型,32 位
Double:双精度浮点型,64 位

1) 字面常量
数值常量字面值有以下几种:
 十进制:123
Long 类型用大写 L标记: 123L
 十六进制: 0x0F
 二进制:0b00001011
注意: 不支持八进制
Kotlin 同样支持浮点数的常规表示方法:
 默认 double:123.5、123.5e10
 Float:用 f或者 F 标记:123.5f
另外,在表示数值是,可以使用下划线来让数字更加易读,例:val om=1_000_000
2) 整数:
定义常量时,所有未超出 Int 最大值的整型值都会推断为 Int 类型。如果初始值超过了其最大
值,那么推断为 Long 类型。如需显式指定 Long 类型值,需在该值后面加上 L 后缀。例如:
val one=1 //Int
val threeBillion 300000000 //Long
val oneLong=1L //Long
val oneByte:Byte=1 //Byte
3) 浮点数:
对于以小数初始化的变量,编译器会推断为 Double 类型。如 需 将 一 个 值 显 式 指 定 为
Float 类型,添加 f 或 F 后缀。 如果这样的值包含多于 6~7 位十进制数,那么会将其舍入。
val pi = 3.14 //Double
val e = 2.7182818284 //Double
val eFloat= 2.7182818284f //Float,实际值为 2.7182817注意,由于不同的表示方式,较小类型并不是较大类型的子类型,所以 Kotlin 不支持取值范
围小的数据类型隐式转换为取值范围大的类型。在赋值时,要求不同数值类型的变量或值之间必
须进行显式转换。例如:
//隐式转换,编译器会报错
val aInt: Int = 5
val cLong: Long = aInt
//需要去显式的转换,下面这个才是正确的
val dLong: Long = aInt.toLong()
显示转换是通过 toXxx()方法将变量或值转换为另外的类型(注意,函数转换的是变量的值拷
贝,转换后是一个新值,与原变量无关,原变量的类型是不会变的,与 C 语言是一样的概念):
toByte() :转换为 Byte
toShort():转换为 Short
toInt():转换为 Int
toLong():转换为 Long
toFloat():转换为 Float
toDouble():转换为 Double
toChar():转换为 Char
4) 表达式类型自动提升:
当一个算术表达式中包含多个数值型的值时,整个算术表达式的数据类型将发生自动提升。
(与 Java 基本相似的自动提升规则)。
所有的 Byte、Short 类型将被提升到 Int 类型,整个算术表达式的数据类型自动提升到与表
达式中最高等级操作数同样的类型。
例如:
var b: Byte = 40
var c: Short = 97
var i: Int = 23
var d: Double = .314
val result = b + c + i * d //变量的值发生类型提升,变量本身类型不变
println(result) //144.22,类型为 Double
4.3. boolean 型
用“Boolean”表示,只有两个值,分别是 true 和 false。例如:
var a:Boolean=true
var b=true4.4. 逻辑运算符:&& , || , ! (与 java 相同,略)
4.5. 字符型
用“Char”表示。每个字符类型的变量占用 2 个直接。在使用字符常量时,需要用单引号将
字符括起来,例如:
var a:Char =’A’
var b=’B’
注意,kotlin 中,Char 类型的变量不能直接赋值为数字,必须使用单引号把数字括起来才能
使用。
4.6. 字符串

1) 表示格式
用“String”表示。在使用字符串常量时,需要用双引号将字符括起来,例如:
var a: String = “Hello World!”
var a = “Hello World!”
字符串中的元素可以通过索引的形式访问,例如:a[i]。
字符串的长度可以通过属性 length 获取,例如:”abc”.length,值为 3。
2) toString()函数
kotlin 中,所有对象的基类是 Any 类,Any 类中的 toString()方法可以将其他类型的数值转
换为 String 类型。
3) 字符串常用处理函数
函数 功能描述
first()/last()/get(index) 查找字符串的第 1 个/最后一个/第 index 个元素
subString(startindex:Int) 从下标为 startindex 开始截取,直至末尾,返回截取的字符串
subString(startindex:Int,
endindex:Int)
从 startindex~ endindex-1,截取字符串
subString(range:IntRange) IntRange 表示截取范围。
subSequence(…) 与上面的 substring 一致。
replace(oldChar,newChar) oldChar 为要被替换的字符串,newChar 为新字符串
split(separator:String) 分隔字符串,例如:str.split(“.”),根据点号拆分
trim()/trimEnd()/… 删除字符串前面的空格,字符后面的空格
4) 转义字符
与 java 中一致,略。
5) 原生字符串
使用 3 对引号(””” …”””),包左右的字符括起来。原生字符可以保证字符串中原有内容
的输出,即使包含转义字符也不会被转义。6) 模板表达式$
模板表达式:在字符串中添加占位符。由:${变量名/函数/表达式} 组成,后面是变量名时,
可以省略{}(函数和表达式不行),例如:$变量名。
注意,原生字符串中,使用模板表达式输出“$”需要使用:${$}
例 1:使用模板表达式存储字符串的值
var a=1
var s1=”a is ${a}” // a is 1
var s2=”a is $a” // a is 1
例 2:使用模板表达式调用其他方法
fun helloWorld():String{
return “Hello World”
}
fun main(args:Array){
//语法格式:${方法()}
println( “${helloWorld()}” ) //{}不能少
}
例 3:使用模板表达式执行替换操作
var s2=”a is 1”
//语法格式:${表达式} ,执行表达式
var s3=”${s2.replace(“is”,”wa”)}” //s3 为:a was 1
4.7. 数组

1) 数组型
数组类型用 Array 表示,加上泛型后,表示方式为 Array
创建数组的方式:
 arrayOf():创建一个数组,参数是一个可变参数的泛型对象,此方法最常用
例如:
var int_ary: Array = arrayOf(1, 2, 3)
var str_ary:Array = arrayOf(“happy”,”new”,”year”)
注意: =之间要有空格,不然会被解释为 >=,比较运算了。
 arrayOfNulls():用于创建一个指定数据类型且可以为空元素的数组
例如:
var arr= arrayOfNulls(3) //注意,类型,数量
 Array():工厂函数
此函数有两个参数,第一个为数组元素个数,第二个为使用其元素下标(特殊变量 index)
组成的表达式,例如:var arr = Array(5,{index -> (index * 2).toString() }) //5 为元素个数,index 为下标
2) 原生类型数组
kotlin 中有专门的类来表示原生类型的数组,分别是:
 ByteArray:字节型数组
 ShortArray:短整型数组
 IntArray:整型数组
 LongArray:长整型数组
 BooleanArray:布尔型数组
 CharArray:字符型数组
 FloatArray:浮点型数组
 DoubleArray:双精度浮点型数组
对 应 这 些 原 始 类 型 , 初 始 化 数 组 时 , 它 们 也 有 相 应 的 方 法 : typeArrayOf(xxx)
(ByteArrayOf(xxx)、IntArrayOf(xxx)、……)。例如:
val arr:IntArray = intArrayOf(1,2,3)
或者采用构造函数、工厂方法,例如:
var arr=IntArray(5) //构造函数创建对象,默认初始值为 0
var arr=IntArray(5,{it *1} ) //使用 lambda 表达式初始化数组中的值
从上面的类型可以看到,原生类型只能是基础类型的数组,注意,不支持字符串等复合类型
为原始类型,即 stringArray 是错误的,必须使用 Array这样的格式。
另外,由于智能推断,在初始化数组变量时,变量的数据类型同样可以省略,例如:
var string_array = arrayOf(“Hello”, “World”, “!”)”
数组常用属性:
Array.size:获取数组长度
注意:数组中的索引不能超出索引的范围,不然程序会报错。
4.8. 区间

1) 正向区间(从小到大)
区间是数值序列的一种定义方式,并通过序列中的首、尾值表示。范围可利用两个“.”表示,
也可以通过 rangeTo(other:Int)函数,如下所示:
val intRange=1..4 //表示 1,2,3,4 这 4 个值的区间
val charRange=’b’.. ‘g’ //表示从字母 b~g 区间
i in 1.rangeTo(4) //判断 i 是否在[1,4]的范围内
kotlin 中,还有一个函数 until(to:Int),该函数输出的区间不包含结尾元素,且使用时可以省
略(),并和前面的对象之间不需要点号,在 until 后面加上空格,再后面加上范围,例如:
i =1.until( 4 ) //i 的范围是:[ 1 , 4 )
i =1 until 4注意,区间不是一个具体的值,不能将它赋值给一个 int 变量,例如语句:
var a=1.rangeTo(4)
print(a) //输出的结果是:1..4,表示是一个区间,而不是一个具体的值
in 运算符的使用:
in 和区间一起使用,表示在…区间。!in 也是运算符,正好和 in 相反,表示不在…区间。运
算的结果是一个 boolean。
另外,in 用在 for 循环中,for( 循环变量 in 循环条件 ),表示迭代循环条件中的值。
2) 逆向区间(从大到小)
函数:downTo(to:Int),该函数可以省略(),省略时,在 downTo 后面加上空格,然后加上
范围值即可。例如:
5.downTo(1) / 5 downTo 1 //i 的范围是:[5,1]
3) 步长
前面的步长默认为 1,可以使用 step(step:Int)函数来指定,step 中的()也可以省略,书写时
在 step 后面加上空格,空格后面加上步长即可。例如:
1..4 step 2 //取值为 1,3
4.9. 类型转换
类型转换是指将某一特定类型的对象转换为另一种类型。在 kotlin 中,根据转换方式的不同,
数据类型转换分为两种:智能类型转换和强制类型转换。
与“数字”章节中的强制类型转换不同,这里的转换是变量类型的转换,而不是值拷贝的转
换,注意,类型必须是能够转换的,例如子类转换为父类,或者类型推断后为同一个类型。

1) 智能类型转换
is 和 !is 操作符,通过这两个操作符,能判断出当前对象是否属于 is 或者 !is 后面的数据类
型(返回 boolean)。如果当前对象属于 is 后面的数据类型,则在使用该对象时可以自动进行智
能类型转换。
例如:
var a:Any=”hello”
if( a is String ){ //a 自动转换为 String 类型,且返回 true
println( “字符串长度”+a.length )
} else{
println(“I don’t konw”)
}
上述代码中,定义了一个 Any 类型变量 a,当通过“is”操作符进行判断时,编辑器可以判
断变量 a 中实际上存储的是一个 String 类型的数据,会自动将 a 转换为 String 类型。
2) 强制类型转换
 将对象转换为不同类型;或者将可空类型隐式的转换为非空类型(智能转换机制)as 非安全型转换,转换无法实现时,抛出 ClassCastException 异常,例如:
var a=”1” //定义变量 a,没有指定具体类型,但可以智能推导出 String 类型
var b:String = a as String //将变量 a 强制转换为 String 类型
而如果 a 定义为其他类型,程序将会抛出异常,例如:
var a=1 //定义变量 a,智能推导出 Int 类型
var b:String = a as String //将 int 变量 a 强制转换为 String 类型,抛出异常,指出这个
类型转换不会成功
as? 安全型转换,该操作符表示转换无法实现时,返回 null,不会抛出异常。例如:
var a=1 //定义变量 a,智能推导出 Int 类型
var b:String? = a as? String //b 的值为 null,转换无法实现
var c: Float? = a as? Float //c 的值为 null,Int 无法转换为 Float

5. 运算符

与 Java 中一致,略

6. 控制流

6.1. if 语句
格式:
 if(条件) 语句/{语句块}
 if(条件)-else
另外还有:if(条件)? a else b ,相当于 java 中的三元运算符:条件? 表达式 1:表达式 2
 if-else if-else
注意,如果将 if 作为表达式(返回值或者将它赋给变量),该表达式需要有 else 分支。
6.2. when
when 用于代替 switch 操作符,格式如下:
when(表达式){
目标值 1->{语句 1}

目标值 n->{语句 n}
else ->{
语句 n+1
}
}
when 语句将表达式的值与每个目标值进行匹配,如果找到匹配的值,会执行相应“->”后
面的语句(若只有一条语句,{}可以省略),执行完后自动 break。若没有匹配值,会执行 else 后面
的语句。when 既可以被当做表达式使用,也可以当做语句使用。若当成表达式使用,符合条件的分
支的值就是整个表达式的值,若当成语句使用,则忽略个别分支的值。
分支条件不仅仅局限于常量,可以是任意表达式,例如:
 多个分支放在一起,用逗号分隔
0,1->print(“xxx”)
 任意表达式
parseInt(s)->print(“xxx”)
 一个值在(in)或不在(!in)一个区间或集合中
when(x){
in 1..10 ->print(“xxx”) //x 的值是在[1,10]之间则匹配
 一个值是(is)或不是(!is)一个特定类型的值
when(x){
is String->print(“xxx”) //x 是 String 类型的值则匹配
注意,如果 when 作为一个表达式使用时,必须有 else 分支。
若 when 后面没有表达式,那么所有的分支条件都是简单的布尔表达式,当一个分支条件为
真时,可执行该分支后面的语句。例如:
when{
a>b -> print(“a 大于 b”)
a<b -> print(“a 小于 b”)
else -> print(“a 等于 b”)
}
6.3. while 循环/do-while 循环(与 java 一致,略)
6.4. for 循环

1) for 循环语句
for 循环可以对任何提供了迭代器的对象进行遍历,相当于 C#中的 foreach,python 中的 for
in,语法格式如下。
for( 循环变量 in 循环条件 ) { 语句 }
循环变量不需要定义成变量形式,直接给出变量名即可,表示是循环条件中的某个变量。
所以,for 可以遍历任何提供了迭代器的对象。例如迭代一个数组。
val array=arrayOf(“a”,”b”,”c”)
for( i in array) println( i ) //分行输出 a,b,c
循环条件也可以是一个区间,若在数字区间上迭代,可使用区间表达式,例如:
for( i in 1..3 )
for( i in 6 downTo 0 step 2 )
若要遍历数组或 list 的索引(注意,不是直接取值)可以进行如下操作:val array=arrayOf(“a”,”b”,”c”)
for( i in array.indices ) println( i ) //分行输出 0,1,2
2) forEach 语句
[1] 普通的 forEach 语句
格式:
调用者.forEach(){
println(“ i = $it “)
}
调用者可以是数组或集合,it 是一个特定名称的值,代表数组或集合中的元素。表示在循环中
迭代调用者中的元素。例如:
var array =arrayOf(“a”,”b”,”c”)
array.forEach(){
println(it) //分行输出 a,b,c
println(“$it”) //注意,println($it)是错误的
}
[2] 带索引的 forEachIndexed 语句
格式:
调用者. forEachIndexed(){
index,it->println(“ 索引=$index, 元素 = $it “)
}
index 和 it 都是特定名称的值,index 代表数组/集合的索引,it 代表数组/集合索引对应的元
素。表示在循环中迭代调用者的索引和元素。
例如:
fun main() {
var array =arrayOf(“a”,”b”,”c”)
array.forEachIndexed(){
index,it->println(“索引=$index,元素=$it”)
}
}
输出:
索引=0,元素=a
索引=1,元素=b
索引=2,元素=c
6.5. break 与 continue
Kotlin 中有三种结构化的跳转表达式: return:默认从最直接包围它的函数或者匿名函数返回。
 break:终止最直接包围它的循环。
 continue:继续下一次最直接包围它的循环。
注意:在 when 中需要 break,匹配项执行完后会自动终止 when 语句。

7. 函数

7.1. 函数式编程
函数式编程是种编程方式,它将电脑运算视为函数的计算。函数编程语言最重要的基础是λ演
算(lambda calculus),而且 λ 演算的函数可以接受函数当作输入(参数)和输出(返回值)。
函数式编程具有如下特征:
 一等函数支持,函数也是一种数据类型,可以作为参数传入另一个函数中,同时函数也可
以返回一个函数。
 纯函数和不变性,纯函数指的是没有副作用的函数,即函数不去改变外部的数据状态。
 函数组合,在面向对象的编程中是通过对象之间发送消息来构建程序逻辑的。而在函数式
编程中是通过不同函数的组合来构建程序逻辑的。
7.2. 函数的定义
fun 函数名称( [参数名称:参数类型,参数名称:参数类型] ):返回值类型 {
执行语句

return 返回值
}
函数参数使用 pascal 表示法,即 name:type,每个参数必须有显式的类型。参数之间用逗号
隔开。
注意:不返回任何类型的值,定义为:Unit(无类型),类似于 Java 中的 void。当函数的返
回值类型为 Unit 时,可以省略不写 Unit。
例如:
fun sum( str:String ):Int{
……
}
函数也可以当做变量来使用,例如:
val sum=fun(x:Int,y:Int):Int{return x+y} //将一个匿名函数赋给 sum
7.3. 函数的分类

1) 顶层函数
顶层函数又称为包级别函数,可以直接放在某一个包中,而不像 Java 一样必须将函数放在
某一个类中(类似 python)。在 Kotlin 中,函数可以独立存在,例如经常用的 main()函数。顶层函数在被调用时,如果在同一个包中,可直接调用,如果在不同的包中,需要导入对应的包。
2) 成员函数
成员函数是在类或对象内部定义的函数,例如在类中定义的函数:
class 类名{
fun 函数名(){
函数体
}
}
在其他地方调用此类中的函数时,需要通过:“类的实例.成员函数()”的形式
3) 局部函数(嵌套)
局部函数又称嵌套函数,主要是在一个函数的内部定义另一个函数。语法格式如下:
fun 函数名 1(){
fun 函数名 2(){
执行语句
}
执行语句
}
局部函数可以访问外部函数的局部变量,并且在外部函数中可以调用其内部的局部函数。
7.4. 函数的参数
函数的参数分为默认参数、具名参数以及可变参数 3 种。

1) 默认参数
在定义函数时,可以给函数中的每一个形参指定一个默认的值。在调用是,当省略相应的参
数时使用默认值。语法格式如下:
fun 函数名(形参 1: 类型,形参 2: 类型= 默认值, …) {
函数体
}
例如:
fun introduce(name: String = “江小白”, age: Int) {
println(“姓名:$name”) //打印传递的姓名
println(“年龄:$age”)
}
fun main(args: Array) {
ntroduce(age = 20) //调用 introduce()函数,并指定函数中的形参与实参
}
在上述代码中,将 introduce()函数传递的形参 name 赋了一个默认的值“江小白” 。此时在 main()函数中调用 introduce()函数时,可以不用指定形参 name 的实参,直接传递形参 age
的值即可。若在调用时制定了 name 的值,那以此值为准。
2) 命名参数
在调用函数时显示指定形参的名称,这样即使形参和实参的顺序不一致也不会有任何影响,
因为已经明确指定了每个形参对应的实参。调用语法格式如下:
函数名称(形参 1=实参 1,形参 2=实参 2,形参 3=实参 3…)
例如:
fun info(name: String, age: Int) {
println(“姓名:$name”)
println(“年龄:$age”)
}
fun main(args: Array) {
info(name = “江小白”, age = 20) //调用 info()函数,指定函数中的形参与实参
}
在 main()函数中调用了 info()函数,在调用的同时指定了该函数的形参和实参,在调用函数
info()时,传递的两个参数的顺序可以不固定,可写为 info(name = “江小白”, age = 20),也可
写为 info(age = 20, name = “江小白”)
3) 可变参数
可变参数指的是参数类型确定但个数不确定的参数,可变参数通过 vararg 关键字标识,可
以将其理解为数组。可变参数通常声明在形参列表中的最后位置,如果不声明在最后位置,那么
可变参数后面的其他参数都需要通过命名参数的形式进行传递。
例如:
fun sum(name: String, vararg scores: Int) {
var result = 0
scores.forEach { //forEach 为迭代 scores 中的元素
result += it //it 代表 scores 中的每个元素
}
rintln(result)
}
fun main(args: Array) {
sum(“江小白”, 100, 99, 98, 100, 96)
}
在上述代码中, sum()函数定义了一个可变参数 scores。由于可变参数可以当作数组处理,
因此可以使用 forEach 循环遍历 scores 中的值。
当在 main()方法中调用 sum()函数时,需要传递一个 String 类型的实参,以及任意多个 Int
类型数据。由于可变参数实质上就是数组,因此,可以直接使用数组存放可变参数,在传递时使用数组
即可,例如:
var scores: IntArray = intArrayOf(100, 99, 98, 100, 96)
sum(“江小白”, scores)
注意:在实参中传递数组时,需要使用“”前缀操作符,意思是将数组展开,并且它只能展
开数组,不能展开集合。
Kotlin 中可变参数规则:
• 可变参数可以出现在参数列表的任意位置;
• 可变参数是通过关键字 vararg 来修饰;
• 可以以数组的形式传递实参,使用时,需要使用“
”前缀操作符。
Java 中可变参数规则:
• 可变参数只能出现在参数列表的最后;
• 用“…”代表可变参数,“…”位于变量类型与变量名称之间;
• 调用含有可变参数的函数时,编译器为该可变参数隐式创建一个数组,在函数体中以数组
的形式访问可变参数。
7.5. 函数重载
函数名相同,形参列表不同的函数,就被称为函数重载,不推荐重载形参个数可变的函数。
注意:函数的重载与函数的返回值类型无关,只需要同时满足两个条件,一是函数名相同,
二是参数个数或参数类型不相同即可。
7.6. 单表达式函数
如果函数体中只有一行代码,则可以把包裹函数体的花括号{}替换为等号“=” ,把函数体
放在等号“=”的后面,这样的函数称为单表达式函数。例如:
fun add(a:Int,b:Int):Int{
return a+b
}
上述函数只有一行,且返回一个表达式,因此可以转换为表达式函数,转换后如下:
fun add(a:Int,b:Int):Int=a+b //函数返回的值是表达式 a+b 的值
在上述语法格式的基础上,单表达式还可以省略函数的返回值类型,结果如下:
fun add(a:Int,b:Int) =a+b //编译器会根据函数的返回值进行推断
7.7. Lambda 表达式
7.7.1. 概述
Lambda 表达式就是一个匿名函数(表达式、函数和逗号运算符的综合),它是函数式编程的
基础。函数式编程的思想是将计算机运算视为函数的计算,并且计算的函数可以接收函数作为输
入参数或者当作返回值来使用。格式:
{ params -> expressions }
params 表示参数列表,expressions 表示具体实现,可以是单行语句,也可以是多行语句。
返回值:
注意:Lambda 表达式所表示的函数都是有返回值的,返回值的值和类型和都是由方法体中
最后一条语句决定,但在定义的时候省略了返回值的类型,因为返回类型可以自动推断出来。
Lambda 表达式的特点总结如下:
 Lambda 表达式必须用大括号括起来
 其参数(如果存在)在 -> 之前声明(参数类型可以省略)
 函数体(如果存在)在 -> 后面
7.7.2. 无参数表达式
在定义无参 Lambda 表达式时,只需要将函数体写在“{}”中,函数体可以是表达式或语句
块,语法格式如下:
{函数体}
调用格式如下:
{函数体}()
例如:
fun main(args: Array) {
{
println(“无参数 Lambda 表达式”) //这一行就是函数体
}()
}
上面代码中,{ println(“无参数 Lambda 表达式”) } 为无参数的 lambda 表达式,后面加括
号就此表达式的调用,Lambda 表达式被调用后,便会执行表达式中的函数体。
7.7.3. 有参数表达式
定义有参数的 Lambda 表达式时,需要指定参数名称以及参数类型,参数之间使用英文逗号
“,”分隔。Lambda 表达式中的“->”用于指定参数或数据的指向,语法格式如下:
{参数名:参数类型,参数名:参数类型 … ->函数体}
注意:
 参数不需要括号
 若函数体是多行,不能加{}(加了{},函数体变成了一个无参数的 lambda 表达式)
调用方式与无参 Lambda 表达式类似,在表达式后面加“()”,但是“()”中需要填写函数的
参数,格式如下:
{参数名:参数类型,参数名:参数类型 … ->函数体}(参数 1,参数 2… )7.7.4. 单个参数的隐式名称 it
若一个 lambda 表达式只有一个参数,可以不用声明唯一的参数,并忽略符号“->”,该参数
会隐式声明为 it,例如:
ints.filter{ it>0 } // { it:Int –>it>0}
7.7.5. 将 Lambda 表达式赋值给一个变量,通过变量来直接调用。
fun main(args: Array) {
val sum={a:Int, b:Int -> a+b}
print( sum(6,8) ) //通过 sum 变量调用 lambda 表达式
}
7.7.6. 传递末尾的 Lambda 表达式
在 kotlin 中有一个约定:如果函数的最后一个参数是函数,那么作为实参传入的 lambda 表
达式可以放在圆括号之外。例如:
val product=items.fold(1){acc,e->acce}
这种语法也称为拖尾 lambda 表达式。
如果该 lambda 表达式是调用时的唯一参数,那么圆括号也可以省略,例如:
run{ print(“xxx”)} //{ print(“xxx”)}作为 run()函数的唯一参数
另外,如果在默认参数(赋值的形参)之后的最后一个参数是 lambda 表达式,那它既可以
作为具名参数在括号内传入,也可以在括号外传入。例如:
fun foo(bar: Int=0, baz: Int=1, qux:() ->Unit){ /
……
/ } //参数 qux 为 lambda 表达式
foo(1) { println(“hello”) } // baz=1,lambda 表达式在括号外
foo(qux = { println(“hello”) }) // 前两个参数使用默认值,qux 使用具名参数,且在括号内
foo { println(“hello”) } // 前两个使用默认值,lambda 在括号外,且由于只有一个参数,
所以可以省略括号
7.7.7. lambda 表达式的应用

1) 函数式接口隐式转换为 lambda 表达式
button.setOnClickListener { [view : View ->]
Toast.makeText(this,”Hello World”,Toast.LENGTH_LONG).show()
}
参考 “匿名内部类”
7.8. 匿名函数
省略名称的函数,例如:
fun(x:Int, y:Int):Int = x+y //fun 后面省略了函数名
匿名函数看起来与一个常规函数的声明类似,只是名称省略了,其函数体可以是表达式(上一个函数)或代码块(下面的函数)。
fun(x:Int, y:Int):Int {
return x+y
}
与 lambda 的区别:
 匿名函数参数作为实参时,必须在括号内传递。允许将函数留在圆括号外的简写语法仅适
用于 lambda 表达式。
 大部分情况下,匿名函数和 lambda 表达式几乎可以通用,推荐使用 lambda 表达式。
但匿名函数可以使用 return 关键字,而 lambda 表达式不行。
7.9. 函数类型

1) 表示函数类型的元素有:参数个数,参数类型,返回值类型。格式为:
(参数类型,参数类型,…)->返回类型
 参数类型列表确定参数个数和类型,可以为空,但圆括号不能少,例如:()->A
 返回类型必须显式声明,Unit 类型也不能省略。
2) 函数类型为可空类型的写法:
var funOrNull:( (Int, Int) -> Int )? = {…}
即在非空函数类型基础上,外层添加括号,右侧添加 ? , 该函数类型就变成了可空函数类型;
如果不加括号,那就是函数类型中,函数的返回值为可空的情况。
可以通过使用类型别名给函数类型起一个别称,例如:
typealias ClickHandler = (Button, ClickEvent)->Unit
3) Kotlin 中函数类型有三种实例化方式(初始化函数变量):
 通过函数名,可以通过 :: Name 或者 obj:: Name,表示把一个函数当做一个参数,传
递到另一个函数中进行使用,即引用一个函数。
 匿名函数类型。
 Lambda 表达式。
例如:
//定义函数 get
fun get(i:Int):String{
return “${i3}”
}
fun main(){
//使用带名称的函数
val func3:(Int)->String = ::get
//使用匿名函数
val func1:(Int)->String = fun(i:Int):String{ return “${i
3}” }// 使用 Lambda 表达式实例化
val func2:(Int)->String = { i:Int-> “${i*3}” }
}
7.10. 高阶函数
高阶函数是将另一个函数用作参数或者返回值的函数。Kotlin 可以以 lambda 或函数引用作
为参数或返回值,所以,任何以 lambda 或函数引用作为参数或返回值的都是高阶函数。

1) Lambda 表达式作为其他函数的参数
//普通函数类型参数的高阶函数
fun caculate(operation: (Int, Int) -> Int) { //形参 operation 参数为函数类型
val result = operation(2,3) //通过 operation 调用函数
println(“result is $result”)
}
caculate { a,b -> a+b } //实参为{ a,b -> a+b }的 lambda 函数
caculate { a, b -> a* b} //实参为{ a,b -> a*b }的 lambda 函数
从上述例题可以看到,定义了高阶函数 caculate,其参数为一个 lambda 函数,调用 caculate
时,根据传入不同的 lambda 函数作为参数,caculate 内部进行不同的调用,并返回不同的值。
//参数为可空的函数类型
fun foo(callback: (() -> Unit)?) {
if (callback != null) //必须显式检查 null
callback()
}
//更为常用的是采用安全调用符:
fun foo(callback: (() -> Unit)?) {
callback?.invoke()
}
2) Lambda 表达式作为其他函数的返回值
函数的返回值为另外一个函数。所以定义函数时,需要指定一个函数类型作为返回类型,函
数体中需要一个包含 lambda 的 return 语句。
fun foo(): (Int,Int)->Int{
return { Int a,Int b-> a+b }
}
7.11. 内联函数
被“inline”修饰符修饰的函数称为内联函数,当函数被声明为 inline 时,函数体会被直接替
换到函数被调用的地方,而不是被正常调用。
例如:public inline fun xxx(){ … }

8. 可见性修饰符

在 Kotlin 中有这四个可见性修饰符:private、protected、internal 和 public。如果没有显
式指定修饰符的话,默认可见性是 public。
8.1. 包内元素
函数、属性、类、对象、接口可以在顶层声明,即直接在包内声明,例如:
package foo
fun baz(){ ……} //顶层函数
class Bar{ ……}
他们的可见性为:
public 默认可见性,其他文件和包皆可访问
private 只会在声明它的文件内可见;
internal 它会在相同模块内可见;
protected 不适用
注意:要使用另一包中可见的顶层声明,仍需将其导入。
8.2. 类和接口中的元素
public 默认可见性,能访问类/接口,即可访问此元素
private 类/接口内部(包含其所有成员)可见
internal 能见到类声明的本模块内可访问
protected 本类和子类可访问
外部类不能访问内部类的 private 成员
覆盖一个 protected 成员并且没有显式指定其可见性,该成员还会是 protected 类
8.3. 局部变量
局部变量、局部函数和局部类不能有可见性修饰符
8.4. 模块(intenal)
可见性修饰符 internal 意味着该成员只在相同模块内可见。更具体地说,一个模块是编译在
一起的一套 Kotlin 文件:
 一个 IntelliJ IDEA 模块
 一个 Maven 项目
 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明)
 一次Ant任务执行所编译的一套文件9. 类

9. 类

9.1类的定义
Kotlin 类的成员可以包含:
 构造函数和初始化块
 属性
 函数
 嵌套类和内部类
 对象声明
注意,kotlin 类中是没有字段的概念的,定义的变量都是属性。
类默认是 public final 的。即 public final class xxx,不能被继承。
例如:
class name head{

}
类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类
头与类体都是可选的;
如果一个类没有类体,则可以省略花括号,例如:class Empty
通常,一个类的内容按照以下顺序排列:
 属性声明与初始化块
 次构造函数
 方法声明
 伴生对象
9.2. 构造函数

1) 主构造函数
在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类
头的一部分:它跟在类名后,用关键字 constructor 定义(而不是与类名一致),例如:
class Person constructor(name:String){ …… } //name 为构造函数的参数
如果主构造函数在 constructor 前面没有任何注解或者可见性修饰符,可以省略 constructor
关键字。例如:
class Person(var name:String){ …… }
class Person public constructor(var name:String) { …… }
注意:主构造函数不包含代码,代码放到以 init 关键字作为前缀的初始化块中。在实例化期
间,初始化块按照它们出现在类体中的顺序执行,且初始化块可以分割后与属性初始化器交织在
一起。
例如:class testDemo(name:String){
val firstProperty=”First property:$name” //属性初始化器
//主构造函数初始化块
init {
println(“First initializer block that is prints ${name}”)
}
val len=name.length //属性初始化器
//主构造函数初始化块
init{
println(“Second initializer block that is ${ len }”) //若前面已定义属性,此处可用
}
}
fun main(){
testDemo (“hello”)
}
注意:
 主构造函数的参数可以在初始化块中使用,也可以在类体内声明的属性初始化器中使用。
 一个非抽象类没有声明任何(主或次)构造函数,默认有一个不带参数的主构造函数。
 构造函数默认为 public
2) 次构造函数
在类体内,前缀为 constructor 的构造函数。例如:
class Person{
constructor(xxx){ … }
}
注意,如果类存在主构造函数和次构造函数,次构造函数必须通过 this 函数调用主构造函数。
并且主构造函数的参数个数必须小于新定义的次构造函数中参数的个数。如果一个类没有定义主
构造函数,次级构造函数就不必显式调用主构造函数(会自动隐式调用)。
this()函数作用为调用本类的构造函数,格式:
次构造函数:this(参数列表)
调用方式有如下两种:
 方式 1 直接调用:次构造函数主构造函数
class Con(name: String) { //主构造函数,参数为 name
var age = 0; //属性
var name:String //属性
init{ //主构造函数初始化块
this.name=name}
//次构造函数,直接委托主构造函数,参数名与主构造函数不需要一致,各管各的
constructor(na: String, age: Int) : this(na) {
this.age = age;
println(“次构造函数,$name , $age”)
}
}
fun main() {
var tt=Con(“jack”,22)
}
 方式 2 间接调用:次构造函数次构造函数…主构造函数
class Con(name: String) {
var age = 0
var name:String?=null
var sex:Char?=null
init{ //主构造函数,参数为 name
this.name=name
println(“主构造函数,$name”)
}
//此次构造函数直接调用主构造函数
constructor(jackname: String, age: Int) : this(jackname) {
this.age = age;
println(“次构造函数 1,$name , $age”)
}
//此次构造函数间接调用上面的次构造函数
constructor( johnname:String, age:Int, sex:Char ) : this(johnname,age) {
this.sex = sex;
println(“次构造函数 2,$name , $sex”)
}
}
fun main(){
var t2=Con( “john”, 22, ‘m’ )
}
运行结果为:
主构造函数,john
次构造函数 1,john , 22次构造函数 2,john , m
可以看到,执行的顺序为:主构造函数直接调用的次构造函数间接调用的次构造函数
9.3. 创建类实例(对象)
kotlin 没有 new 关键字,创建实例时,直接调用构造函数即可。例如
val con=Con(“jack”) //创建对象
如果有多个构造函数,会根据不同的参数自动选择调用相应的构造函数
val con=Con(“jack”,22)
val con=Con(“jack”,22,’m’)
9.4. 属性

1) 定义与初始化
在 java 中,字段和其访问器的组合被叫做属性,在 kotlin 中的属性自动包含字段和访问器方
法,不再需要手动生成。
在类中声明一个属性和声明一个变量一样,使用 val 和 var。
 val 是只读属性,生成一个字段和一个简单的 getter
 var 是可写属性,生成一个字段 一个 getter 方法和一个 setter 方法
例如:
class Person{
val name:String = “只读属性” //生成一个字段和一个简单的 getter
var age:Int=20 //读写属性,生成一个字段,一个 getter 和一个 setter
}
另外,可以从主构造函数(次构造函数不可以)的参数中声明属性,格式:
//注意:在构造对象时,必须要有签名一致的参数来初始化属性
class Person( val/var 属性名:类型 , … ){ /……/ }
可以看到,其与参数的区别是有 val/var 修饰符。
例如:
class Con(var name: String, var age:Int) { //两个属性
fun show(){
println(“$name,$age”) //输出属性值
}
}
fun main(){
var t2=Con(“john”,22) //参数签名必须与属性签名一致
t2.show()
}
综上所述,属性的定义与初始化方法有如下三种: 主构造函数内定义属性,使用传入的参数初始化属性
 类体内定义属性,同时初始化
 类体内定义属性,init 块里初始化
lateinit 修饰符:kotlin 中的属性都是需要初始化值的(除非设置为可空类型),lateinit 表明
这个属性暂时不需要初始化(但是记得一定要在构造方法中初始化,不然还是会报空引用的异常),
会推迟到构造方法后调用。使用 lateinit 应注意:
 lateinit 只能应用于 var 声明的属性
 lateinit 不可以修饰原始数据类型(Byte,Char,Short ,Int,Long,Float,Double)
 委托中的 by lazy 则要求变量为 val
2) 属性访问
要访问属性,只需要通过名称来对它进行引用即可,格式:
对象.属性
例如:
val con=Con(“jack”,22,’m’)
con.name、con.age、con.sex
对于 ral 属性,只能取值,不能赋值,var 可以进行任意读取。
3) 自定义属性访问器
如果定义了一个自定义的 getter/setter 访问器,那么每次访问该属性时,都会自动调用它。
getter()对应 Java 中的 get()函数,setter()对应 Java 中的 set()函数。不过注意的是,不存在
Getter()与 Setter(),这只是 Kotlin 中的叫法而已,真是的写法,还是用 get()、set()。
例如:
var pro =“a”
get()=field
set(value) {
field = value
}
value 是 Koltin 写 setter()函数时其参数的默认名称,也可以换成其他名称。field 代表这
个属性本身。
9.5. 继承

1) 概述
在 Kotlin 中所有类都有一个共同的超类 Any(在程序运行时,Any 类会自动映射为 Java
中的 java.lang.Object 类)。Any 有三个方法:equals()、hashCode()与 toString()。因此,Kotlin
所有类都定义了这些方法。
如需声明一个显式的超类,在类头中把超类初始化(类似 C++的子类构造函数的书写方式)
放到冒号之后,例如:class Derived(p:Int):Base(p) //注意:是超类初始化
由于 kotlin 中的类默认是 public final 的,所以不能被继承。因此,某个类作为超类被继承
时,需要在此类前面加上 open 关键字。
例如:
open class Parent{ //基类必须有 open 修饰
fun show(){
println(“hello Parent”)
}
}
//注意,即使基类没有定义构造函数,但还有隐式的构造函数,所以此处必须初始化,即调
用构造函数进行初始化
class Child:Parent(){}
fun main(){
var c=Child()
c.show()
}
如果派生类有一个主构造函数,其基类必须用派生类主构造函数的参数就地初始化。如果派
生类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另
一个构造函数做到这一点。注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构
造函数。
继承注意点:
 一个类只能继承一个父类,不能继承多个父类,即单继承
 多个类可以继承一个父类
 多层继承也是可以的,即一个类的父类可以再去继承另外的父类,例如 C 类继承 B 类,
而 B 类又可以去继承 A 类,这时,C 类也可称作 A 类的子类。例如:
open class A {}
open class B : A() {} //类 B 继承类 A,类 B 是类 A 的子类
class C : B() {} //类 C 继承类 B,类 C 是类 B 的子类,同时也是类 A 的子类
 子类继承父类时会自动继承父类中定义的方法和属性
2) 方法重写
 在子类中重写的方法与在父类中被重写的方法必须具有相同的方法名、参数列表、返回值
类型
 重写的方法需要使用“override”关键字标识。
 在子类中重写的属性与在父类中被重写的属性必须具有相同的名称和类型,并且重写的属
性前边也需要使用“override”关键字标识。
 在父类中需要被重写的属性和方法前面必须使用“open”关键字来修饰 重写基类中使用默认值的方法时,重写方法中必须省略默认参数值。例如:
open class A{
open fun foo(i:Int=10){ … }
}
class B:A(){
override fun foo(i:Int){ … }
}
3) super 关键字
当子类重写父类的方法后,通过子类对象将无法直接访问父类被重写的方法,需通过 super
关键字才能访问。格式如下:
super.成员变量
super.成员方法( [ 形参 1, 形参 2… ] )
9.6. 抽象类
被 abstract 关键字修饰的类(class) 被称为抽象类。注意,当一个类中包含了抽象方法,该
类必须定义为抽象类。格式:
abstract class className{
abstract fun funName()
}
包含抽象方法的类必须声明为抽象类,但抽象类可以不包含任何抽象方法。另外,抽象类不
可以被实例化。
抽象方法:必须使用 abstract 关键字修饰,没有方法体的方法;且默认是 open 方法,例如:
abstract fun xxx()
被 abstract 关键字修饰的属性(var|val) 被称为抽象属性;抽象属性没有初始值,也没有
setter 和 getter 访问器。
抽象方法和抽象属性只能在抽象类中声明,它们和抽象类默认都是 open,不需要手动添加
open 修饰符。
9.7. 接口
 接口通过 interface 定义,格式:
interface 接口名称 [ : 继承接口 ]{

}
 Kotlin 的接口可以既包含抽象方法的声明,也包含实现的方法(实现的方法类似 java 接
口中的 default 方法),实现的方法在实现类中不需要重写,可以直接调用。没有实现的
必须重写
例如:interface Interface1 {
fun bar() // 未实现
fun function1() { //已实现方法
println(“Interface1 function1 called”)
}
}
interface Interface2 {
fun function2() { //已实现方法
println(“Interface2 function2 called”)
}
}
class MyClass : Interface1, Interface2 {
override fun bar() { //实现 Interface1 中未实现的方法
// 方法体
}
fun myFunction() { //实现类中直接调用接口中已实现的方法
function1()
function2()
}
}
fun main(){
MyClass().myFunction()
}
 与抽象类不同的是,接口无法保存状态(即属性)。所以接口可以定义属性,但必须声明
为抽象的或提供访问器实现。在实现接口时,必须重写抽象属性。
例如:
interface MyInterface{
var name:String //name 属性, 默认实抽象的
//提供访问器实现的属性,不是抽象的,但是不能引用它们
val proImpl: String
get() = “foo”
}
class MyImpl:MyInterface{
override var name: String = “runoob” //必须重写抽象属性
}fun main(){
var myimpl=MyImpl()
myimpl.name=”new string” //可以引用,正常赋值
myimpl.proImp=”new proimpl” // 不可以引用,报错 “Unresolved
reference”
println(myimpl.name)
print(myimpl.proImpl)
}
 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
 函数式接口:有且仅有一个抽象方法的接口
函数式接口可以使用 lambda 方式实现。即函数式接口可以自动被隐式转换为 lambda
表达式。
函数式接口的 lambda 实现:
例 1:无参数的情况
button.setOnClickListener{ textView.setText(“新内容”) }
例 2:有参数的情况
TabLayoutMediator(tab,viewpage2){ tab: TabLayout.Tab, position:Int->
tab.setText(str_ary[position])
}
9.8. 常用类
9.8.1. 伴生对象(companion)
Kotlin 取消了关键字 static,也就无法直接声明静态成员,所以引入了伴生对象这个概念。
在对象声明的前面加上 companion 关键字就生成了伴生对象。作用就是为其所在的外部类模拟
静态成员。伴生对象是在类加载时初始化,生命周期与该类的生命周期一致。
注意:由于每个类中有且仅有一个伴生对象,因此也可以不指定伴生对象的名称,并且类的
各个对象可以共享伴生对象。所以,调用过程中,可以不需要伴生对象,而是通过外部类直接调
用伴生对象内的成员。
定义语法:
companion object [伴生对象名称] : [0~N 个父类型] {
……
}
伴生对象成员调用:
(1)有名称:调用方式为“类名.伴生对象名.成员名”或“类名.成员名” 。
(2)无名称:调用方式为“类名.Companion.成员名”或“类名.成员名” 。
特点: 每个类最多定义一个伴生对象;
 伴生对象相当于外部类的对象,可以直接通过外部类名访问伴生对象的成员;
 虽然伴生对象是为其所在对象模拟静态成员,但是伴生对象成员依然属于伴生对象本身的
成员,而不属于其外部类的成员。
例如:
class OuterClass {
companion object Co{
val name = “伴生对象属性”
fun companionFun() {
println(“调用伴生对象方法”)
}
}
}
fun main() {
println(OuterClass.name) / println(OuterClass.Co.name)
OuterClass.companionFun() / OuterClass.Co.companionFun()
OuterClass.Co //通过伴生对象名称获取伴生对象本身
}
省略伴生对象名称后,如果想获取伴生对象本身,可以通过 Companion 获取,例如:
OuterClass.Companion //通过 Companion 获取伴生对象本身
9.8.2. 单例模式(object)
单例模式就是在程序运行期间针对该类只存在一个实例。就好比这个世界只有一个太阳一样,
假设现在要设计一个太阳类,则该类就只能有一个实例对象,否则就违背了事实。
在 Kotlin 中,单例模式是通过 object 关键字来完成的,通过 object 修饰的类即为单例类,
单例类在程序中有且仅有一个实例(即定义的类即是类又是此类的实例)。
例如:
object Singleton { //定义单例对象
var name = “单例模式”
fun sayHello() { … }
}
fun main(args: Array) {
Singleton.name = “小太阳”
Singleton.sayHello()
}
注意,kotlin 中没有 static 类,所以,java 中的 static 类,在 kotlin 中用单例模式表示。9.8.3. 内部类(inner) 和嵌套类

1) 概念
嵌套类是指嵌套在其他类中的类,嵌套类不能访问外部类的成员。
内部类指的是在其他类中用 inner 标记定义的类,内部类能够访问外部类的成员。
Kotlin 中的内部类与嵌套类与 Java 中的类似,不同的是在没有任何修饰的情况下,定义在
一个类内部的类被默认称为嵌套类,如果想将它声明为一个内部类,则需要加上 inner 修饰符。
例如:
class Outer {
var name = “njucm” //外部类的属性
inner class Inner {
fun sayHello() {
println(“Hello!我叫$name。”) //内部类可以调用外部类的成员变量
}
}
}
2) 内部类中访问外部类对象
在 Android 开发中,若有内部类访问外部对象的需求,就不能采用 java 中采用 OutClass.this
的方式了。
在 Kotlin 中,访问外部类对象的方式为:this@OutClass,若访问外部对象的元素,字需要
在后面加上元素名即可,例如:this@OutClass.xxx
例如,在 MainActivity 的内部类中访问 MainActivity 对象的代码如下:
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {

button.setOnClickListener(ClickListener()) //绑定事件处理对象(新建对象)
}
//内部类实现接口,所以事件处理对象需用本类定义
inner class ClickListener : View.OnClickListener {
override fun onClick(v: View?) {
//注意:内部类访问外部对象,方法为:this@外部类名
this@MainActivity.findViewById(R.id.textView).setText(“新内容
“)
// 由于内部类可以访问外部类的对象,所以简化为
// textView.setText(“新内容”)
}}}
注意: 在内部类内部访问外部对象,方法有两种,一种是当前程序所用的“this@外部类名”,还
有一种方法就是将对象定义为类变量,然后在 onCreate()方法中初始化,那所有的方法或
者内部类就都可以访问了。
 在内部类中才可以访问外部类的内容,所以,必须定义成内部类(inner),而不是嵌套类。
9.8.4. 匿名内部类
在 java 中创建匿名内部类,只需要把这个类直接 new 出来。例如最常见的设置一个按钮的
单击事件:
button.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) { …… }
} );
上述代码中,通过 new OnClickListener(){}创建了一个 OnClickListener 接口的匿名的内部
类,此接口中唯一的方法就是 onClick(View v)。
Kotlin 中不需要 new,可以通过如下方法实现匿名内部类:

1) object 方式(单例模式)。
匿名内部类就使用一次,与单例模式正好吻合。且在创建单例模式时,可以省略类名。
例如,object MyClass:BaseClass{ … },采用匿名类表示后为:object:BaseClass{ … },省
略掉类名。所以,Java 中的 OnClickListener 接口的实现对象可以如下定义:
button. setOnClickListener ( object : OnClickListener{
override fun onClick (v: View?){ …… }
})
2) lambda 方式
上面单例模式中,setOnClickListener 函数的参数是一个单例,且它只有一个需要实现的抽
象方法。
这种有且仅有一个抽象方法、但可以有多个非抽象方法的接口称为函数式接口。在 kotlin 中,
可以通过 lambda 表达式来实现一个函数式接口的匿名对象。格式如下:
val myOnClickListener = OnClickListener{ //… }
上述代码通过 lambda 表达式显式的实现了一个函数式接口对象,即用 lambda 表达式代替
了 OnClickListener 接口中的唯一函数,参数的设定参考 lambda 表达式,若只有一个参数,可
省略,在函数体内用 it 访问。
下面就可以将此对象作为参数传入,代码如下。
button. setOnClickListener ( myOnClickListener )
但是,Kotlin 对于函数式接口对象作为参数这种情况,提供了更为简洁的方式,即
OnClickListener{ //… }生成对象的代码中,可以省略接口名,只保留{ //… } 部分,即
lambda 表达式,编译器会将 lambda 实参自动转换成一个 OnClickListener 实例(形参规定了接口类型)。
所以,对于函数式接口对象作为参数,一般的代码为:
button. setOnClickListener { //省略单个参数,若要使用,用 it 访问

}
上述代码中,setOnClickListener 的参数只有一个,且是简化的 lambda 表达式,按照 lambda
表达式的规定(如果该 lambda 表达式是函数调用时的唯一参数,那么函数的圆括号可以省略),
所以可以这样表示。
注意:若是纯粹定义一个函数式接口的实例,必须采用显式定义的方式,即:
val/var 属性 = 接口名{ //… }
9.8.5. 数据类(data)
在 Java 程序中,一般会用一些类来保存一些数据或者对象的状态,习惯上将这些类称为
bean 类或 entity 类或 model 类。在 Kotlin 中,专门处理这些数据的类被称为数据类,此类只包
含数据。
语法格式如下:
data class 类名(主构造函数列表) [:继承类和实现接口] [(/类体/)]
“类名(主构造函数列表)”是该类的主构造函数。定义一个数据类时,必须注意以下几点。
 数据类的主构造函数至少有一个参数,如果需要一个无参的构造函数,可以将构造函数中
的参数都设置为默认值。
 数据类中的主构造函数中传递的参数必须用 val 或 var 来修饰(属性)。
 数据类数据类默认为 final,不可以用 abstract、open、sealed 或 inner 关键字来修饰。
 kotlin1.1 版本之后数据类可以继承其他类。
 编译器自动生成一些常用方法,如 equals()、hashCode()、toString()、componentN()、
copy()等。这些方法也可以进行自定义,例如若在类体中显式定义或继承自其基类,则不
会自动生成该函数。
在实际开发中,经常会用到数据类来存储一些数据信息,这些信息一般是通过该类的主构造
函数来传递的。
例如:
data class LoginUser(val username: String, val password: String) //不需要类体
数据类仅仅包含状态而没有任何可执行的操作,使用数据类替换普通的好处是 Kotlin 会自动
产生大量代码,从而省下大量重复性的工作。
9.8.6. 枚举类
枚举类型中,每个枚举常量都是一个此类型的对象,枚举常量用逗号分隔。定义枚举类型的
关键字是 enum。
枚举类有两个内置的属性:public final val name: String //枚举对象名
public final val ordinal: Int //下标位置
由于每个枚举常量都是当前枚举类的实例,因此这些实例也可以初始化。同时枚举支持构造
函数,因此可以使用构造函数来初始化。例如:
enum class MyWeek (val myname: String) {
//WU/WANG 为创建的实例,”吴强”等为属性 myname 的值
WU(“吴强”),
WANG(“王娟”)
}
fun main(){
val en=MyWeek.WU
print(en.myname) //输出:吴强
print(en.name) //输出:WU
print(en.ordinal) //输出:0
}
上述代码中,每个枚举都通过构造函数进行了初始化
9.8.7. 密封类(sealed)
kotlin 定义密封类的关键字是 sealed,例如:
sealed class SealedClass //与普通类的区别是增加了 sealed 关键字
当一个值只能在一个集合中取值,而不能取其他值时,此时可以使用密封类。在某种意义上,
密封类是枚举类的扩展,即枚举类型的值集合。
如果想要创建一个密封类必须满足以下两个条件:
(1)密封类必须用 sealed 关键字来修饰。
(2)虽然密封类可以有子类,但是由于密封类的构造函数是私有的,因此密封类的子类只能
定义在密封类的内部或者同一个文件中。
例如:
sealed class College{
class Nju : College(){}
class Njucm(){}
}
class Njnu : College()
上述代码中,定义了一个密封类 College,并在该类的内部定义了 Nju 子类;在同一个文件
中定义了 Njnu 子类;Njucm 类是该密封类的嵌套类
注意,密封类的非直接继承子类可以声明在其他文件中。
Kotlin 中密封类与枚举类的区别:密封类适用于子类可数的情况,而枚举类适用于实例可数的情况。
9.9. 扩展函数/属性
Kotlin 可以对一个类的属性和方法进行扩展,且不需要继承或使用 Decorator 模式。

1) 扩展函数
扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式:
fun receiverType.functionName(params):returntype{

}
 receiverType:表示函数的接收者,即函数扩展的类/接口。除了这个,其他的都与定义
一个函数一样。
例如:
class Person(val name: String) {
fun eat() {
Log.i(name, “I’m going to eat”)
}
}
//扩展 Person 的方法
fun Person.drink() {
//方法中的 this 指的是调用这个扩展函数的当前对象
Log.i(“Person”, “${this.name}: I’m going to drink”)
}
扩展函数的调用:扩展函数调用与其他的实例函数相同,例如:
val person=Person(“alice”)
person.eat()
person.drink()
伴生对象的扩展:
伴生对象通过”类名.”形式调用伴生对象,伴生对象声明的扩展函数,通过用类名限定符来调
用,例如:
class MyClass {
companion object{ … } //将被称为 “Companion”
}
fun MyClass.Companion.foo() {
println(“伴随对象的扩展函数”)
}
fun main(args: Array) {MyClass.foo()
}
扩展函数和成员函数的区别:
 扩展函数不能打破扩展类的封装性,不能像成员函数一样直接访问内部私有函数和属性。
(原理:扩展函数访问实际上是类的对象访问,由于类的对象不能访问内部私有函数和属性,
自然扩展函数也就不能访问内部私有函数和属性了)
 扩展函数实际上是一个静态函数,处于类的外部,而成员函数则是类的内部函数。
 父类成员函数可以被子类重写,而扩展函数则不行
2) 扩展属性
扩展属性实际上就是提供某个属性访问的 set,get 方法,这两个方法是静态函数,同时都会传
入一个接收者类型的对象,然后在其内部用这个对象实例去访问和修改对象所对应的类的属性。
语法格式:
var/val receiverType.attributeName:T
get(){ … }
set(value){ … }
例如:
class Person(val name: String, val birthdayYear: Int) {
fun eat() {
print(name, “I’m going to eat”)
}
}
//通过扩展属性为 Person 增加读写的 age 属性
var Person.age: Int
get() = 2020 - this.birthdayYear
set(value){ … }
注意:扩展属性允许定义在类或者 Kotlin 文件中,不允许定义在函数中。
3) 扩展中的 this
在类的成员函数中,this 指向这个类的当前对象实例;这里的 this 指的是接收者对象,也就
是调用扩展函数时,在点号“.”之前指定的对象实例。
如果 this 没有限定符,那么它指向包含当前代码的最内层范围。如果想要指向其他范围内的
this,需要使用标签限定符。

10. 反射

10.1. kotlin 和 java 的反射
Java 反射:
JAVA 反射机制是指:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的以及动态调用对象的方法的
功能称为 java 语言的反射机制。
那么如何能够完成上述功能呢,那就需要通过内存中的 Class 对象才能完成。下面来介绍如
何获取 Class 对象,并通过此对象完成上述功能。
获得 Class 对象有以下三个方法:

1) 通过 Object 类的 getClass()方法 :public final Class<?> getClass()
例:
TextField t = new TextField();
Class c = t.getClass();
2) 通过“类.class”的形式
例:Class c = java.awt.Button.class;
3) 如果类名在编译期不知道, 但是在运行期可以获得,可以通过 Class 类的静态方法:
forName(string className)
例:Class cla=Class.forName(“cn.edu.njutcm.demo1.TestOne”);
注:className 必须为全名,也就是需包含包名
kotlin 的反射
 kotlin 的 KClass 和 java 的 Class 可以看作同一个含义的类型,并且可以通过.java(转
换为 java 中的 Class)和.kotlin(转换为 kotlin 中的 KClass)属性在 KClass 和 Class 之
间互相转化
 kotlin 的 KCallable 和 java 的 AccessibleObject 都可以理解为可调用元素。java 中构
造方法 Constructor 为一个独立的类型,而 kotlin 则统一作为 KFunction 处理
 kotlin 的 KProperty 和 java 的 Field 不太相同。java 的 Field 通常仅仅指字段本身,
Field 具备了 getter 和 setter;kotlin 并没有字段的概念,她将属性分为了 KProperty 和
KMutableProperty ,当变量声明为 val 时,getter 即为 KProperty,当变量声明为 var
时,getter 和 setter 即为 KProperty 和 KMutableProperty
java 的反射操作步骤大概可以总结为以下几点:
 获取 Class
 通过 Class 获取 obj
 通过 obj 反射获取对象的成员、方法等
kotlin 的反射操作步骤大致和 java 相同,只是因为反射基本结构不同,所以在代码实现上有
稍微的不同。
10.2. kotlin 的反射操作
 获取静态已知 kotlin 类的引用(KClass)
使用“类名::class”或“实例::class(支持协变)”,类似于 java 中的“类名.class”,即采用新
符号“::”例如:
val c: KClass< MyClass >=MyClass::class / val c= MyClass::class
若要获得 java 类的引用,方法有:
 在 Kclass 实例上使用.java 属性
 实例.javaClass
例如:
val c=MyClass::class
c.java //或者将两条语句合并为:MyClass::class.java
或者:
MyClass().javaClass
这个在 android 的 Intent 初始化中使用频繁。例如:
var intent = Intent(this,SecondActivity::class.java)
startActivity(intent)
同理,在 java 中,若需要获取 kotlin 中的 Kclass,可以通过.kotlin 属性来获取。
 Kotlin 运行时获取一个对象的 kclass 类:
对象.javaclass.kotlin
例如:val clazz = country4.javaClass.kotlin
11. 委托
委托模式也叫代理模式,是最常用的一种设计模式。在委托模式中,如果有两个对象参与处
理同一个请求,则接受请求的对象将请求委托给另一个对象来处理, 简单来说就是 A 的工作交
给 B 来做。
根据上述功能描述,委托模式中,有三个角色,需处理业务(即需要执行的函数,一般用接
口描述)、委托对象和被委托对象,如下图所示。
业务:它定义了通用的业务类型,也就是需要被代理的业务,通过接口或者抽象类来描述
委托对象:将具体业务委托给具体的委托对象
被委托对象:具体的业务逻辑执行者,需要实现请求接口/类中需要实现的函数。
委托模式是实现多继承的一个很好的替代方式。在 Kotlin 中,委托是通过 by 关键字实现
的,并且主要分为两种形式,一种是类委托,一种是属性委托。
业务

11. 委托对象 被委托对象

11.1. 类委托
格式:
class 委托类( 被委托对象 : 被委托实现类 ) : 业务类 by 被委托对象
委托类中的参数是被委托对象,用来实现业务中的函数。后面的“业务类 by 被委托对象”
表示业务类中的事务将会由被委托对象实现,所以,委托类中不需要重写这些方法,但却可以调
用。
例如:大家都喜欢玩游戏,游戏中有相应的等级,越往上越难,如果老是升不上去,就想着
找个代练帮我们打。这就是委托,这里面,游戏是业务,委托对象是玩家,被委托对象是代练,
代练需要实现游戏的任务,所以代练需要实现业务中的方法。
用程序完成上述例题:
//角色 1:业务接口/类
interface Game{
//升级任务
fun upgrade()
}
//角色 2:/被委托类,即代练,实际上是他在玩游戏,即实现 Game 中的函数
class GameImpl(val name:String):Game{
//实现升级任务
override fun upgrade(){
println(“${name}正在努力帮你升级!”)
}
}
//角色 3:玩家,由于玩家要请代练来帮他玩游戏,所以需要委托代理
class GamePlayer(player:GameImpl):Game by player
//测试
fun main() {
val gameImpl = GameImpl (“好助手游戏代理”) //创建游戏代理对象
val gamePlayer = GamePlayer(gameImpl) //创建玩家对象
gamePlayer.upgrade()
}
从上述例题可以看到,实际是被委托类已经实现了业务,而其他类也需要执行业务,就不再
需要再次实现业务中的方法了,只需要委托给已经实现了业务的被委托类即可。所以参数中需要
有被委托对象,而在接口后面通过 by 关键字来说明实现此接口的对象。
11.2. 属性委托
属性委托是指一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代理类,从而实现对该类的属性进行统一管理。属性委托的语法格式如下:
val/var 属性名: 类型 by 表达式
例如:
import kotlin.reflect.Kproperty //注意:一定要导入
class Example {
var p: String by Delegate() //属性 p 委托到了一个 Delegate 实例
}
上述代码中,by 关键字前面是正常的属性定义,而后面的表达式是一个接收属性委托的委托
实例。
因为属性对应的 get()/set()会被委托给此委托类的 getValue()与 setValue()方法。因此被委
托类需要提供 setValue/getValue 这两个方法。如果是 val 属性,只需提供 getValue。如果是
var 属性,则 setValue/getValue 都需要提供。
//委托类的定义
class Delegate{
private var pro:String?=null //在属性赋值时,用来存放值
operator fun getValue(thisRef:Any?, property:KProperty<>):String{
return “对象${thisRef},属性名称:${property.name},值${pro}” //输出属性值
}
operator fun setValue(thisRef:Any?, property: KProperty<
>,value: String){
pro=value //存放属性值
println(“在对象${thisRef}中,$value 被赋值给属性${property.name}”)
}
}
//测试
val e=Example()
e.p=”你好啊”
println(e.p)输出结果:
对象 Example@4c873330 中,你好啊 被赋值给属性 p
对象 Example@4c873330 中,属性名称:p,值:你好啊
参数:
 thisRef:是属性所属的对象,必须与属性所有者类型相同(本例为 Example 类)或者是
它的超类型;
 property:保存了对 p 自身的描述(注意不是属性值),必须是类型 KProperty<*>或其
超类型。
 value:必须与属性同类型或者是它的子类型。
注意: setValue()方法和 getValue()方法前必须使用 operator 关键字修饰。
 getValue()方法的返回类型必须与委托属性相同或是其子类
11.3. 延迟加载(by lazy)
Kotlin 标准库为几种有用的委托提供了工厂方法,包括延迟加载、可观察属性。
在 Kotlin 中,声明变量或者属性的同时要对其进行初始化,否则就会报异常,尽管可以定义
可空类型的变量, 但有时却不想这样做, 能不能在变量使用时再进行初始化呢?为此, Kotlin
中提供了延迟加载功能,即当变量被访问时才会被初始化,这样不仅可以提高程序效率,还可以
让程序启动更快。延迟加载是通过“by lazy”关键字标识的,延迟加载的变量要求声明为 val,
即不可变变量。延迟加载也是委托的一种形式,延迟加载的语法结构如下:
val 变量:变量类型 by lazy{
变量初始化代码
}
需要注意的是,延迟加载的变量在第 1 次初始化时委托方法会完整执行并记录结果,之后在
调用该变量时,都只会返回记录的结果。
例如:
fun main(args: Array) {
val content by lazy {
println(“Hello”)
“World” //第 1 次初始化后,再次调用该变量时,只会输出最后一行代码内容
}
println(content)
println(content)
}
在上述代码中,通过 by lazy 定义了一个延迟加载变量 content,该变量中有一行打印语句
以及一个字符串,当调用该变量时,会发现只有第 1 次加载时会输出变量中的所有内容,而第 2
次加载时,只输出变量中的最后一行内容。
比如这样的常见操作,只获取,不赋值,并且多次使用的对象
private val mUserMannager: UserMannager by lazy {
UserMannager.getInstance()
}
lazy{}与 lateinit 的区别:
 lateinit 只用于变量 var,而 lazy 只用于常量 val
 lateinit只能在生命周期流程中进行获取或者初始化的变量,比如在Activity的onCreate()
方法中,且可以初始化多次。而 lazy 在第一次被调用时就被初始化,想要被改变只能重
新定义。11.4. 可观察属性
Delegates.observable()接受两个参数:初始值与修改时处理程序(handler)。 每当我
们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值与新值:
importkotlin.properties.Delegates
classUser{
varname:StringbyDelegates.observable(““){
prop,old,new->println(“$old->$new”)
}
}
funmain(){
valuser=User()
user.name=”first”
user.name=”second”
}
如果你想截获赋值并“否决”它们,那么使用 vetoable()取代 observable()。在属性被赋新
值生效之前会调用传递给 vetoable 的处理程序。

12. 异常

12.1. 概述
Kotlin 中所有异常类都是 Throwable 类的子孙类。每个异常都有消息、堆栈回溯信息以及可
选的原因。
12.2. try…catch…finally
异常捕获通过 try…catch 语句来实现,格式如下:
try {
//可能发生异常的语句
}catch (e: SomeException) { // e 的类型可以是 Exception 类或者其子类
// 对捕获的 Exception 进行处理
} finally{ //注意,若不需要,finally 模块可以省略
//若没有捕捉到异常,执行的代码
}
当 try 代码块中的程序发生了异常,系统会将这个异常的信息封装成一个异常对象, 并将这
个对象传递给 catch 代码块。catch 代码块中传递的 Exception 类型的参数是指定这个程序能够
接收的异常类型,这个参数的类型必须是 Exception 类或者其子类。
使用注意:
 可以有零到多个 catch 块 finally 块可以省略,但是 catch 与 finally 块至少应该存在一个。
12.3. throw
如果去调用一个别人写的方法时,是否能知道别人写的方法是否会有异常呢?这是很难做出
判断的。针对这种情况,允许在可能发生异常的代码中通过 throw 关键字对外声明该段代码可能
会发生异常,这样在使用这段代码时,就明确地知道该段代码有异常,并且必须对异常进行处理。
throw 关键字抛出异常的语法格式如下:
throw ExceptionType(“异常信息”)
上述语法格式是通过 throw 表达式来抛出一个异常, 这个表达式需要放在容易出现异常的
代码中,也可以作为 Elvis 表达式(三元表达式)的一部分,throw 后面需要声明发生异常的类
型,通常将这种做法称为抛出一个异常。
12.4. Nothing 类型
在 Kotlin 中,有些函数的“返回值类型”的概念没有任何意义,此时 Kotlin 使用一种特殊
的返回值类型 Nothing 来表示,Nothing 是一个空类型,也就是说在程序运行时不会出任何一
个 Nothing 类型的对象。在程序中,可以使用 Nothing 类型来标记一个永远不会有返回数据
的函数,具体示例代码如下:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
当调用含有 Nothing 类型的函数时,可能会遇到这个类型的一种情况就是类型推断,这个
类型的可空类型为 Nothing?,该类型有一个可能的值是 null,如果用 null 来初始化一个变量的
值,并且又不能确定该变量的具体类型时,编译器会推断这个变量的类型为 Nothing?类型,具
体示例代码如下:
val x = null //变量“x”的类型为“Nothing?”
val l = listOf(null) //变量“l”的类型为“List<Nothing?>”
上述代码中,变量 x 的初始值为 null,并且该变量的具体类型不确定,因此编译器会推断该
变量的类型为“Nothing?”。 变量 l 的初始值为 listOf(null), 可以看到 List 集合的初始值为
null,此时不能确定该集合的具体类型,因此编译器会推断该集合的类型为“List<Nothing?>”。
Unit 与 Nothing 之间的区别是:
 Unit 是一个真正的类,继承自 Any 类,只有一个值,也就是所谓的“单例”(目的在
于函数返回 Unit 时避免分配内存),可省略。
 Nothing:表示永远都不会有返回,所以,Nothing 是没有实例的,程序运行时不会出
现任何一个 Nothing 类型对象,在抛出异常的函数中,返回的类型设置为 Nothing。
 Nothing? :它唯一允许的值是 null ,被用作任何可空类型的空引用。12.5. 受检异常
Java 中有两种异常类型,一种是受检异常,一种是非受检异常,在编写 Java 代码时,由于
编译器在编译时会检查受检异常,因此 IDEA 会提示进行 try…catch 操作。
而 Kotlin 中相比于 Java 没有了受检异常,IDEA 也不会提示进行 try…catch 操作。

13. 泛型

13.1. 概述
在实现集合类时,如果不使用泛型,那使用的是 Object[]数组,但是这种实现方式存在如下
的问题:
 向集合中添加对象元素的时候,没有对元素的类型进行检查。也就是说,我们向集合中添
加任意对象,编译器都不会报错。
 从集合中获取一个值的时候,不能都使用 Object 类型,需要进行强制类型转换。而这个
转换过程由于在添加元素的时候没有做任何的类型的限制与检查,所以容易出错。
所以,提出在设计类的时候,给定一个虚拟类型,在实例化的时候,所有的对象都必须是此
类型(C++类模板,Java 的泛型)。
13.2. 在类、接口和函数上使用泛型

1) 泛型类/接口
使用泛型标记的类称为泛型类。当定义泛型时,泛型是在类型名之后、主构造函数之前用尖
括号“<>”括起来的类型参数。
class/interface 类/接口名<E,K,…>(){ … } //注意,泛型参数可以有多个
E,K 为类型参数,表示某种类型(名称自定),定义时是一个未知类型,当创建类的实例时,
需要传入具体的类型。例如:
class Box(t: T) { //构造函数参数 t 的类型为 T
var value = t
}
val box: Box = Box(1) //创建实例,传入具体类型为 Int
当定义泛型类型的实例时(注意,不是在定义类时),可以完整地写明类型参数,如果编译器
可以自动推断类型参数,则可以省略类型参数。例如:
val box = Box(1) //编译器可以自动推断出参数 1 是 Int 类型,所以省略类型参数
由于传递到构造函数中的参数是 1,这个参数是一个 Int 类型的参数,编译器可以推断出泛
型的类型是 Int 类型,因此在创建类的实例时可以省略类型参数。
2) 自定义泛型函数
也可以直接在类或接口中的函数声明泛型参数或者在包级函数中直接声明泛型参数,格式:
可见性修饰符 fun <泛型符号> 函数名(参数): 函数返回值{
… }
例如:
class MyClass{
fun show(t:T){ … }
}
13.3. 泛型约束/泛型上届

1) <T : 类或接口>约束
泛型约束是对类或者方法中的类型参数进行约束。例如:当创建一个泛型 List时,类型
参数 E 理论上是可以被替换为任意的引用类型,但是有时候需要约束泛型实参的类型,例如想对
E 类型变量求和,则 E 应该是 Int 类型、Long 类型、Double 类型或者 Float 类型等,而不应
该是 String 类型,因此在特殊情况下,需要对类型变量 E 进行限制。
格式:
<T : 类或接口>
泛型约束也称之为泛型的上界,表示在实例化类型参数时,类型 T 必须是后面的类/接口或其
子类。并且,如果泛型约束中指定了类型参数的上界,则可以调用定义在上界类中的方法。
fun twice(value: T): Double {
return value.toDouble() * 2
}
fun main(args: Array) {
println(“4.0 的两倍:${twice(4.0f)}”) //将 4.0f 传递到 twice()中并打印结果
println(“4 的两倍:${twice(4)}”) //将 4 传递到 twice()中并打印结果
}
运行结果:
4.0 的两倍:8.0
4 的两倍:8.0
在上述代码的 twice()方法中,参数 value 调用的 toDouble()方法是在 Number 类中定义
的。由于在泛型约束中已经指定类型参数的上界为 Number,因此 twice()方法中
传递的参数 value 可以调用定义在上界类 Number 中的方法(虽然参数为 Float 和 Int 类型)。
如果上界约束需要多个约束,则可以通过 where 语句来完成。例如:
fun manyConstraints(value: T) where T : CharSequence, T : Appendable{ … }
通过 where 关键字实现了上界约束的多个约束,每个约束中间用逗号分隔,并且传递的参
数 value 可以调用第 1 个约束类中的方法,同时也可以调用第 2 个约束类中的方法。
2) <T : Any?>与约束
默认的上届是<T : Any?>,<T : Any?>表示类型实参是 Any 的子类,且类型实参可以为 null。
表示类型实参是 Any 的子类,且类型实参不可以为 null。在 Kotlin 中,Any 类型是任意类型的父类型,类似 Java 中的 Object 类,因此声明的<T : Any?>等同于
13.4. 协变与逆变(out/in)

1) 协变(out)
 协变:若泛型的类型参数存在继承关系,那使用泛型时可以进行向上转换性质的转换(注
意,类型本身之间不存在继承关系)。例如:如果 Ch 是 Pa 的子类型,协变就是 Generic
也是 Generic 的子类型。协变语法上用 out 表示。
例如程序如下:
class Test //定义一个泛型类
fun main() {
var ta:Test?=null //定义类型参数为 Any 的泛型类变量 ta
var tb:Test?=null //定义类型参数为 String 的泛型类变量 tb
ta=tb //错误,提示:推断的类型是 Test?,但是需要的类型是 Test?
}
从上面的例题可以看到,虽然 ta 和 tb 都是 Test 类,但是不能互相赋值。也就是说,泛型类
定义对象时,类型参数不一样,定义的变量类型也是不一样的。
假设不是这样的设定,不管泛型类中的类型参数差别有多大,定义的变量都看做同一个类型,
那实际运用时,错误将非常多。例如 List类型,可以放入简单的 Int,也可以放入复杂的对象,
那 List 对象之间的差异非常明显,这些实例如果是同一个类型,那运用相同的方法时,肯定会产
生错误。
协变考虑的是定义的泛型变量的类型参数之间存在继承关系的情况,例如:
class Test //定义支持协变的泛型类
fun main() {
var ta:Test?=null
var tb:Test?=null
ta=tb //可以,因为类型参数 String 是 Any 的子类,所以 tb 向上转型
}
特点:
 out 关键字只能出现在泛型类/接口的泛型参数声明上,表示支持协变,不能出现在泛型
方法的泛型参数声明上。所以’out’修饰的泛型常常作为方法的返回而使用。
例如:
interface Product {
fun produce(): T
fun add(t: T) //编译器报错
}
 out 的另一层意思是只能输出,不能输入,与向上转型类似,基类不能操作子类中特有的元素,所以约定为只读。
例如:
//定义父类
open class Animal(val name: String) {
open fun sound() { println(“我是动物$name”) }
}
//定义子类
class Dog(name: String) : Animal(name) {
override fun sound() { println(“我是狗${name},汪汪汪”) }
}
//定义函数,参数类型为 List,表示此泛型支持协变
fun setAnimal(animalList: List) {
for (item in animalList) { //调用 List 中每个对象的 sound 方法
item.sound()
}
}
fun main() {
//创建 animalList1 对象,泛型参数为 Animal
val animalList1: MutableList = ArrayList()
animalList1.add(Animal(“ani”))
//创建 animalList2 对象,泛型参数为 Dog
val animalList2: MutableList = ArrayList()
animalList2.add(Dog(“dog22”))
// MutableList和 MutableList没有继承关系,都是 List 对象,但是
他们的泛型存在继承关系。且函数 fun setAnimal(animalList: List) 的
参 数 类 型 为 支 持 协 变 的 泛 型 , 所 以 , 可 以 将 MutableList 作 为
MutableList的子类来用。
setAnimal(animalList1)
setAnimal(animalList2)
}
注意:kotlin 中,List 默认实支持协变的泛型。所以即使将 out 拿掉,程序也可以正常运行。
2) 逆变(in)
 逆变:与协变情况类似,在实例化的对象之间存在类型向下转换,父类向子类转换(java
中不允许),用 in 表示。
特点
 泛型参数在使用了 in 关键字后,不能声明成 val 或者 rar 类型的变量。 用 in 关键字声明的泛型参数类型可以作为方法的参数类型,但是不能作为方法的返回值
类型。例如:
interface WritableList {
fun add(t: T): Int //允许
fun get(index: Int): T //不允许,这句代码的写法是错误的。
}
同样用协变中的简单例题来说明其应用:
class Test //声明支持逆变的泛型
fun main() {
var ta:Test?=null
var tb:Test?=null
tb=ta //泛型参数类型为父类的对象赋值给子类对象
}

14. 集合

14.1. 概述
kotlin 的集合类就是一个容器,用于存储一系列对象,这些对象可以是任意的数据类型,并
且长度可变。集合类都在 kotlin.collections 包内。
kotlin 中的集合按照其存储结构可以分为两大类,即单列集合(Collection)和双列(Map)
集合。

1) Collection
Collection 接口有 3 个常用子接口,分别是:List(有序可重复)、Set(无序不重复)和
MutableCollection(元素可变),MutalbleCollection 一般被 List 实现类和 Set 的实现类实现,
用于可变集合。
一般将实现 List 接口的对象称为 List 集合。在 List 集合中允许出现重复的元素, 所有的元
素是以一种线性方式存储的,在程序中可以通过索引来访问集合中的指定元素。
List 接口在 Kotlin 中有两个实现类,分别为 ArrayList(不可变)、MutableList(可变)。
Collection 是所有单列集合的父接口,因此在 Collection 中定义了单列集合(List、Set、
MutableCollection)通用的一些方法,这些方法可用于操作所有的单列集合,如下表所示:
方法 功能
add(element:E):Boolean 向集合中添加一个元素
addAll(element:Collection) 将指定 Collection 中的所有元素添加到该集合中
clear():Unit 删除该集合中的所有元素
remove(element:E):Boolean 删除集合中指定的元素
removeAll(element:Collection) 删除该集合中的所有元素
isEmpty(): Boolean 判断该集合是否为空属性:val size: Int 获取该集合中元素个数
… …
2) Map
Map 是双列集合的根接口,用于存储具有键(Key)、值(Value)映射关系的元素,每个元
素都包含一对键值,在使用 Map 对象时,可以通过指定的 Key 找到对应的 Value。其类的继承关
系如下:
3) 可变与不可变集合
根据可变性,集合分为可变集合和不可变集合。可变集合可以对集合中的元素进行增加、删
除的操作(mutableList)。不可变集合只提供只读操作(ArrayList)。
注意:val 定义的集合类型,只是不能改变其引用,若是可变集合,本身是可以改变的,例如
val 定义的可变集合,通过 add()函数添加元素是可以的。
注意:只读集合是协变的,可变集合不是。
14.2. 创建集合对象
可以使用 listOf()、setOf()、mapOf()函数创建不可变的 List 列表容器、Set 集容器、Map
映射容器;
可 以 使 用 mutableListOf() 、 mutableSetOf() 、 mutableMapOf() 函数来创建可变的
MutableList 列表容器、MutableSet 集容器、MutableMap 映射容器;
如下图所示:
Map
HashMap
LinkedHashMap
MutableMap例如:
//创建不可变 List
val list = ListOf(1,2,3,4,5,6) // val list: List=ListOf(),由于可以推导出类型,所以
可省略类型,下面的原理都是一样的
//创建可变 MutableList
val mutableList=mutableListOf(“a”,”b”,”c”)
//创建不可变 Set
val set = setOf(1,2,3,4,5,6)
//创建可变 MutableSet
val mutableSet = mutableSetOf(“a”,”b”,”c”)
//创建不可变 Map
val map = mapOf(1 to “a”, 2 to “b”)
//创建可变 MutableMap
val mutableMap = mutableMapOf( 1 to “a”, 2 to “b” )
注意,to 符号创建了一个短时存活的 Pair 对象,因此建议仅在性能不重要时才使用它。为避
免过多的内存使用,请使用其他方法。例如,可以创建可写Map 并使用写入操作填充它。apply()
函数可以帮助保持初始化流畅。
val map=mutableMapOf<String,String>().apply{ this[“one”]=”1”;this[“two”]=”2” }
如果创建没有元素的空 List,使用 listOf()即可。不过变量的类型不能省略,需要显式声明,
否则会报错。
val emptyList:List=listOf() //显式声明 List 的元素为 Int
具体类型构造函数
要创建具体类型的集合,例如 ArrayList 或 LinkedList,可以使用这些类型的构造函数。
类似的构造函数对于 Set 与 Map 的各实现中均有提供。
例如:
val linkedList = LinkedList(listOf(“one”,”two”, “three”))
val presizedSet = HashSet(32)
复制:
标准库中提供的 toList()、toMutableList() 、toSet()等创建了一个具有相同元素的新集合14.3. 遍历集合中的元素
List、Set 和 Map 类继承了 Iterable 接口,扩展了 forEach 函数来迭代遍历元素; 例如:
list.forEach{ … }
set.forEach{ … }
map.forEach{ … }
若要访问 index 下标,在 List 和 Map 对象中,可以使用 forEachIndex 函数,例如:
list.forEachIndex{ index,value->
println(“${index} , ${value}”)
}
第一个参数 index 是下标,第二个参数 value 是对应下标位置上的值。
Map 的元素是 Entry 类型,与 List 和 Set 不同,Map 没有下标,只有 key 和 value,由 entries
属性持有。
val entries: Set<Entry<K, V>>
这个 Entry 类型定义如下:
public interface Entry<out K, out V> {
public val key: K //键值对的 Key
public val value: V //键值对的 Value
}
可以直接访问 entries 属性获取该 Map 中的所有键/值对的 Set。
代码如下:
val map = mapOf(1 to “a”, 2 to “b”)
map.entries.forEach( print( it.key+it.value ) )
14.4. 查询和检索
操作主要有判断集合是否为空、集合中元素个数、返回集合中的元素迭代器、查询某个位置
的元素等,常用的方法有:
对象 查询方法 功能
List/Set/Map isEmpty():Boolean 集合是否为空
List/Set/Map val size:Int 集合中元素个数
List/Set/Map contains() 判断结合是否包含某个元素
List/Set iterator():Iterator 返回集合元素迭代器
List/Set get(index:Int):E 查询集合中某个位置的元素
List/Set add(element: E): Boolean 向集合中添加元素,成功返回 ture
List/Set add(index: Int, element: E):
Unit
在指定位置添加一个元素
List/Set remove(element: E): Boolean 移除集合中的元素,成功返回 tureList/Set removeAt(index: Int): E 移除指定索引处的元素
List/Set set(index: Int, element: E): E 用指定的元素替换集合中指定位置的元
素,返回该位置的原元素
Map get(key: K): V? 根据 key(键)获取 value(值),如果该
元素存在,则返回元素的值,否则返回 null
Map put(key: K, value: V): V? 将指定的 value 与映射中指定的 key 添加
到集合中
Map remove(key: K): V? 移除集合中指定的 Key 映射的元素
例如:
//List 对象的操作
val list: List = listOf(0, 1, 2)
if (list.isEmpty()) { //判断集合中元素是否为空
println(“集合中没有元素”)
return
} else {
println(“集合中有元素,元素个数为:” + list.size)
}
//Map 对象的操作
val muMap = mutableMapOf(1 to “江小白”, 2 to “小小白”, 3 to “江小小”)
muMap.put(4, “江江”)
println(“添加元素后的集合:” + muMap)
muMap.remove(4)
println(“删除元素后的集合:” + muMap)
println(“集合中元素的个数为:” + muMap.size)
}
14.5. 过滤函数
filter 函数,例如:
//创建 Student 类
data class Student(var id:Long, var name:String , var age:Int){

}
//创建 List 对象
val studentList = listOf(
Student(1,”jack”,18), Student(2,”beny”,18), Student(3,”rose”,19),
)//过滤出年龄大于等于 19 的学生
studengList.filter{ it.age>=18 }
14.6. 排序函数
升序:sorted()
倒序:reversed()
例如:
list.sorted() / list.reversed()
set.sorted() / set.reversed()
14.7. 元素去重
distinct()函数,例如:
val dupList = listOf(1, 1, 2, 2, 3, 3, 3)
dupList.distinct() //去重函数,返回 [1, 2, 3]

15. IO

15.1. 概述
Kotlin 的 I/O 操作的 API 在 kotlin.io 包下。Kotlin 的原则就是 Java 已经有的好用的类就直
接使用,没有的或者不好用的类,就在原有类的基础上进行功能扩展。例如 Kotlin 就给 File 类写
了扩展函数。
Kotlin 为 java.io.File 类扩展了大量好用的扩展函数,这些扩展函数都在 kotlin\io\File
ReadWrite.kt 源代码文件中。
同时,Kotlin 也针对 InputStream、OutputStream 和 Reader 等都做了简单的扩展。它们
主要在 kotlin\io\IOStreams.kt、kotlin\io\ReadWrite.kt 源代码文件中。
Koltin 的序列化直接采用了 Java 序列化类的类型别名:
internal typealias Serializable = java.io.Serializable
Kotlin 中常用的文件读写 API 如下表所示:
函数 功能
File.readText(charset:Charset=Charset.UTF_8):
String
读取文件内容,返回一个字符串
File.readLines(charset:Charset=Charset.UTF
_8):List
读取文件每一行,存入一个 List 返回
File.readByte():ByteArray 文件内容以 ByteArray 返回
File.writeText(text:String,charset:Charset=Char
set.UTF _8):Unit
text 字符串覆盖写入到文件中
File.writeBytes(array:ByteArray):Unit ByteArray 字节流数组覆盖写到文件File.appendText(text:String,charset:Charset=C
harset.UTF _8):Unit
文件末尾追加写入 text 字符串
File. appendBytes(array:ByteArray):Unit 文件末尾追加写入 ByteArray 字节流
数组
File 类:
File 类专门用来管理磁盘文件和文件夹(增、删、改名),而不负责数据的输入输出。每个 File
类对象表示一个磁盘文件或者文件夹。构建对象的方法为:
var file = File(pathname:String)
注意:在 Android 开发中,一般不会直接使用绝对路径。
15.2. 读文件
//通过 readText()函数读取一个文件,直接返回整个文件内容
fun getFileContent(filename:String):String{
val file=File(filename)
return file.readText( Charset.forName(“UTF-8”) ) //获取整个文件的内容,并返回
UTF-8 编码格式的字符串
}
接着使用 File 对象来调用 readText()函数即可获得该文件的全部内容,它返回一个字符串。
如果指定字符编码,可以通过传入参数 Charset 来指定,默认是 UTF-8 编码。
//通过 readLines()函数获取文件每行的内容
fun getFileLines(filename:String):List{ //返回每一行构成一个 String 元素的 List
val file=File(filename)
return file.readLines( Charset.forName(“UTF-8”) )
}
//通过 readBytes()函数读取字节流数组
//读取为 bytes 数组
val bytes: ByteArray = file.readBytes() //返回这个文件的字节数组
15.3. 写文件
写文件通常分为覆盖(一次性写入)和追加两种情况。

  1. writeText:覆盖写文件
    fun writeFile(text: String, destFile: String) {
    val f = File(destFile) //destFile 参数是目标文件名(带目录)。
    if (!f.exists()) {
    f.createNewFile()
    }
    f.writeText(text, Charset.defaultCharset()) //覆盖写入字符}
  2. appendFile:末尾追加写文件
    fun appendFile(text: String, destFile: String) {
    val f = File(destFile)
    if (!f.exists()) {
    f.createNewFile()
    }
    f.appendText(text, Charset.defaultCharset()) //追加写入内容
    }
  3. appendBytes:追加写入字节数组
    追加字节数组到该文件中方法签名:
    fun File.appendBytes(array: ByteArray)
    1. 遍历文件树
  4. walk 函数:遍历文件树
    fun traverseFileTree(filename: String) {
    val f = File(filename)
    val fileTreeWalk = f.walk()
    fileTreeWalk.iterator().forEach { println(it.absolutePath) }
    //遍历指定文件夹下
    }
    上面的测试代码将输出当前目录下的所有子目录及其文件。我们还
    可以遍历当前文件下所有的子目录文件,将其存入一个 Iterator 中:
    fun getFileIterator(filename: String): Iterator {
    val f = File(filename)
    val fileTreeWalk = f.walk()
    return fileTreeWalk.iterator()
    }

16. 线程

16.1. 同步和异步
例如:烧开水需要 15 分钟,洗碗需要 5 分钟,扫地需要 5 分钟,请问做完这三件事,总共
需要几分钟?
同步就是做完一件事情后才能进行下一件事。异步就是同时进行一个以上彼此目的不同的任
务,例如在烧水的时候,可以同时扫地和洗碗的工作。16.2. 回调
既然有了异步,那是不是所有的事情都可以通过异步去完成呢?考虑到一种情况,洗碗需要
热水,这时候就需要等烧水这个事情做完才可以洗碗了,也就是洗碗依赖于烧水什么时候执行完。
这个情况异步需要如何处理呢?
如果我们在水壶上安装一个汽笛,在水烧开时可以发出鸣叫。这样,在烧水的过程中伦然可
以去做其他的工作,这个汽笛就是异步中的回调机制。
这样的概念在程序设计中使用非常频繁,例如:
Android 中,渲染某个视图依赖于网络请求这个耗时操作的结果才能进行,所以主线程在发
起网络请求的时候,为网络请求配置了回调,以便在网络请求完成的时候,回调函数被调用,主
线程就可以继续用网络请求的结果渲染某个视图了,而等待网络请求结果的时候,主线程可以做
其他的事情。
所以,需要回调机制的原因就很明显了,就是因为不同的任务之间存在前后的依赖关系。
但是,函数回调也有缺陷,那就是代码结构过于耦合,遇到多重函数回调的嵌套,代码难以
维护。
16.3. java 的多线程
Java 的线程是通过 java.lang.Thread 类来实现的。
public class Thread implements Runnable //注意:Thread 类实现了 Runnable 接口,
所以 Thread 类也是 Runnable 的实现类
Java 的线程类是 java.lang.Thread 类。当生成一个 Thread 类的对象之后,一个新的线程就
被创建了(创建状态)。Java 中线程任务都是通过 Thread 对象的方法 run()来完成其操作的,方
法 run( )称为线程体,注意:线程必须要通过 Thread 对象的 start()方法才能启动。
下面是构建线程对象几种常用的方法:
 public Thread() public Thread(String name)
 public Thread(Runnable target) //Thread 类实现了 Runnable 接口,所以 Runnable
的实现类对象可以作为参数来构建 Thread 对象
 public Thread(Runnable target, String name)
参数 target 是一个实现 Runnable 接口的实例,它的作用是实现线程体的 run()方法。目标
target 可为 null,表示由 Thread 本身实例来执行线程。name 参数指定线程名字,但没有指定
的构造方法,线程的名字是 JVM 分配的,例如 JVM 指定为 thread-1、thread-2 等名字。
Java 线程对象生成的两种方法:

1) 生成一个 Thread 类的子类,并在子类中重写其 run 方法(构造函数 1、2)。
2) 生成一个 Runnable 接口实现类的对象(构造函数 3、4),用此对象来实例化 Thread 对

16.4. kotlin 中的线程
在 kotlin 中,创建线程的方式与 java 中类似,分别如下:

1) 继承 Thread 类
object : Thread() {
override fun run() {
println(“running from Thread: ${Thread.currentThread()}”)
}
}.start()
2) 使用 Runnable 类的实例初始化 Thread 对象
Thread{ //lambda 表达式代替 Runnable 对象
println(“running from lambda: ${Thread.currentThread()}”)
}.start()
这里并没有看到 Runnable 对象,其实是被 lambda 表达式代替了。因为 lambda 可以传
给任何期望函数式接口的方法,而在此处,编译器会自动把它转换成一个 Runnable 实例。效
果等同于:
object :Runnable{
override fun run() {
println(“running from lambda: ${Thread.currentThread()}”)
}
}
顺序代码结构是阻塞式的,每一行代码的执行都会使线程阻塞在那里,但是主线程的阻塞
会导致很严重的问题, 这就要求所有的耗时操作不能在主线程中执行,所以就需要多线程来执
行。
通常线程切换的工作是由异步函数内部完成的,即通过回调的方式异步调用外界注入的代码。也就是说,异步回调其实就是代码的多线程顺序执行。
那么能不能既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行呢?有没有
帮助我们自动地完成线程的切换工作的功能呢?这个就是 Kotlin 协程的作用了。

17. 协程(Coroutine)

添加依赖(注意版本变化):
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9’
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9’
上一个是 java 环境下的,下面一个是 android 环境下的。
或者通过可视化方式添加
17.1. kotlin 的协程
第一个例子
假设需要用 30 分钟进行煮饭,打电话给请朋友吃饭,朋友需要 50 分钟才能到你家。那么,
是不是你需要消耗 30 + 50 = 80 分钟?
实际上,在现实中只需要消耗 50 分钟就可以了,先给朋友打电话,让他现在出门,把米淘洗
干净,放进电饭煲并打开电源,然后要做的就是等待。
第二个例子
假设需要完成语文试卷,数学试卷和英语试卷。每张试卷需要做 1 小时。于是需要 1 + 1 + 1
= 3 小时来完成所有的试卷。
能异步与不能异步
在第一个例子里面,煮饭、等朋友有一个共同点,就是每个操作看似耗时很长,但真正需要
人去操作的只有很少的时间,剩下的大部分时间都不需要人来操作,都是等待即可。
再看第二个例子,每一张试卷都会占用整个你,没有等待的时间,所以必须一张一张试卷完
成。
这两个例子实际上对应了两种程序类型:I/O 密集型程序和计算密集型程序。
在使用 requests 请求 URL、查询远程数据库或者读写本地文件的时候,就是 I/O 操作。这
些操作的共同特点就是要等待。大量的时间浪费在等待网站返回数据。如果可以充分利用这个等
待时间,就能发起更多的请求。而这就是异步请求为什么有用的原因。
但对于需要大量计算任务的代码来说,CPU 始终处于高速运转的状态,没有等待,所以就不
存在利用等待时间做其它事情的说法。
所以:异步只适用于 I/O 操作相关的代码,不适用于非 I/O 操作。
线程会因为 Thread.sleep 方法而进入阻塞状态(就是什么也不会执行),这样就浪费资源了。
能不能将代码块打包成一个个可执行片段,由一个统一的分配器去分配到线程上去执行呢?例如
某个代码块里要求 sleep 一会,那么就去执行别的代码块,等会再来执行。
协程就是这样一个东西,作为使用者不需要再去考虑创建一个新线程去执行一段代码,也不
需要关心线程怎么管理,只需要关心要异步执行一段代码,且要拿到执行的结果,若要异步执行很多段代码,则要按某种顺序或者某种逻辑得到它们的结果。
所以,协程(Coroutine)是一个并发的设计模式,能通过它使用更简洁的代码来解决异步问
题。协程的内部也使用到了线程(对线程进行了更为方便的封装),但它更大的作用是它的设计思
想,协程去除了传统的 Callback 回调方式,将异步编程趋近于同步对齐。我们只需通过协程即可
完成线程切换等问题,非常方便。
协程主要有以下两个特点。
• 协程挂起:表示协程中执行的任务从协程所在的线程剥离,转到协程所指定的线程去执行
其任务,而当前线程继续执行自己的任务(就像在当前线程外挂了一个协程),这个剥离就叫做挂
起。协程挂起之后,原先所在的线程继续执行自己的任务,不会受到任何影响,所以称为非阻塞
挂起。注意:协程中“挂起”的对象不是线程,也不是函数,挂起的对象是协程。
• 简化代码:协程让原来使用“异步+回调”方式写出来的复杂代码,简化成可以用看似同
步的方式表达。
启动一个简单的协程:
GlobalScope.launch{
delay(1000L)
println(“Hello,World!”)
}
上述代码使用 GlobalScope.launch 方法启动了一个协程,GlobalScope 为协程作用域接口
的实现类,launch 后面的花括号就是协程,花括号内的代码就是运行在协程内的代码。
17.2. 挂起函数
suspend 修饰的函数称为挂起函数。注意:挂起函数只能在协程或者其他挂起函数中被调用,
即挂起函数必须直接或者间接地在协程中执行。
一个协程内有多个 suspend 修饰的方法顺序书写时,执行的顺序按照方法的排列顺序。若有
协程挂起时,线程不会空闲,而是继续往下运行,而前面挂起的那个协程在挂起结束后不会马上
运行,而是等待当前正在运行的协程运行完毕后再去执行。
所以说,协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理
解为协程中的代码重新进入协程所在线程的过程。协程就是通过的这个挂起恢复机制进行线程的
切换。
普通函数的类型是()-> Unit,但是加了 suspend 后,函数的类型为:suspend()-> Unit。常
用的 delay、withcontext 等函数就是内置的挂起函数。
例如:
fun main(args:Array){
println(“Start ${Thread.currentThread().name}”) //输出启动时的线程名
CoroutineScope(Dispatchers.Default).launch{ //主线程中,启动一个协程
delay(100) //挂起函数,协程挂起,但不影响线程运行println(“current ${Thread.currentThread().name}”) //等待延迟后执行
}
println(“End ${Thread.currentThread().name}”) //回到主线程,输出线程名
Thread.sleep(500) //保证 main 线程存活,确保上面两个协程运行完成
}
上述代码中通过 GlobalScope.launch 启动了一个协程,程序运行的结果如下:
从上面的例题可以看到,协程代码中,主线程并没有被阻塞,在挂起后,输出发生在主线程
中,说明协程的代码切换到其他线程之后,又自动切回了主线程,而这正是我们所需要的效果。
这就是“既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行”的“顺序编写异步
代码的效果”。顺序编写保证了逻辑上的直观性,协程的自动线程切换又保证了代码的非阻塞性。
如何做到挂起的
如果仅仅在函数前面加一个 suspend,那函数就能被挂起吗?
答案当然是不能的,所以 supend 关键字并不起到协程挂起/切换线程的作用。对于自定义挂
起函数,需要在该函数内部直接/间接调用到某一个自带的挂起函数才行,例如 withContext()、
delay()等。
具体的例题分析见下面的“协程切换”小节。
17.3. CoroutineScope 协程作用域

1) 概述
在 Android 环境中,通常每个界面(Activity、Fragment 等)启动的协程只在该界面有意
义,如果用户在等待 Coroutine 执行的时候退出了该界面,则不需要再继续执行此 Coroutine 了。
所以 Coroutine 在设计的时候,要求在一个范围内执行。例如,在 Android 中使用协程来请求
数据,当接口还没有请求完成时 Activity 就已经退出了,这时如果不停止正在运行的协程将会造
成不可预期的后果。所以要使用 Coroutine 须先确定一个协程的作用域(好比变量的作用域),
然后根据此来确定运行的协程。
CoroutineScope(协程作用域)用于管理协程,管理的内容有:
 启动协程的方式:它定义了 launch、async、withContext 等协程启动方法(以 extention
的方式),并在这些方法内定义了启动子协程时上下文的继承方式。
 管理协程生命周期 - 它定义了 cancel()方法,用于取消当前作用域,同时取消作用域内
所有协程。
CoroutineScope 是一个接口,定义为:
public interface CoroutineScope {
//协程运行环境(上下文,包含如 dispatcher 等对象) public val coroutineContext: CoroutineContext
}
coroutineContext:
可以看到,CoroutineScope 接口中仅定义了一个 CoroutineContext 属性,即协程的运行
环境,它包含一个协程运行所需的各种参数,包括协程调度器(dispatcher)、代表协程本身的 Job
(任务管理器)、协程名称、协程 ID 等。CoroutineContext 定义为一个带索引的集合,集合的元
素为 Element,上面所提到调度器、Job 等都实现了 Eelement 接口。
由于 CoroutineContext 被定义为集合,因此在实际使用时可以自由组合加减各种上下文元
素。例如:CoroutineScope(Dispatchers.Main),参数调度器表示的就是一个元素,由此元素构
成 CoroutineContext 对象。
CoroutineScope 是提供 CoroutineContext 的容器,保证 CoroutineContext 能在整个协
程运行中传递下去,约束 CoroutineContext 的作用边界。所以,作用域用于管理协程;而上下
文只是一个记录协程运行环境的集合。
所以,GlobalScope.launch{ }代码中的 GlobalScope 实现了 CoroutineScope 接口,launch()
是 CoroutineScope 的一个扩展函数,功能是启动一个协程,在 CoroutineScope 上还有很多
扩展函数,比如:async、actor、cancel 等。
2) CoroutineScope
 GlobalScope
GlobalScope 它是 CoroutineScope 的一个单例对象,表示此协程的生命周期伴随应用程序
的生命周期,在该作用域启动的协程为顶层协程。
普通协程会受到外层的一个作用域的生命周期的影响,而 GlobalScope 所创建的协程为顶层
协程,没有外部作用域,所以不受外层的影响。且该 CoroutineScope 没有 Job 对象(管理任务器),
就无法对它执行 cancel()操作,如果不手动取消每个任务,会造成这些任务一直运行,可能会导
致内存泄露等问题。
所以 Android 中一般而言不直接使用 GlobalScope 来创建 Coroutine。
 CoroutineScope( Dispatchers.xxx )方法
此方法为内置的工厂方法(不是构造函数)。Dispatchers 为调度器,其作用是指定协程运行
在某一特定线程、线程池中或不指定(自动选择)运行的线程。
协程调度器 有 : Dispatchers.Default 、 Dispatchers.IO 、 Dispatchers.Main 和
Dispatchers.Unconfined。
 Dispatchers.Default:默认线程池,核心线程和最大线程数依赖 cpu 数量,适合 CPU
密集型的任务,比如解析 JSON 文件,排序较大的 list。
 Disspatchers.IO:工作线程池,针对磁盘和网络 IO 进行了优化,适合 IO 密集型的
任务,比如:读写文件,操作数据库以及网络请求。
 Dispatchers.Main:主线程,在主线程中操作 UI,防止 UI 阻塞,如果没有 UI 的话
一般没有必要使用。 Dispatchers.Unconfined:非受限调度器,会根据运行时的上线文环境决定。
例如:CoroutineScope(Dispatchers.Main),就是指定该 CoroutineScope 启动的协程在主
线程执行。
17.4. 启动协程
在 Kotlin 程序中,启动一个协程的方式有很多种,常见的如下所示:
 CoroutineScope.launch():Job 函数:异步启动一个不会阻塞当前线程的 Coroutine,返
回一个 Job 对象(不需要 return 语句)用于控制这个 Coroutine。注意,函数必须通过
CoroutineScope 实 例 才能调用 , 创 建 方 法 如 上 一 小 节 所 述 , 如 :
CoroutineScope( Dispatchers.xxx )。
 CoroutineScope.async:Deferred:async 用于启动一个异步的协程任务,默认返回一个
Deferred(延迟)对象(不需要 return 语句),Deferred.await()用于得到协程任务结束
时返回的结果(最后一行的值)。同样的,函数必须通过 CoroutineScope 实例才能调用。
 runBlocking:T 函数:创建一个 会 阻 塞 当 前 线 程 的 Coroutine , 此方法 不 是
CoroutineScope 的扩展方法,可以独立使用。runBlocking 域中可以有多个协程,多个
协程可以并发进行,不会等待子协程执行结束通常只用于启动最外层的协程,例如线程环
境切换到协程环境。
注意,launch、async 有默认返回值,所以不能使用 return。

  1. CoroutineScope.launch()函数创建协程
    最常用的方式是通过 launch()函数来启动协程。launch()函数如下:
    public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit ): Job
    参数:
     context: CoroutineContext :协程的上下文,含当前 CoroutineScope 的信息,默认
    值 EmptyCoroutineContext 表示一个空的协程上下文。
     start: CoroutineStart :CoroutineStart 枚举类型,用来指定 Coroutine 启动的选项。
    有如下几个取值:
     DEFAULT:(默认值)立刻安排执行该 Coroutine 实例
     LAZY:延迟执行,只有当用到的时候才执行
     ATOMIC:类似 DEFAULT,区别是当 Coroutine 还没有开始执行的时候无法取消
     UNDISPATCHED:立刻在当前线程执行直到遇到第一个 suspension point 。然后
    当 Coroutine 恢 复 的 时 候 , 继 续 在 suspension 的 context 中设置的
    CoroutineDispatcher 中执行。
     block: suspend CoroutineScope.() -> Unit :一个 suspend 函数,这个就是协程中要执行的代码块, 注 意 , 从 CoroutineScope.() 可以看到,此 block 函数定义为
    CoroutineScope 的扩展函数,所以在代码块中可以直接访问 CoroutineScope 对象(即
    this 对象)。在实际使用过程中通常使用 lambda 表达式,也称之为 Coroutine 代码块,
    在此代码块中完成各种异步操作。
    返回值:
    函数返回一个 Job 类型(Job 为一个接口)的对象,Job 是协程任务的接口,里面定义了协
    程的状态字段、cancel 方法、attach 方法等;该对象代表了这个刚刚创建的 Coroutine 实例。
    基本上每启动一个协程就会产生对应的 Job。
    Job 对象有 6 种不同的状态(New: 创建、Active: 运行、Completing: 已经完成等待自身
    的子协程、Completed: 完成、Cancelling: 正在进行取消或者失败、Cancelled: 取消或失败)
    这六种状态 Job 对外暴露了三种状态,可以通过 Job 对象获取:
     val isActive: Boolean:运行状态
     val isCompleted: Boolean:完成状态
     val isCancelled: Boolean:取消状态
    所以如果需要手动管理协程,可以通过下面的方式来判断当前协程是否在运行。
    while (job.isActive) {
    // 协程运行中
    }
    通过这个 job 对象可以控制这个 Coroutine 实例,比如:
     job.start():启动协程,除了 lazy 模式,协程都不需要手动启动
     job.join(): 要求当前协程等待 job 执行完成之后再继续执行
     job.cancel():取消一个协程
     job.cancelAndJoin():等待 job 协程执行完毕然后再取消
    一般来说,协程创建的时候就处在 Active 状态,但也有特例。例如通过 launch 启动协程的
    时传递的 start 参数默认值是启动的,如果 start 传递的是 CoroutineStart.LAZY,那么它将处于
    New 状态。可以通过调用 start 或者 join 来唤起协程进入 Active 状态。
    例如:
    val job = CoroutineScope(Dispatchers.Unconfined).launch{ //启动协程
    var i = 1
    while(true) {
    println(“$i little sheep”) //开始数羊

++i
delay(500L) //每半秒数一只, 一秒可以输两只
}
}
Thread.sleep(1000L) //在主线程睡眠期间, 协程里已经数了两只羊job.cancel() //协程才数了两只羊, 就被取消了
Thread.sleep(1000L)
2. async+await 函数创建协程
launch 方法的作用是建立协程并当即启动, 返回的是 Job 对象,但是 Job 为协程自身,用于
管理协程,却无法携带协程中处理的值。
async 和 await 是两个函数,它们一般一起使用,async 函数用于启动一个异步协程,并返
回 Deferred 对象,Deferred 是 Job 的子类,但 Deferred 有个 await 方法, 调用它可获得协程中
处理的值。
public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
//launch 方法:block: suspend CoroutineScope.() -> Unit ): Job
block: suspend CoroutineScope.() -> T): Deferred
async 的特点是不会阻塞当前线程,但会阻塞所在协程。
注意: await 是 suspend 方法, 但 async 不是, 因此 async 能够在协程外面调用。 async 只
是启动了协程, 而不会引发协程挂起, 传给async的lambda(也就是协程体)才可能引发协程挂起。
例如:
fun main(…) {
CoroutineScope(Dispatchers.Unconfined).launch{
val deferred1 = async{ requestDataAsync() }
// await 方法为 suspend 方法,所以必须在协程中调用
println(“data=${deferred1.await()}”)
}
Thread.sleep(10000L)}
//创建挂起函数
suspend fun requestDataAsync(): String {
delay(1000L)
return “data1” //函数返回值
}
async 和 launch 的区别:
 aunch 更多是用来发起一个无需结果的耗时异步任务,这个工作不需要返回结果。
 async 函数则用于执行耗时并且需要返回值(如网络请求、数据库读写、文件读写)的异
步任务,在执行完毕通过 await() 函数获取处理值。17.5. 协程切换(withContext)
17.5.1.概述
在 suspend 函数执行完成之后,协程会自动把线程再切回来。那么是 suspend 关键字进行
的线程切换吗?怎么指定切换到哪个线程呢?对此可以做一个简单的试验:
GlobalScope.launch(Dispatchers.Main) {
println(“Hello ${Thread.currentThread().name}”)
test() //调用自定义的挂起函数
println(“End ${Thread.currentThread().name}”)
}
suspend fun test(){ //自定义一个挂起函数
println(“World ${Thread.currentThread().name}”)
}
执行结果为:Hello main -> World main -> End main,也就是说这个 suspend 函数仍然
运行在主线程中,suspend 并没有切换线程的作用。
可以通过 withContext 方法来在 suspend 函数中进行线程的切换,withContext 函数为:
suspend fun withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T (source)
函 数 有 两 个 参 数 , 参 数 1 的 取 值 可 以 是 Dispatchers.Default 、 Dispatchers.IO 、
Dispatchers.Main 和 Dispatchers.Unconfined。参数 2 可以是 lambda 函数,用于耗时操作。
例如:
GlobalScope.launch(Dispatchers.Main) {
println(“Hello ${Thread.currentThread().name}”)
val def = withContext(Dispatchers.IO){ //线程切换
println(“World ${Thread.currentThread().name}”)
val str = “my value”
}
println(“End ${Thread.currentThread().name}”)
println(def.await())
}
执行的结果为:Hello main  World DefaultDispatcher-worker-1  End mainmy
value,这说明 withContext 函数的确运行在不同的线程之中了,所以:
 withContext 方法进行了线程切换的工作
 withContext 是 suspend 函数,必须在协程中进行调用 函数最后一行语句的值为 Deferred.await()所获取的值
那么 suspend 关键字有什么用处呢?
其实,suspend 关键字只起到了标志这个函数是一个耗时操作,必须放在协程中执行的作用。
如果创建一个 suspend 函数,但是内部不包含正真的挂起逻辑,编译器会提醒: Redundant
‘suspend’ modifier ,即这个关键字是多余的。因为这个函数并不会发生挂起,那这个
suspend 只有一个效果:限制此函数只能在协程中被调用,如果在非协程中调用,则编译不会通
过。
所以,创建一个 suspend 函数,为了让他包含挂起,要在内部直接或者间接调用 Kotlin 自
带的 suspend 函数,这个时候函数才是有意义的,线程切换有多个方法,但是 withContext 是最
常用的一个。
所以,withContext()函数可以指定线程来执行代码,并且执行完毕后再自动切换回来,继
续执行原先线程里的后续代码。
17.5.2.如何在主线程更新界面
针对界面的操作只能在主线程中进行,例如:
coroutineScope(Dispatchers.IO).launch{
val image = getImage(imageId)
coroutineScope(Dispatchers.Main).launch {
avatarIv.setImageBitmap(image)
}
}
好像有点不对劲?这不还是有嵌套嘛。
如果只是使用 launch 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的
函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动
把线程切回去继续执行。那么可以将上面的代码写成这样:
coroutineScope(Dispatchers.Main).launch { // 在 UI 线程开始
//切换到 IO 线程,执行完成后切回 UI 线程
val image = withContext(Dispatchers.IO) {
getImage(imageId) //运行在 IO 线程
}
avatarIv.setImageBitmap(image) //自动回到 UI 线程更新 UI
}
//或者:withContext 放进一个单独的函数,然后在 UI 线程中调用
launch(Dispatchers.Main) {
val image = getImage(imageId) //调用 withContext 函数
avatarIv.setImageBitmap(image)}
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {

}
这就是“用同步的方式写异步的代码”。
17.6. 协程取消
比如 Android 中的网络请求等资源数据的加载,需要在页面关闭时中断
kotlin 协程的取消规则是这样的:
 父协程调用 cancel(),会取消自己以及所有子(内部)协程。
 子协程调用 cancel(),默认不会取消父协程。
 可以通过调用 CoroutineScope 的 cancel()方法,取消掉该 scope 产生的所有协程。
例如:
val job = launch {
while (isActive) {
log(“launch”)
}
}
delay(1000)
job.cancel()

18. 内置拓展函数(let, with, run, apply, also)

和 Java 相比,Kotlin 中额外提供了不少高级语法特性,这些高级特性中,定义于 Kotlin 的
Standard.kt,提供了一些内置拓展函数以便写出更优雅的代码。
18.1. let
定义:fun <T, R> T.let( block: (T) -> R ): R
功能:调用对象(T)的 let 函数,则该对象为函数的参数。在函数内可以通过 it 指代该对
象。返回值为函数的最后一行或指定 return 表达式。
any.let {
// 用 it 指代 any 对象
// todo() 是 any 对象的共有属性或方法
// it.todo() 的返回值作为 let 函数的返回值返回
it.todo()
}
let 在使用中可用于空安全验证,变量?.let{}
// 另一种用法any?.let {
it.todo() // any 不为 null 时才会调用 let 函数
}
18.2. with
和 let 类似,又和 let 不同,with 最后也包含一段函数块,也是将最后的计算的结果返回。
但是 with 不是以拓展的形式存在的。其将某个对象作为函数的参数,并且以 this 指代。
一般结构:
whith(any) {
// todo() 是 any 对象的共有属性或方法
// todo() 的返回值作为 with 函数的返回值返回
todo()
}
例如,用 with 设置 TextView,由于代码块中传入的是 TextView(this),而不是 it,那么就
可以直接写出函数名/属性来进行相应的设置:
if (textView == null) return //若 TextView 组件为 null 则进行如下设置
with(textView) {
text = “TextSetInTextView” //textView.text 属性
setTextColor(ContextCompat.getColor(this@TestActivity, R.color.colorAccent))
textSize = 18f
}
这段代码唯一的缺点就是要事先判空了,有没有既能像 let 那样能优雅的判空,又能写出
这样的便利的代码呢?那就是下面的 run
18.3. run
run 函数基本是 let 和 with 的结合体,对象调用 run 函数,接收一个 lambda 函数为参
数,传入 this 并以闭包形式返回,返回值是最后的计算结果。
一般结构:
any.run {
// todo() 是 any 对象的共有属性或方法
// todo() 的返回值作为 run 函数的返回值返回
todo()
}
TextView 设置各种属性的优化写法如下:
textView?.run {
text = “TextSetInTextView”
setTextColor(ContextCompat.getColor(this@TestActivity, R.color.colorAccent)) textSize = 18f
}
18.4. apply
apply 函数和 run 函数很像,但是 apply 最后返回的是调用对象自身。
一般结构:
val result = any.apply {
todo() // todo() 是 any 对象的共有属性或方法
3 * 4 // 最后返回的是 any 对象,而不是 12
}
println(result) // 打印的是 any 对象
18.5. also
also 和 let 函数类似,唯一的区别就是 also 函数的返回值是调用对象本身
val result = any.also {
// 用 it 指代 any 对象
// todo() 是 any 对象的共有属性或方法
it.todo()
3 * 4 // 将返回 any 对象,而不是 12
}
具体的调用情况见下图:

19. 关键字

19.1. ?的含义
在 kotlin 中单独使用?表示可为空;var result = str?.length
19.2. ?:的含义
表示三元操作符(即三目运算符)
var str : String? = null
var result = str?.length ?: -1
//等价于
var result : Int = if(str != null) str.length else -1
19.3. :(冒号)
类型和超类型之间的冒号前要有一个空格(>:产生歧义),而实例和类型之间的冒号前不要有空
格:
interface Foo : Bar {
fun foo(a: Int): T
}
19.4. ::(双冒号)
表示把一个方法当做一个参数,传递到另一个方法中进行使用,就是引用一个方法。
19.5. ::class
反射中获得 class 的实例,val clazz = Hello::class.jav,Hello::class 获取的是 Kotlin 的
KClass
19.6. !!(双叹号)
非空断言,表示一定不能为空,
19.7. abstract :
抽象类 一个类或一些成员可能被声明成 abstract 。一个抽象方法在它的类中没有实现方法。
注意:抽象类或函数默认是 open 的,不用再添加。
19.8. any:
相当于 Java 中的 Object
19.9. by
类委托、属性委托19.10. companion object 伴生对象:
类内定义的伴生对象,相当于 java 中的 static 成员
companion object {
lateinit var instance: App
}
19.11. const:
编译期常量
19.12. constructor
用于标识构造函数
19.13. data class:
只包含数据字段的类,编译器自动成了如下方法:
equals()/hashCode()、toString()方法、componentN()方法、copy()方法
19.14. indices:
数组或 list 的索引属性:
val array=arrayOf(“a”,”b”,”c”)
for( i in array.indices ) println( i ) //分行输出 0,1,2
19.15. inline:
内联函数
19.16. inner:
内部类
19.17. internal:
internal 声明,在同一模块中的任何地方可见
19.18. is:
自动类型转换
19.19. lateinit var 延迟初始化属性:
lateinit 修饰的变量/属性不能是原始数据类型。
19.20. by lazy 懒属性(延迟加载)
val p: String by lazy {// 生成 string 的值
}
lateinit 只用于 var,lazy 只用于 val
lateinit var 只是让编译期忽略对属性未初始化的检查,后续在哪里以及何时初始化还需要开
发者自己决定。
by lazy 真正做到了声明的同时也指定了延迟初始化时的行为,在属性被第一次被使用的时候
能自动初始化。
19.21. object(用于创建单例模式):
object Resource {
val name = “Name”
}
19.22. open 说明可以被继承 :
19.23. override
19.24. reified:
作为 Kotlin 的一个方法泛型关键字,代表可以在方法体内访问泛型指定的 JVM 类对象
19.25. sealed 密封类:
声明密封类需要在 class 前加一个 sealed 修饰符。
19.26. tailrec(尾递归):
作用:修身尾递归函数。与普通递归相比,编译器会对尾递归进行修改,将其优化成一个快
速而高效的基于循环的版本,这样减少内存消耗。
19.27. Unit 类型:
如果函数返回 Unit 类型,该返回类型应该省略:
19.28. var 和 val:
定义可读写和只读变量
19.29. typealias:
定义类型别名
19.30. vararg :
可变参数。一个函数最多只能有一个可变参数。19.31. when 用于判断 相当于 java 中的 switch()语句
19.32. 属性修饰符
annotation //注解类
abstract //抽象类
final //类不可继承,默认属性
enum //枚举类
open //类可继承,类默认是 final

1
2


-------------------本文结束 感谢您的阅读-------------------