在 Kotlin
中的变量、常量以及注释多多少少和 Java
语言是有着不同之处的。下面详细的介绍 Kotlin
中的变量、常量、注释的使用,并且和 Java
的对比。
Kotlin 基础语法
函数定义
函数定义使用关键字 fun
,参数格式为:参数:类型。例如:
// fun 函数名(参数:类型):返回值类型{}
fun sum(a: Int, b: Int): Int{
return a+b
}
表达式作为函数体,返回类型自动推断:
fun sum(a: Int, b:Int) = a + b
public fun sum(a: Int, b: Int): Int = a+b // public方法则必须明确写出返回类型
无返回值的函数(类似Java中的 void)
fun printSum(a: Int, b: Int): Unit{
print(a+b)
}
// 如果是返回 Unit 类型,则可以省略(对于public方法也是这样)
public fun printSum(a: Int, b: Int){
print(a+b)
}
可变长参数函数
函数的变长参数可以用 vararg
关键字进行标识:
fun vars(vararg v: Int){
for(vt in v){
print(vt)
}
}
// 测试
fun main(args: Array<String>) {
vars(1,2,3,4,5) // 输出12345
}
lambda(匿名函数)
lambda表达式使用实例:
fun main(args: Array<String>) {
val sumLambda: (Int,Int) -> Int = {x,y -> x+y}
println(sumLambda(1,2))
}
定义常量与变量
可变变量定义: var
关键字
var 标识符:类型 = 初始化值
不可变变量定义: val
关键字,只能赋值一次的变量(类似Java中 final
修饰的变量)
val 标识符:类型 = 初始化值
常量与变量都可以没有初始化值,但是在引用前必须初始化
编译器支持自动类型判断,即声明时可以不指定类型,由编译器判断
val a: Int = 1
val b = 1
val c: Int
c = 1
var x = 5
x += 1
注释
Kotlin
支持单行和多行注释,实例如下:
// 这是一个单行注释
/*
*这是一个多行的块注释 **/
与Java不同,Kotlin中的块注释允许嵌套。
字符串模板
$
表示一个变量名或者变量值
$varName
表示变量值
${varName.fun()}
表示变量的方法返回值
var a = 1
// 模板中的简单名称:
val s1 = "a is $a"
a = 2
// 模板中的任意表达式
val s2 = "${s1.replace("is", "was")}, but now is $a"
NULL 检查机制
Kotlin的安全设计对于声明可为空的参数,在使用时要进行空判断处理,有两种处理方式,字段加 !!
像 Java一样抛出空异常,另一种字段后加 ?
可不做处理返回值为null或配合 ?:
做空判断处理。
//类型后面加?表示可为空
var age: String? = null
//抛出空指针异常
val ages = age!!.toInt()
//不做处理返回 null
val ages1 = age?.toInt()
//age为空返回-1
val ages2 = age?.toInt() ?: -1
println(age) // null
println(ages) // Exception in thread "main" kotlin.KotlinNullPointerException
println(ages1) // null
println(ages2) // -1
当一个引用可能为 null 值时,对应的类型声明必须明确地标记为可为 null。
当 str 中的字符串内容不是一个整数时,返回 null:
fun parseInt(str: String): Int?{
// ...
}
以下实例演示如何使用一个返回值可为 null 的函数:
fun main(args: Array<String>) {
if (args.size < 2){
print("Two integers expected")
return
}
val x = parseInt(args[0])
val y = parseInt(args[1])
// 直接使用 x*y 会导致错误,因为它们可能为 null
if (x != null && y != null){
// 在进行过 null 值检查后,x 和 y的类型会被自动转换为非 null变量
print(x * y )
}
}
类型检测及自动类型转换
我们可以使用 is
运算符检测一个表达式是否某类型的一个实例(类似于Java中的 instanceof
关键字)
fun getStringLength(obj: Any): Int? {
if(obj is String){
// 做过类型判断后,obj会被系统自动转换为String类型
return obj.length
}
// 在这里还有一种方法,与Java中 instanceof 不同,使用 !is
if (obj !is String){
// XXX
}
// 这里obj仍然是 Any 类型的引用
return null
}
区间
区间表达式由具有操作符形式 ..
的rangeTo 函数辅以 in 和 !in 形成。
区间是为任何可比较类型定义的,但对于整型原生类型,它有一个优化的实现。以下是使用区间的一些示例:
fun main(args: Array<String>) {
print("循环输出:")
for (i in 1..4) print(i)
println("\n ---------------------")
print("设置步长:")
for (i in 1..6 step 2) print(i)
println("\n ---------------------")
print("使用downTo:")
for(i in 6 downTo 1 step 2) print(i)
println("\n ---------------------")
print("使用until:") // 不包括结束元素
for (i in 1 until 4) print(i)
}
/* Output:
循环输出:1234
---------------------
设置步长:135
---------------------
使用downTo:642
---------------------
使用until:123
* */
Kotlin 基本数据类型
比较两个数字
Kotlin 中没有基础数据类型,只有封装的数字类型,你每定义的一个变量,其实Kotlin帮你封装了一个对象,这样可以保证不会出现空指针。数字类型也一样,所以在比较两个数字的时候,就有比较数据大小和比较两个对象是否相同的区别了。
在Kotlin中, ===
表示比较对象地址, ==
表示比较两个值大小。
fun main(args: Array<String>) {
val a: Int = 10000
val b: Int = 10000
println(a === b) // true, 对于运行时表示为原生类型的值(例如Int), === 相等检测等价于 == 检测
val boxA: Int? = 10000
val boxB: Int? = 10000
println(boxA === boxB) // 经过了装箱,创建了两个不同的对象
}
/* Output:
true
false* */
注解:在看Kotlin的基本类型时,文档提到,Kotlin中所有东西都是对象;并且数字在 Java 平台是物理存储为 JVM 的原生类型,除非我们需要一个可空的引用(如 Int?)或泛型。 后者情况下会把数字装箱。
fun testPacking() {
val a: Int = 10000
println(a === a) // 输出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA === anotherBoxedA) // !!!输出“false”!!!
}
fun testPacking2() {
val a: Int = 100
println(a === a) // 输出“true”
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA === anotherBoxedA) // !!!输出“true”!!!
}
上面的代码中,a的值不同,造成的结果也不同;查看一下Kotlin编译出的Java字节码,选中IDEA上面菜单栏的 Tools —> Kotlin —> Show Kotlin Bytecode,在打开的Kotlin Bytecode窗口中点击 Decompile。
所以会发现,Kotlin文档的示例代码中说的不一定保留同一性,表示的是数字范围在[-128,127]中的,引用相同。而不在该范围则是每次创建一个新的对象。
参考文章:Kotlin的装箱操作
类型转换
由于不同的表示方式,较小类型并不是较大类型的子类型,较小的类型不能隐式转换为较大的类型。这意味着,在不进行显示转换的情况下,我们不能把 Byte 型值赋给一个Int变量。
val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b // 错误
我们可以代用其 toInt()
方法
val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b.toInt() // OK
每种数据类型都有下面的这些方法,可以转化为其它的类型:
toByte(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
toChar(): Char
位操作符
对于 Int 和 Long类型,还有一系列的位操作符可以使用,分别是:
shl // 左移位 a shl 2 (a << 2 in Java)
shr // 右移位 a shr 2 (a >> 2 in Java)
ushr // 无符号右移位 a ushr 2
and // 与
or // 或
xor // 异或
inv // 反向
字符
和Java一样,Char是用 ' ' 包含起来的,比如 ‘0’,‘a’。
我们可以显式地把字符转换成 Int 数字:
fun main(args: Array<String>) {
println(decimalDigitValue('1'))
println(decimalDigitValue('0'))
}
fun decimalDigitValue(c: Char): Int{
if(c !in '0'..'9')
throw IllegalArgumentException("Out of range")
// 之所以要减去 '0'.toInt() 是因为 Char.toInt()返回的是ASCII的值,从48按顺序开始的,0的ASCII的值为48
// 还有一个方法:Char.toString().toInt()
return c.toInt() - '0'.toInt()
}
布尔
布尔用 Boolean 类型表示,它有两个值:true和false
若需要可空引用布尔会被装箱。
内置的布尔运算有:
||
, &&
, !
数组
数组用类 Array 实现,并且还有一个 size
属性及 get
和 set
方法。由于使用 [] 重载了 get
和 set
方法,所以我们可以通过下标获取或者设置数组对应位置的值。
数组的创建两种方式:
- 使用函数
arrayOf()
- 使用工厂函数
fun main(args: Array<String>) {
val a = arrayOf(1,2,3)
val b = Array(3,{i -> (i*2)}) // 0,2,4
println(a[1])
println(b[1])
}
如上所述,[] 运算符代表调用成员函数 get()
和 set()
。
注意: 与 Java 不同的是,Kotlin 中数组是不协变的(invariant)。
除了类Array,还有ByteArray, ShortArray, IntArray,用来表示各个类型的数组,省去了装箱操作,因此效率更高,其用法同Array一样:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
字符串
和Java一样,String是不可变的。 []
语法可以很方便的获取字符串中的某个字符,也可以通过 for
循环来遍历:
for (c in str) {
println(c)
}
Kotlin 支持三个引号 """
扩起来的字符串,支持多行字符串,比如:
fun main(args: Array<String>) {
val text = """
多行字符串
多行字符串
""".trimMargin() // 删除前置空格
println(text)
}
默认 |
用作边界前缀,但你可以选择其他字符并作为参数传入, 比如 trimMargin(">")
字符串模板
字符串可以包含模板表达式,即一些小段代码。由 $
开头。
fun main(args: Array<String>) {
val s = "runoob"
val str = "$s.length is ${s.length}" // 求值结果为 "runoob.length is 6"
println(str)
}
Kotlin 条件控制
IF 表达式
一个 if 语句包含一个布尔表达式和一条或多条语句。
// 传统用法
var max = a
if(a < b) max = b
// 使用 else
var max: Int
if(a > b){
max = a
}else {
max = b
}
}
// 作为表达式
val max = if(a > b) a else b
我们也可以吧 if 表达式的结果赋值给一个变量
var max = if(a > b){
print("Choose a")
a
}else{
print("Choose b")
b
}
这也说明,kotlin 不需要像Java那样有一个三元操作符,有更简单的实现:
val c = if(condition) a else b
使用区间
使用 in
运算符来检测某个数字是否在指定区间内,区间格式为 a..b
:
fun main(args: Array<String>) {
val x = 5
val y = 9
if (x in 1..8) {
println("x 在区间内")
}
}
When 表达式
when 将它的参数和所有的分支条件顺序比较 ,直到某个分支满足条件。
when 既可以被当作表达式使用也可以被当作语句使用。如果它被当作表达式,符合条件的分支的值就是整个表达式的值,如果当作语句使用,则忽略个别分支的值。
when 类似其他语言的 switch
操作符。其最简单的形式如下:
when(x){
1 -> print("x = 1")
2 -> print("x = 2")
// 可以把多个分支条件放在一起
3,4 -> print("x == 3 or x ==4")
// 可以检测一个值 in 或者 !in 一个区间或者集合中
in 5..7 -> print("x is in the range of 3-6")
!in 8..10 -> print("x is not in the range of 7-9")
else -> print("none of above")
}
另一种可能性是检测 is
或者 !is
一个特定类型的值。
fun isNumber(x: Any) = when(x){
is Int -> true // return Boolean
else -> false
}
另外,when也可以 用来取代 if-else if
链 。如果 when 不提供参数,所有分支条件都是简单地布尔表达式,而当一个分支条件满足,则执行该分支:
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
Kotlin 循环控制
For 循环
for 循环可以对任何提供迭代器(iterator)的对象进行遍历,语法如下:
for (item in collection) print(item)
循环体可以是一个代码块:
for(item: Int in ints){
// xxx
}
如上所述,for 可以循环遍历任何提供了迭代器的对象。
如果你想要通过索引遍历一个数组或者一个list,你可以这么做:
for (i in array.indices) {
print(array[i])
}
注意,这种“在区间上遍历”会编译成优化的实现而不会创建额外对象。
或者你可以用库函数 withIndex
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
实例:
对集合进行迭代:
fun main(args: Array<String>) {
val items = listOf("apple", "banana", "kiwi")
for (item in items) {
println(item)
}
for (index in items.indices) {
println("item at $index is ${items[index]}")
}
}
Output:
apple
banana
kiwi
0 + apple
1 + banana
2 + kiwi
while 与 do...while 循环
while 是最基本的循环,它的结构为:
while( 布尔表达式 ) {
//循环内容
}
do...while
循环 对于 while语句而言,如果不满足条件,则不能进入循环。
do...while
循环和 while循环相似,不同的是, do...while 循环至少会执行一次。
do {
//代码语句
}while(布尔表达式);
实例:
fun main(args: Array<String>) {
println("------while-------")
var x = 5
while(x > 0){
println(x--)
}
println("------do...while-------")
var y = 5
do {
println(y--)
}while (y > 0)
}
返回与跳转
Kotlin 有三种结构化跳转表达式:
-
return
默认从最直接包围它的函数或者匿名函数返回。 -
break
终止最直接包围它的循环 -
continue
继续下一次最直接包围它的循环
在循环中 Kotlin 支持传统的break
和continue
操作符。
fun main(args: Array<String>) {
for (i in 1..10){
if (i == 3) continue // i 为 3 时跳过当前循环,继续下一次循环
println(i)
if (i > 5) break // i 为 6 时 跳出循环
}
}
Output:
1
2
4
5
6
Break 和 Continue 标签
Kotlin中标签的作用与其在Java中的作用相同。
简单来说,就是标签可以使 break
和 continue
作用到标记的那一个循环中。
Java循环中标签的作用
在Kotlin中任何表达式都可以用标签(label)来标记。标签的格式为标识符后跟 @
符号,例如:abc@、fooBar@都是有效的标签。要为一个表达式加标签,我们只要在其前加标签即可。
loop@ for (i in 1..100) {
// ……
}
现在,我们可以用标签限制 break
或者 continue
:
fun main(args: Array<String>) {
loop@ for (i in 1..4){
for (j in 1..4){
if(i == 3) break@loop //or continue@loop
println("i is $i and j is $j")
}
}
}
Output:
// break@loop
i is 1 and j is 1
i is 1 and j is 2
i is 1 and j is 3
i is 1 and j is 4
i is 2 and j is 1
i is 2 and j is 2
i is 2 and j is 3
i is 2 and j is 4
// continue@loop
i is 1 and j is 1
i is 1 and j is 2
i is 1 and j is 3
i is 1 and j is 4
i is 2 and j is 1
i is 2 and j is 2
i is 2 and j is 3
i is 2 and j is 4
i is 4 and j is 1
i is 4 and j is 2
i is 4 and j is 3
i is 4 and j is 4
标签处返回
Kotlin 有函数字面量、局部函数和对象表达式。因此Kotlin的函数可以被嵌套。标签限制的 return
允许我们从外层函数返回。最重要的一个用途就是从lambda表达式中返回。当我们这么写的时候:
fun foo() {
ints.forEach {
if (it == 0) return
print(it)
}
}
foreach
用法
val numbers = listOf("one", "two", "three", "four") numbers.forEach { println(it) }
这个 return
表达式从最直接包围它的函数即 foo
中返回。(注意,这种非局部的返回只支持传给内联函数的lambda表达式)。如果我们需要从lambda表达式中返回,必须给它加标签并用以限制 return
。
fun foo() {
ints.forEach lit@ {
if (it == 0) return@lit
print(it)
}
}
通常情况下,使用隐式标签更方便。该标签与接受该 lambda 的函数同名。
fun foo() {
ints.forEach {
if (it == 0) return@forEach
print(it)
}
}
Kotlin 类和对象
类定义
Kotlin 类可以包含:构造函数 和 初始化代码块、函数、属性、内部类、对象声明。
Kotlin 中使用关键字 class
声明类,后面紧跟类名:
class KotlinTest{ // 类名为 KotlinTest
// 大括号内是类体构成
}
我们也可以定义一个空类:
class Empty
可以在类中定义成员函数:
class Runoob() {
fun foo() { print("Foo") } // 成员函数
}
类的属性
属性定义
类的属性可以用关键字 var
声明为可变的,否则使用只读关键字 val
声明为不可变。
class Runoob {
var name: String = ……
var url: String = ……
var city: String = ……
}
我们可以像使用普通函数那样使用构造函数创建类实例:
val site = Runoob() // Kotlin 中没有 new 关键字
要使用一个属性,只要用名称引用它即可
site.name // 使用 . 号来引用
site.url
Kotlin 中的类可以有一个主构造器,以及一个或多个次构造器。主构造器是类头部的一部分,位于类名称之后:
class Person constructor(firstName: String) {}
如果主构造器没有任何注解,也没有任何可见度修饰符,那么 constructor
关键字可以省略
class Person(firstName: String) { }
getter 和 setter
属性声明的完整语法:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
getter 和setter 都是可选
如果属性类型可以从初始化语句或者类的成员函数中推断出来,那就可以省去类型, val
不允许设置 setter
。是因为它是只读的。
实例
以下实例定义了一个 Person 类,包含两个可变变量 lastName
和 no
, lastName
修改了 getter
方法, no
修改了 setter
方法。
class Person{
var lastName: String = ""
get() = field.toUpperCase() // 转换大小写
set
var no:Int = 100
get() = field + 5 // 后端变量
set(value) {
if (value < 10){
field = value
} else{
field = -1
}
}
var heiht: Float = 145.4f
private set
}
fun main(args: Array<String>){
var person: Person = Person()
person.lastName = "chen"
println("lastName: ${person.lastName}")
person.no = 9
println("no:${person.no}")
person.no = 10
println("no:${person.no}")
}
Output:
lastName: CHEN
no:14
no:4
注释:
no
出现 4 的原因是, person.no = 10
在 set
时大于10。 故走 else
,此时 no
的值为 -1,但是在 get
时,由于 get() = field + 5
,故最终结果为 4.
非空属性必须在定义的时候初始化,kotlin提供了一种可以延迟初始化的方案,使用 lateinit 关键字描述属性:
public class MyTest {
lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
}
@Test fun test() {
subject.method() // dereference directly
}
}
主构造器
主构造器中不能包含任何代码,初始化代码可以放在初始化代码段中,初始化代码段使用 init
关键字作为前缀。
class Person constructor(firstName: String) {
init {
println("FirstName is $firstName")
}
}
注意: 主构造器指的是 constructor(firstName: String)
,为了更好地理解 “其中不能包含任何代码” 可以与 次构造器对比来看。
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
注意2:主构造器的参数可以在初始化代码段中使用,也可以在类主体n定义的属性初始化代码中使用。一种简洁语法,可以通过主构造器来定义属性并初始化属性值(可以是 var
或 val
)
class People(val firstName: String, val lastName: String) {
//...
}
实例:
class ConstructorTest constructor(name: String){
var url: String = ""
var country: String = ""
var website: String = name
init {
println("初始化网站名:$name")
}
fun printTest(){
println("this is class function")
}
}
fun main(args: Array<String>){
val con = ConstructorTest("Google")
con.url = "www.google.com"
con.country ="CN"
println("the url is ${con.url}")
println("the country is ${con.country}")
con.printTest()
}
Output:
初始化网站名:Google
the url is www.google.com
the country is CN
this is class function
次构造函数
类也可以有二级构造函数,需要加前缀 constructor
:
class Person(val pets: MutableList<Pet> = mutableListOf())
class Pet {
constructor(owner: Person) {
owner.pets.add(this) // adds this pet to the list of its owner's pets
}
}
如果类有主构造函数,每个次构造函数都要,或直接或间接通过另一个次构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this
关键字。
实例:
class ConstructorTest constructor(name: String){
var url: String = ""
var country: String = ""
var website: String = name
init {
println("初始化网站名:$name")
}
constructor(name: String, alexa: Int): this(name){
println("the year is $name, the alexa is $alexa")
}
fun printTest(){
println("this is class function")
}
}
fun main(args: Array<String>){
val con = ConstructorTest("Google")
con.url = "www.google.com"
con.country ="CN"
println("the url is ${con.url}")
println("the country is ${con.country}")
con.printTest()
val con1 = ConstructorTest("Baidu",2)
con1.url = "www.baidu.com"
con1.country ="US"
println("the url1 is ${con1.url}")
println("the country1 is ${con1.country}")
con1.printTest()
}
Output:
初始化网站名:Google
the url is www.google.com
the country is CN
this is class function
初始化网站名:Baidu
the year is Baidu, the alexa is 2
the url1 is www.baidu.com
the country1 is US
this is class function
抽象类
抽象是面向对象编程的特征之一,类本身,或类中的部分成员,都可以声明 abstract
的。抽象成员在类中不存在具体的实现。
注意:无需对抽象类或抽象成员标注 open
注解
open class Base {
open fun f() {}
}
abstract class Derived : Base() {
override abstract fun f()
}
嵌套类
我们可以把类嵌套在其他类中,看一下实例:
class Outer { // 外部类
private val bar: Int = 1
class Nested { // 嵌套类
fun foo() = 2
}
}
fun main(args: Array<String>) {
val demo = Outer.Nested().foo() // 调用格式:外部类.嵌套类.嵌套类方法/属性
println(demo) // == 2
}
内部类
内部类使用 inner
关键字来表示
内部类会带有一对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。
class Outer{
private val bar = 1
var v = "Outer 成员属性"
// 嵌套内部类
inner class Inner{
var inner = bar
fun foo(){
var o = this@Outer
println("内部类可以引用外部类的成员, ${o.bar} 和 ${o.v}")
}
}
}
fun main(args: Array<String>){
val innerDemo = Outer().Inner().inner
val innerDemo2 = Outer().Inner().foo()
println(innerDemo)
}
Output:
内部类可以引用外部类的成员, 1 和 Outer 成员属性
1
为了消除歧义,要访问来自外部作用域的 this
,我们使用 this@label
,其中 @label
是一个代指 this 来源的标签。
匿名内部类
使用对象表达式来创建 匿名内部类:
class Test {
var v = "成员属性"
fun setInterFace(test: TestInterFace) {
test.test()
}
}
/**
* 定义接口
*/
interface TestInterFace {
fun test()
}
fun main(args: Array<String>) {
var test = Test()
/**
* 采用对象表达式来创建接口对象,即匿名内部类的实例。
*/
test.setInterFace(object : TestInterFace {
override fun test() {
println("对象表达式创建匿名内部类的实例")
}
})
}
类的修饰符
类的修饰符包括 classModifier 和 accessModifier :
- classModifier:类属性修饰符,标示类本身特性
abstract // 抽象类
final // 类不可继承,默认属性
enum // 枚举类
open // 类可继承,类默认是final的
annotation // 注解类
- accessModier:访问权限修饰符
private // 仅在同一个文件中可见
protected // 同一个文件中或子类可见
public // 所有调用的地方都可见
internal // 同一个模块中可见
实例:
// 文件名:example.kt
package foo
private fun foo() {} // 在 example.kt 内可见
public var bar: Int = 5 // 该属性随处可见
internal val baz = 6 // 相同模块内可见
Kotlin继承
Kotlin 中所有类都继承该Any类,它是所有类的超类,对于没有超类型声明的类是默认超类:
class Example // 从 Any 隐式继承
Any 默认提供了三个函数:
equals()
hashCode()
toString()
注意:Any 不是 java.lang.Object
。
如果一个类要被继承,可以使用 open
关键字进行修饰。
open class Base(p: Int) // 定义基类
class Derived(p: Int) : Base(p)
构造函数
子类有主构造函数
如果子类有主构造函数,则基类必须在主构造函数中立即初始化。
open class Person(var name: String, var year: Int){
// 基类
}
class Student(name: String, year: Int, var no: Int, var score: Int): Person(name, year){
}
fun main(args: Array<String>){
val s = Student("Matthew", 1994, 80352170, 94)
println("姓名: ${s.name}")
println("年龄: ${s.year}")
println("学号: ${s.no}")
println("分数: ${s.score}")
}
Output:
姓名: Matthew
年龄: 1994
学号: 80352170
分数: 94
注意: Student(name: String, year: Int, var no: Int, var score: Int)
是子类的主构造函数。 所有构造函数中第一次出现的参数,需要 var
声明。
子类没有主构造函数
如果子类没有主构造函数,则必须在每一个二级构造函数中用 super
关键字初始化基类,或者在代理另一个构造函数。初始化基类时,可以调用基类的不同构造方法。
class Student : Person {
constructor(ctx: Context) : super(ctx) {
}
constructor(ctx: Context, attrs: AttributeSet) : super(ctx,attrs) {
}
}
实例:
// 用户基类
// 主构造函数
open class Person(var name: String, var year: Int){
// 次级构造函数, var 不能在 次级构造函数 中使用
constructor(name: String, year: Int, month: Int): this(name, year){
println("------基类 次级 构造函数------")
}
}
// 子类
class Student: Person{
// 子类 第一个次级构造函数 代理 基类 主构造函数
constructor(name: String, year: Int, no: Int, score: Int): super(name, year){
println("-----这是子类 第一个 次级构造函数-----")
}
// 第二个 次级构造函数 代理 基类 次级构造函数
constructor(name: String, year: Int, month: Int, no: Int, score: Int):super(name, year, month ){
println("-----这是子类 第二个 次级构造函数-----")
}
}
fun main(args: Array<String>){
val p = Person("Matthew", 1994)
// No output
val p1 = Person("Matthew", 1994,9)
// ------基类 次级 构造函数------
val s = Student("Matthew", 1994, 80352170, 94)
//-----这是子类 第一个 次级构造函数-----
val s1 = Student("Matthew", 1994,9, 80352170, 94)
//------基类 次级 构造函数------
//-----这是子类 第二个 次级构造函数-----
// 尝试调用 子类 次级构造函数 中的属性
println("no is $s1.no")
//Output: no is com.learn.day01.Student@10455d6.no
}
分析: 从上述代码中,我们可以看出:
- 在次级构造函数中,不可以用
var
或val
声明变量 - 基类主构造函数,除了 会执行 初始化中的代码,其他不会执行
- 子类 由于 继承了基类,所以像 执行 子类 次级构造函数时,先执行其代理的 基类 构造函数。这些从打印结果就可以看出
重写
在基类中,使用 fun
声明函数时,此函数默认为 final
修饰,不能被子类重写。如果允许子类重写该函数,那么就要手动添加 open
修饰它,子类重写方法使用 override
关键词:
open class Highschool{
open fun study(){
println("我上高中了")
}
}
class College: Highschool(){
override fun study() {
println("我上大学了")
}
}
fun main(args: Array<String>){
val c = College()
c.study()
}
Output:
我上大学了
如果有多个相同的方法(比如继承或实现自别的类),则必须要重写该方法,使用 super
范型去选择性地调用父类的实现。
open class A{
open fun f(){
println("fun f in class A")
}
fun a(){
"fun a in class A"
}
}
interface B{
fun f(){ println("fun f in interface B")} // 接口成员变量默认是 open 的
fun b(){ println("fun b in interface B")}
}
class C: A(),B{
override fun f() {
super<A>.f() // 调用 A.f()
super<B>.f() // 调用 B.f()
}
}
fun main(args: Array<String>){
val c = C();
c.f()
}
属性重写
属性重写 使用 override
关键字,属性必须具有兼容类型,每一个声明的属性都可以通过初始化程序或者 getter
方法被重写。
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
你可以用一个 var
属性重写一个 val
属性,但是反之则不行。因为 val
属性本身定义了 getter
方法,重写为 var
属性会在衍生类中额外声明一个 setter
方法。
可以在主构造函数中使用 override 关键字作为属性声明的一部分:
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点
class Polygon : Shape {
override var vertexCount: Int = 0 // 以后可以设置为任何数
}
派生类初始化顺序
在构造子类的新实例的过程中,第一步完成其基类的初始化(在之前只有对基类构造函数参数的求值),因此发生在子类的初始化逻辑运行之前。
open class Base(val name: String) {
init { println("Initializing Base") }
open val test: Int =
name.length.also { println("----------$it------------") }
open val size: Int =
name.length.also { println("Initializing size in Base: $it") }
val test1: Int =
name.length.also { println("----------$it------------") }
}
class Derived(
name: String,
val lastName: String
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {
init { println("Initializing Derived") }
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}
fun main() {
println("Constructing Derived(\"hello\", \"world\")")
val d = Derived("hello", "world")
}
Output:
Constructing Derived("hello", "world")
Argument for Base: Hello
Initializing Base
----------5------------
Initializing size in Base: 5
----------5------------
Initializing Derived
Initializing size in Derived: 10
从上述结果中,我们可以发现 子类初始化时代码的执行顺序。即,先执行基类构造函数及其中的所有代码(即使没有 override
,除 fun
之外),而后,再去执行子类中的代码。
调用超类实现
子类中的代码可以使用 super
关键字调用其超类的函数与属性访问器的实现。
在一个内部类中访问外部类的超类,可以通过由外部类名 限定的 super
关键字来实现: super@Outer
示例:
open class Rectangle{
val borderColor: String = "black"
open fun draw(){
println("这是 Rectangle 的 draw() 方法")
}
}
class FilledRectangle: Rectangle(){
val fillColor: String = super.borderColor.also { println("调用 父类 的属性 borderColor is $it") }
override fun draw() {
println("调用 父类的 draw()方法")
super.draw() // 调用父类 draw()
val filler = Filler()
filler.drawAndFill()
}
inner class Filler{
fun drawAndFill(){
// 调用 外部类的父类 Rectangle 的 draw()
println("调用 外部类的父类 Rectangle 的 draw() 方法 ")
super@FilledRectangle.draw()
// 调用 外部类的父类 Rectangle 的 属性 borderColor
println("调用 外部类的父类 Rectangle 的 属性 borderColor ${super@FilledRectangle.borderColor}")
}
}
}
fun main(args: Array<String>){
val a = FilledRectangle()
a.draw()
}
Output:
调用 父类 的属性 borderColor is black
调用 父类的 draw()方法
这是 Rectangle 的 draw() 方法
调用 外部类的父类 Rectangle 的 draw() 方法
这是 Rectangle 的 draw() 方法
调用 外部类的父类 Rectangle 的 属性 borderColor black
覆盖规则
在Kotlin中,实现继承由下述规则规定:
如果一个类,从它的直接超类继承相同成员的多个实现,它必须覆盖这个成员并提供其自己的实现。为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super
,如 super<Rectangle>
open class Rectangle {
open fun draw() { /* …… */ }
}
interface Polygon {
fun draw() { /* …… */ } // 接口成员默认就是“open”的
}
class Square() : Rectangle(), Polygon {
// 编译器要求覆盖 draw():
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}
可以同时继承 Rectangle
与 Polygon
,但是二者都有各自的 draw()
实现,所以我们必须在 Square
中覆盖 draw()
,并提供其自身的实现以消除歧义。
接口
Kotlin 的接口可以既包含抽象方法的声明,也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。
使用关键字 interface
来定义接口。
interface MyInterface {
fun bar()
fun foo() {
// 可选的方法体
}
}
实现接口
一个类或者对象可以实现一个或多个接口。
class Child : MyInterface {
override fun bar() {
// 方法体
}
}
接口中的属性
可以在接口中定义属性。在接口声明的属性要么是抽象的,要么提供访问器的实现。在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们。
interface MyInterface {
val prop: Int // 抽象的
val propertyWithImplementation: String
get() = "foo" //访问器
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29
}
接口继承
一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。实现这样接口的类只需定义所缺少的实现:
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName"
}
// 数据类 后续会介绍
data class Employee(
// 不必实现“name”
override val firstName: String, // 只需定义所缺少的实现
override val lastName: String,
val position: Position
) : Person
解决覆盖冲突
实现多个接口时,可能会遇到同一方法继承多个实现的问题,例如:
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
override fun bar() {
super<B>.bar()
}
}
上例中,接口A和B都定义了方法 foo()
和 bar()
。两者都实现了 foo()
,但是只有B实现了 bar()
( bar()
在A中没有标记为抽象,因为在接口中没有方法体时,默认为抽象)。因为C是一个实现了A的具体类,所以必须要重写 bar()
并实现这个抽象方法。
然而,如果我们从 A 和 B 派生 D,我们需要实现我们从多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则既适用于继承单个实现( bar()
)的方法也适用于继承多个实现( foo()
)的方法。
函数式(SAM)接口
只有一个抽象方法的接口称为 函数式接口 或 SAM(单一抽象方法) 接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。
可以用 fun()
修饰符在 Kotlin 中声明一个函数式接口。
// 函数式接口
fun interface KRunnable {
fun invoke()
}
SAM 转换
对于函数式 接口,可以通过 lambda表达式 实现SAM转换。从而使代码更简洁、更有可读性。
使用 lambda 表达式 可以替代手动创建实现函数式接口的类。通过SAM转换,Kotlin可以将其签名与接口的单个抽象方法的签名匹配的任何lambda表达式转换为实现该接口的类的实例。
扩展
Kotlin能够扩展一个类的新功能无需继承该类或者使用像装饰者这样的设计模式。这通过叫做 扩展 的特殊声明完成。例如,你可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。这种机制称为 扩展函数 。此外,也有 扩展属性,允许你为一个已经存在的类添加新的属性。
扩展函数
声明一个扩展函数,我们需要用一个 接受者类型 也就是被扩展的类型来作为他的前缀。下面代码为 MutableList<Int>
添加一个 swap
函数:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
这个 this
关键字在扩展函数内部对应到接受者对象(传过来的在点符号前的对象)。现在,我们对任意 MutableList<Int>
调用该函数了:
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // “swap()”内部的“this”会保存“list”的值
当然,这个函数对任何 MutableList<T>
都起作用,我们可以泛化(从 Int
泛化到各种类型)它:
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // “this”对应该列表
this[index1] = this[index2]
this[index2] = tmp
}
为了在接收者类型表达式中使用泛型,我们要在函数名前声明泛型参数。详细可参考 泛型函数
扩展式静态解析的
扩展不能真正的修改它们所扩展的类。通过定义一个扩展,其实并没有在一个类中插入新的成员,仅仅是可以通过该类型的变量用点表达式去调用这个新函数。
故,扩展函数是静态分发的,即它们不是根据接收者类型的虚方法。这意味着,调用的扩展函数是由函数调用所在的表达式类型来决定的,而不是由表达式运行时求值结果决定的。例如:
open class Shape
class ShapeExtension: Shape() // 继承自Shape
fun Shape.getName() = "Shape" // Shape 扩展函数
fun ShapeExtension.getName() = "Shape extensions" // ShapeExtension 扩展函数
fun printName(s: Shape){
println(s.getName())
}
fun main(){
printName(ShapeExtension())
}
// Output: Shape
在上述例子中会输出 “Shape”, 因为调用的扩展函数只取决于参数 s
的声明类型,该类型是 Shape
类。
如果一个类兴义有一个成员函数与一个扩展函数,而这两个函数又有相同的接收者类型、相同的名字,并且都适用给定的参数,这种情况总是满足成员函数。例如:
class Example{
fun fun1(){
println("class method")
}
}
fun Example.fun1(){
println("extension method")
}
fun main(){
val e = Example()
e.fun1()
}
// Output: class method
可空接收者
注意,可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用,即使其值为 null
,并且可以在函数体内检测 this == null
,这能让在没有检测 null
的时候调用 Kotlin中的 toString():
检测发生在扩展函数的内部。
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}
扩展属性
与函数类似,Kotlin支持扩展属性:
val <T> List<T>.lastIndex: Int
get() = size - 1
注意:由于扩展没有实际的将成员插入类中,因此对扩展属性来说 幕后字段 是无效的。这就是为什么 扩展属性不能有初始化器。他们的行为只能由显示提供的 getter/setter 定义。
参考:幕后字段/幕后属性
注:如果一个类有两个概念上相同的属性,一个是公共API的一部分,另一个是实现细节,那么使用下划线作为私有属性名称的前缀:class C { private val _elementList = > > mutableListOf<Element>() val elementList: List<Element> get() = _elementList }
例如:
val House.number = 1 // 错误:扩展属性不能有初始化器
伴生对象的扩展
如果一个类定义有一个伴生对象,你也可以为伴生对象定义扩展函数与属性。就像伴生对象的常规成员一样,可以只使用类名作为限定符来调用伴生对象的扩展成员:
class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion()
}
扩展的作用域
大多数时候我们在顶层定义扩展——直接在包里:
package org.example.declarations
fun List<String>.getLongestString() { /*……*/}
要使用所定义包之外的一个扩展,我们需要在调用方导入它:
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
扩展声明为成员
在一个类内部,你可以为另一个类声明扩展。
在这样的扩展内部,有多个 隐式接收者——其中的对象成员可以无需通过限定符访问。
扩展声明所在的类的实例称为 分发接收者 ,
扩展方法调用所在的接收者类型的实例称为 扩展接收者 。
class Host(val hostname: String){
fun printHostName(){ println(hostname)}
}
class Connection(val host: Host, val port: Int){
fun printPort(){ println(port) }
fun Host.printConnectString(){
printHostName() // 调用 Host.printHostName()
print(":")
printPort() // 调用 Connection.printPort()
}
fun connect(){
host.printConnectString() //调用扩展函数
}
}
fun main(){
Connection(Host("kotl.in"),443).connect()
}
对于分发接收者与扩展接收者的成员名字冲突的情况,扩展接收者优先。要引用分发接收者的成员,可以使用 this
class Connection {
fun Host.getConnectionString() {
toString() // 调用 Host.toString()
this@Connection.toString() // 调用 Connection.toString()
}
}
声明为成员的扩展可以声明为 open
并在子类中覆盖。这意味着这些函数的分发对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。
open class Base { }
class Derived : Base() { }
open class BaseCaller {
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}
open fun Derived.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}
fun call(b: Base) {
b.printFunctionInfo() // 调用扩展函数
}
}
class DerivedCaller: BaseCaller() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}
override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}
fun main() {
BaseCaller().call(Base()) // “Base extension function in BaseCaller”
DerivedCaller().call(Base()) // “Base extension function in DerivedCaller”——分发接收者虚拟解析
DerivedCaller().call(Derived()) // “Base extension function in DerivedCaller”——扩展接收者静态解析
}
委托
由委托实现
代理模式,就是把自己要做的事情委托给另外一个对象,这个对象就是代理对象。在Java中,代理模式包括静态代理以及动态代理。
委托模式已经证明是实现继承的一个很好的替代方式,而Kotlin可以零样板代码地原生支持它。 Derived
类可以通过将其所有公有成员都委托给指定对象来实现一个几口 Base
:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print()
}
Derived
的超类型列表中的 by
子句表示: b
将会在 Derived
中内部存储,并且编译器将生成转发给 b
的所有 Base
的方法。
覆盖由委托实现的接口成员
覆盖符合预期:编译器会使用 override
覆盖的 实现而不是委托对象中的。如果将 override fun printMessage() { print("abc") }
添加到 Derived
,那么当调用 printMessage
时,程序会输出 “abc”而不是“10”
interface Base {
fun printMessage()
fun printMessageLine()
}
class BaseImpl(val x: Int) : Base {
override fun printMessage() { print(x) }
override fun printMessageLine() { println(x) }
}
class Derived(b: Base) : Base by b {
override fun printMessage() { print("abc") }
}
fun main() {
val b = BaseImpl(10)
Derived(b).printMessage()
Derived(b).printMessageLine()
}
// Output: abc10
但请注意,以这种方式重写的成员不会再委托对象的成员中调用,委托对象的成员只能访问其自身对接口成员实现:
interface Base {
val message: String
fun print()
}
class BaseImpl(val x: Int) : Base {
override val message = "BaseImpl: x = $x"
override fun print() { println(message) }
}
class Derived(b: Base) : Base by b {
// 在 b 的 `print` 实现中不会访问到这个属性
override val message = "Message of Derived"
}
fun main() {
val b = BaseImpl(10)
val derived = Derived(b)
derived.print()
println(derived.message)
}
// Output: BaseImpl: x = 10
// Message of Derived
委托属性
有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们,但是如果能够为大家把它们只实现一次并放入一个库会更好。例如包括:
- 延迟属性(lazy properties):其值只在首次访问时计算;
- 可观察属性(observable properties):监听器会收到有关此属性变更的通知;
- 把多个属性存储在一个映射(map)中,而不是每个存在单独的字段中。
为了涵盖这些(以及其他)情况,Kotlin支持 委托属性:
class Example {
var p: String by Delegate()
}
语法是: val/var <属性名>: <类型> by <表达式>
在 by
后面的表达式是该 委托,因为属性对应的 get()
与 set()
会被委托给它的 getValue()
与 setValue()
方法。属性的委托不必实现任何的接口,但是需要提供一个 getValue()
函数(与 setValue()
——对于 var
属性)。
换句话说, by
后面的 委托 需要 含有或重写 getValue()
和 setValue()
的方法。并且在前面需要加 operator
修饰符。
例如:
operator关键字:表明重载 操作符的函数 需要用 operator
修饰符标记。
import kotlin.reflect.KProperty
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}
当我们从委托到一个 Delegate
实例的 p
读取时,将调用 Delegate
中的 getValue()
函数,所以它第一个参数时读出 p
的对象、第二个参数保存了对 p
自身的描述(例如你可以取它的名字)。例如:
val e = Example()
println(e.p)
所以,将代码整合到一起就是:
import kotlin.reflect.KProperty
class Delegate{
operator fun getValue(thisRef: Any?,property: KProperty<*>): String{
return "$thisRef, thank you for delegating '${property.name}' to me"
}
operator fun setValue(thisRef: Any?,property: KProperty<*>, value: String){
println("$value has been assigned to '${property.name}' in $thisRef")
}
}
class Ex{
var p: String by Delegate()
}
fun main(){
val e = Ex()
println(e.p)
e.p = "newValue"
}
Output:
com.learn.Ex@117ae12, thank you for delegating 'p' to me
newValue has been assigned to 'p' in com.learn.Ex@117ae12
类似地,当我们给 p
赋值时,将调用 setValue()
函数。前两个参数相同,第三个参数保存将要被赋予的值,示例如上述代码。
标准委托
Kotlin 标准库为几种有用的委托提供了工厂方法。
延迟属性Lazy
lazy()
是接受一个lambda并返回一个 Lazy<T>
实例的函数,返回的实例可以作为实现延迟属性的委托:
第一次调用 get()
会执行已传递给 lazy()
的lambda 表达式并记录结果,后续调用 get()
只是返回记录的结果。
val lazyValue: String by lazy{
println("computed")
"Hello"
}
fun main(){
println(lazyValue)
println(lazyValue)
println(lazyValue)
}
// computed
// Hello
// Hello
// Hello
可以看出,实现延迟属性的委托后,第一次调用会先执行第一条语句后再返回后面的值,在后续的调用中,不再调用调用第一行。
可观察属性 Observable
observable 可以用于实现观察者模式。
Delegates.observable() 函数接受两个参数:第一个是初始化值,第二个是属性值变化事件的响应器(handler)。
在属性赋值后,会执行事件的响应器(handler),它有三个参数:被赋值的属性、旧值和新值:
class User{
var name: String by Delegates.observable("初始值"){
property, oldValue, newValue ->
println("旧值:$oldValue -> 新值:$newValue")
}
}
fun main(){
val user = User()
user.name = "第一次赋值"
user.name = "第二次赋值"
user.name = "第三次赋值"
}
Output:
旧值:初始值 -> 新值:第一次赋值
旧值:第一次赋值 -> 新值:第二次赋值
旧值:第二次赋值 -> 新值:第三次赋值
将属性存储在映射中
一个常见的用例是在一个映射(map)里存储属性的值。这里经常出现在像解析JSON或者做其他“动态”事情的应用中。在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。
class Site(val map: Map<String, Any?>){
val name: String by map
val url: String by map
}
fun main(){
val site = Site(mapOf(
"name" to "谷歌",
"url" to "www.google.com"
))
println(site.name)
println(site.url)
}
Output:
谷歌
www.google.com
如果使用 var
属性,需要把 Map
换成 MutableMap
局部委托属性
我们可以将局部变量声明为委托属性。例如,可以使一个局部变量 memoizedFoo
惰性初始化:
fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)
if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}
memoizedFoo
变量只会在第一次访问时计算。如果 someCondition
失败,那么该变量根本不会计算。
属性委托要求
对于 val
,它的委托必须提供一个名为 getValue()
的函数。该函数接受以下参数:
thisRef —— 必须与属性所有者类型(对于扩展属性——指被扩展的类型)相同或者它的超类型
property —— 必须是类型KProperty<*>或其超类型
这个函数必须返回与属性相同的类型(或其子类型)。
对于一个值可变(mutable)属性(也就是说,var 属性),除
getValue()
函数之外,它的委托还必须 另外再提供一个名为setValue()
的函数, 这个函数接受以下参数:property —— 必须是类型 KProperty<*> 或其超类型
new value —— 必须和属性同类型或者是它的超类型。
高阶函数与lambda表达式
Lambda是什么
简单来讲,Lambda是一种函数的表示方式,也就是说,一个Lambda表达式等于一个函数。更确切的讲,Lambda是一个未声明的函数,会以表达式的形式传递。
为什么要用Lambda
设想一下,在Android中实现一个View的点击事件,可以使用如下实现:
View view = findViewById(R.id.textView);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
viewClicked(view);
}
});
而如果在Kotlin中,使用Lambda,则实现可以简单如下:
val view = findViewById(R.id.image)
view.setOnClickListener { v - viewClicked(v) }
可以很明显的看出Lambda一方面可以简省很多代码,最重要的一点是:Lambda表达式可以避免在抽象类或接口中编写明确地函数声明,进而也避免了类的实现部分(省去了OnClickListener 接口环节)
Lambda 表达式语法
- lambda 表达式总是被大括号括着
- 其参数(如果有)在
-
之前声明(参数类型可以省略); - 函数体(如果有)在
-
后面
具体的写法可以有以下两种写法:
// 第一种
val sum1 = {x: Int, j: Int - x + j}
// 第二种
val sum2: (x: Int, j: Int) - Int = {a, b - a + b }
分析上述两种表达式:
第一种: 比较好理解,首先 ‘=’ 左边声明了一个变量sum1,‘=’ 右边是一个Labmda表达式,然后将其赋值给sum1。
第二种: 稍微复杂一点,主要是复杂在左边的 sum2:
后面代码。 (x: Int, j: Int) – Int
表示的是 一个需要传入两个 Int 类型参数,并返回 Int 类型的函数。
Lambda传递使用
在需要的时候,可以将 sum1、sum2 传递给一个高阶函数,或者也可以直接将 = 之后的表达式传递给高阶函数。
val view = findViewById(R.id.image)
view.setOnClickListener { v - imageClicked(v) }
高阶函数
以函数作为参数或返回函数的函数被称为高阶函数
示例:
fun highOrderFunc(arg1: Int, arg2: Int, paramFunc: (a: Int, b: Int) -> Boolean): Int {
return if (paramFunc(arg1, arg2)) {
arg1
} else {
arg2
}
}
在上面具体示例中,定义了一个名为 highOrderFunc
的高阶函数,并且传入了 3 个参数,前两个为Int类型: arg1
和 arg2
,最后一个参数是一个函数,并且函数类型是传入两个 Int 参数并返回 Boolean 类型值。 最后这个高阶函数自己的返回类型是 Int 值。
使用高阶函数
至此,我们可以将一个 Lambda传递给这个高阶函数,完整示例如下:
fun highOrderFunc(arg1: Int, arg2: Int, paramFunc: (a: Int, b: Int) -> Boolean): Int{
return if (paramFunc(arg1, arg2)){ arg1 }
else{ arg2 }
}
fun main(){
val sum1 = {x: Int, j: Int -> x + j} // sum1 = x+j
val sum2 : (x:Int, j: Int) -> Int = {a, b -> a+b}
val max = {x: Int, y: Int -> x > y}
println(sum1)
println(sum2)
println(max)
println(sum1(10,20))
println(sum2(10,20))
println(max(10,20))
val biggerNum = highOrderFunc(60,80,max)
println("biggerNum is $biggerNum")
}
Output:
(kotlin.Int, kotlin.Int) -> kotlin.Int
(kotlin.Int, kotlin.Int) -> kotlin.Int
(kotlin.Int, kotlin.Int) -> kotlin.Boolean
30
30
false
biggerNum is 80
Kotlin —— 集合详解
集合是开发中非常常用的知识,比如操作各种数据集,各种算法,保存网络请求结果,作为 Adapter
数据集。
Kotlin 中的集合按照可变性分类可以分为:
- 不可变集合,一个 只读 接口,提供访问集合元素的操作。
- 可变集合,一个 可变 接口,通过写操作扩展相应的只读接口:添加、删除和更新其元素。
按照类型分类可以分为:
-
List集合
List是一个有序集合,可通过索引(反映元素位置的整数)访问元素。元素可重复。 -
Map集合
Map又称为 字典,是一组 Key-Value 对。Key是唯一的,每个Key都刚好映射到一个值。值可以重复,map对于存储对象之间的逻辑连接非常有用。例如,员工的ID与员工的位置。 -
Set集合
Set是唯一元素的集合,即元素不可重复。它反映了集合 (Set)的数学抽象:一组无重复的对象。一般来说 set 中元素的顺序并不重要。例如,字母表是字母的集合(set)。
Kotlin让你可以独立于所存储对象的确切类型类操作集合。换句话说,将 String
添加到 String
list 中的方式与添加 Int
或者用户自定义类的相应list中的方式相同。因此,Kotlin标准库为创建、填充、管理任何类型的集合提供了泛型的(通用的,双关)接口、类与函数。
这些集合接口与相关函数位于 kotlin.collections 包中。
结合在一起就是说 List、Map、Set又可分为可变和不可变两种。
具体来说
对于List:
- List —— 声明不可变List集合
- MutableList —— 声明可变List集合
对于Map:
- Map —— 声明不可变Map集合
- MutableMap —— 声明可变Map集合
对于Set:
- Set —— 声明不可变Set集合
- Mutable —— 声明可变Set集合
除此之外,还有四个基本接口:
- Iterable —— 所有集合的父类
- MutableIterable —— 继承于Iterable接口,支持遍历的同时可以执行删除操作
- Collection —— 继承于Iterable 接口,仅封装了对集合的只读方法
- MutableCollection —— 继承于Iterable,Collection 封装了添加或移除集合中元素的方法
此外,记住List,MutableList,Set,MutableSet归根到底都是 Collection的子类。
请注意,更改可变集合不需要它是以 var
定义的变量:因为写操作修改同一可变集合对象,因此引用不会改变。但是,如果尝试对 val
集合重新赋值,将收到编译错误。
fun main(){
val numbers = mutableListOf("one","two","three")
numbers.add("four")
println(numbers) // [one, two, three, four]
numbers = mutableListOf("four","five") // error: val has been assigned
}
型变
只读集合( listOf<>()
)类型是 型变 的。这意味着,如果类 Rectangle
继承自 Shape
,则可以在需要 List<Shape>
的任何地方使用 List<Rectangle>
。换句话说,集合类型与元素类型具有相同的子类型关系。
val list = listOf<Shape>()
val r = Rectangle()
list.add(r)
反之,可变集合 ( mutableListOf<>()
)不是型变的 ,否则将导致运行时故障。如果 MutableList <Rectangle>
是 MutableList <Shape>
的子类型,可以在其中插入其他 Shape
的继承者(例如 Circle
),从而违反了它的 Rectangle
类型参数。
示例如下:
open class Parents
class Child1: Parents()
class Child2: Parents()
fun covariant( list1: List<Parents>, list2: MutableList<Parents>){
println(list1)
println(list2)
}
fun main(){
val c1 = Child1()
val c11 = Child1()
val c2 = Child2()
val list = listOf<Child1>(c1, c11)
val mList = mutableListOf<Child2>(c2)
/*
* Type mismatch.
* Required:
* MutableList<Parents>
*Found:
* MutableList<Child2>
*/
covariant(list,mList) // mList 会报 error
}
注解:在 covariant()
方法中的两个参数分别为: 不可变list list1<Parants>
和 可变list list2<Parents>
,在后续调用此方法时,将 Parents
的子类 listOf<Child1>
和 mutableListOf<Child2>
分别作为此函数的两个参数代入,会发现 covariant(list,mList)
的后者会报错。
map
在值(value)类型上是型变的,但在键(key)类型上不是。
Kotlin 集合接口
Collection
Collection<T>
是集合层次结构的根。 此接口表示一个只读集合的共同行为:检索大小、检测是否为成员等等。 Collection
继承自 Iterable <T>
接口,它定义了迭代元素的操作。可以使用
Collection
作为适用于不同集合类型的函数的参数。对于更具体的情况,请使用 Collection
的继承者: List
与 Set
。
fun printAll(str: Collection<String>){
for (s in str){
print("$s ")
}
println()
}
fun main(){
val list = listOf<String>("one", "two", "two")
val set = setOf<String>("one", "one", "two")
printAll(list)
printAll(set)
}
Output:
one two two
one two
注意: set
打印出来只有两个元素。
MutableCollection
是一个具有写操作的 Collection
接口,例如 add
以及 remove
。
fun List<String>.getShortWordsTo(shortWords: MutableList<String>, maxLength: Int) {
this.filterTo(shortWords) { it.length <= maxLength }
// throwing away the articles
val articles = setOf("a", "A", "an", "An", "the", "The")
shortWords -= articles
}
fun main() {
val words = "A long time ago in a galaxy far far away".split(" ")
val shortWords = mutableListOf<String>()
words.getShortWordsTo(shortWords, 3)
println(shortWords)
}
Output:
[ago, in, far, far]
List
List<T>
以指定的顺序存储元素,并提供使用索引访问元素的方法。索引从 0 开始。
val numbers = listOf("one", "two", "three", "four")
list.size
println("Number of elements: ${numbers.size}")
// Number of elements: 4
list.get()
println("Third element: ${numbers.get(2)}")
// Third element: three
list[index]
println("Fourth element: ${numbers[3]}")
// Fourth element: four
list.indexOf()
println("Index of element \"two\" ${numbers.indexOf("two")}")
// Index of element "two" 1
List 元素(包括空值)可以重复:List 可以包含任意数量的相同对象或单个对象的出现。如果两个 List 在相同的位置具有相同大小和相同结构的元素,则认为它们是相等的。
MutableList<T>
是可以进行写操作的 List
,例如用于在特定位置添加或删除元素。
val numbers = mutableListOf(1, 2, 3, 4)
numbers.add(5) // [1,2,3,4,5]
numbers.removeAt(1) // [1,3,4,5]
numbers[0] = 0 // [0,3,4,5]
numbers.shuffle() // 洗牌
println(numbers) // [4,5,0,3]
如你所见,在某些方面,List 与数组(Array)非常相似。但是有一个重要的区别:数组的大小是在初始化时定义的,永远不会改变;反之,List没有预定义的大小;作为写操作的结果,可以更改List的大小:添加、更新或删除元素。
Set
Set<T>
存储唯一的元素;它们的顺序通常是未定义的。 null
元素也是唯一的,即一个Set只能包含一个 null。当两个 set 具有相同的大小并且 对于一个 set 中的每个元素都能在另一个 set 中存在相同的元素, 则两个 set 相等。
MutableSet
是一个带有来自 MutableCollection 的写操作接口的 set。
Set的默认实现 - LinkedHashSet,即保留元素插入的顺序。因此,依赖于顺序的函数,例如 first()
或 last()
会在这些 set 上返回可预测的结果。
fun main() {
val numbers = setOf(1, 2, 3, 4) // LinkedHashSet is the default implementation
val numbersBackwards = setOf(4, 3, 2, 1)
println(numbers.first() == numbersBackwards.first())
println(numbers.first() == numbersBackwards.last())
}
// Output: false
// true
Map
Map<K, V>
不是 Collection 接口的继承者;但是它也是 Kotlin 的一种集合类型。 Map 存储 Key-Value 对; 键是唯一的,但是不同的键可以与相同值配对。Map 接口提供特定的函数进行通过键访问值、搜索键和值等操作。
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
println("All keys: ${numbersMap.keys}")
// All keys: [key1, key2, key3, key4]
println("All values: ${numbersMap.values}")
// All values: [1, 2, 3, 1]
if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}")
// Value by key "key2": 2
if (1 in numbersMap.values) println("The value 1 is in the map")
// The value 1 is in the map
if (numbersMap.containsValue(1)) println("The value 1 is in the map") // 同上
// The value 1 is in the map
无论键值对的顺序如何,包含相同键值对的两个 Map 是相等的。
MutableMap
是一个具有写操作的 Map
接口,可以使用该接口添加一个新的键值对或更新给定键的值。
val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap.put("three", 3)
numbersMap["one"] = 11
println(numbersMap)
// {one=11, two=2, three=3}
Map
的默认实现 – LinkedHashMap
– 迭代 Map 时保留元素插入的顺序。 反之,另一种实现 – HashMap
– 不声明元素的顺序。
构造集合
由元素构造
创建集合的最常用方法是使用标准库函数 listOf<T>()
、setOf<T>()
、mutableListOf<T>()
、mutableSetOf<T>()
。 如果以逗号分隔的集合元素列表作为参数,编译器会自动检测元素类型。创建空集合时,须明确指定类型。
val numbersSet = setOf("one", "two", "three", "four")
val emptySet = mutableSetOf<String>()
同样的,Map 也有这样的函数 mapOf()
与 mutableMapOf()
。映射的键和值作为 Pair
对象传递(通常使用中缀函数 to
创建)。
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
注意:to
符号创建了一个短时存活的 Pair
对象,因此建议仅在性能不重要时才使用它。 为避免过多的内存使用,请使用其他方法。例如,可以创建可写 Map 并使用写入操作填充它。 apply()
函数可以帮助保持初始化流畅,示例:
val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }
空集合
还有用于创建没有任何元素的集合的函数: emptyList()
, emptySet()
, emptyMap()
。创建空集合时,应指定集合将包含的元素类型。
val empty = emptyList<String>()
list 的初始化函数
对于 List,有一个接受 List 的大小与初始化函数的构造函数,该初始化函数根据索引定义元素的值。
val doubled = List(3, { it * 2 }) // [0,2,4]
具体类型构造函数
要创建具体类型的集合,例如 `ArrayList或
LinkedList``` ,可以使用这些类型的构造函数。类似的构造函数对于 Set 与 Map 的各实现中均有提供。
操作符
总数操作
any
说明:如果至少有一个元素符合判断条件,则返回true,否则 false。
val list = listOf(1,2,3,4,5)
list.any{ it > 10} // false
all
说明:如果集合中所有的元素都符合判断条件,则返回true,否则false
list.all{ it > 0} // true
count
说明:返回集合符合判断条件的元素总数。
list.count{ it < 3} // 2
fold
说明:在一个初始值的基础上,从第一项到最后一项,通过一个函数累计所有的元素。
list.fold(2){ total, next -> total + next} // (((((2+1)+2)+3)+4)+5) = 17
foldRight
说明:与 fold
一样,但是顺序是从最后一项到第一项
list.foldRight(15){ total, next -> total - next}
(1 - (2 - (3 - (4 - (5 - 15)))) = -12
reduce
说明:与 fold
一样,但是没有一个初始值。通过一个函数从第一函数从第一项到最后一项进行累计。
list.reduce{ total, next -> total + next} // 15
reduceRight
说明:与 foldRight
一样,只不过没有初始值。
list.reduceRight { total, next -> total + next } // 15
forEach
说明:遍历所有元素,并执行给定的操作(类似于Java 中的 for
循环)。
list.forEach{ Log.i(TAG,it.toString()) } // 1 2 3 4 5
forEachIndexed
说明:与 forEach
作用一样,但是同时可以得到元素的 index 。
list.forEachIndexed { index, i -> Log.i(TAG, "index:" + index + " value:" + i.toString()) }
Output:
index:0 value:1
index:1 value:2
index:2 value:3
index:3 value:4
index:4 value:5
max
说明:返回集合中最大的一项,如果没有则返回 null。
Log.i(TAG, list.max().toString()) // 5
min
说明:返回集合中最小的一项,如果没有则返回null
Log.i(TAG, list.min().toString()) // 1
maxBy
说明:根据给定的函数返回最大的一项,如果没有则返回null。
Log.i(TAG, list.maxBy { it-10 }.toString())
结果为 :5 (因为从1到5这5个元素中只有5减去10后的值最大,所以返回元素5,注意返回的不是计算结果)
sumBy
说明:返回所有每一项通过函数转换之后的数据的总和
list.sumBy { it + 2 } // 25
每个元素都加2,最后求和
过滤操作
drop
说明:返回去掉前n个元素的列表。
val list = listOf(1, 2, 3, 4, 5)
var s = list.drop(2) // 3, 4, 5
dropWhile
说明:返回根据给定函数从第一项开始去掉指定元素的列表
list.dropWhile { it < 3 } // 3, 4, 5
dropLastWhile
说明:同dropWhile,但是是从最后一项开始
list.dropLastWhile { it >3 } // 1, 2, 3
filter
说明:过滤所有符合给定函数条件的元素
list.filter { it > 2 } // 3 4 5
filterNot
说明:过滤所有不符合给定函数条件的元素
list.filterNot{ it > 2 } // 1 2
filterNotNull
说明:过滤所有元素中不是null的元素
list.filterNotNull() // 1 2 3 4 5
slice
说明:过滤集合中指定index的元素(其实就是获取指定index的元素)
list.slice(listOf(0,4)) // [1, 5]
take
说明:返回从第一个开始的n个元素
list.take(2) // [1, 2]
takeLast
说明:返回从最后一个开始的n个元素
list.takeLast(2) // [4, 5]
takeWhile
说明:返回从第一个开始符合给定函数条件的元素
list.takeWhile { it<3 } // [1, 2]
映射操作
flatMap
说明:遍历所有的元素,为每一个创建一个集合,最后把所有的集合放在一个集合中
list.flatMap { listOf(it, it + 1) } // [1, 2, 2, 3, 3, 4, 4, 5, 5, 6] 每个元素都执行加1后作为一个新元素
groupBy
说明:返回一个根据给定函数分组后的map
list.groupBy { if (it >3) "big" else "small" }
small=[1, 2, 3]
big=[4, 5]
map
说明:返回一个每一个元素根据给定的函数转换所组成的集合
list.map { it * 2 } // [2, 4, 6, 8, 10]
mapIndexed
说明:返回一个每一个元素根据给定的包含元素index的函数转换所组成的集合
val list = listOf(0,1,2,9,4)
list.mapIndexed { index, it -> index - it }
// [0-0, 1-1, 2-2, 3-9, 4-4]
mapNotNull
说明:返回一个每一个非null元素根据给定的函数转换所组成的集合
list.mapNotNull { it * 2 } // 2 4 6 8 10
顺序操作
reversed
说明:返回一个与指定集合相反顺序的集合
list.reversed() // 5 4 3 2 1
sorted
说明:返回一个自然排序后的集合。例:
val list = listOf(1, 2, 6, 4, 5)
var s = list.sorted() // 1 2 4 5 6
sortedBy
说明:返回一个根据指定函数排序后的集合。例:
val list = listOf(1, 2, 6, 4, 5)
var s = list.sortedBy { it % 3 } // [6, 1, 4, 2, 5] 比较(it%3) 之后的大小
sortedDescending
说明:返回一个降序排序后的集合。例:
list.sortedDescending() // 5 4 3 2 1
sortedByDescending
说明:返回一个根据指定函数降序排序后的集合。例:
list.sortedByDescending { it % 3 } // [2, 5, 1, 4, 6]
生产操作
partition
说明:把一个给定的集合分割成两个,第一个集合是由原集合每一项元素匹配给定函数条件返回true的元素组成,第二个集合是由原集合每一项元素匹配给定函数条件返回false的元素组成。
val list = listOf(1, 2, 3, 4, 5)
var s = list.partition { it > 2 } // ([6, 4, 5], [1, 2])
plus
说明:返回一个包含原集合和给定集合中所有元素的集合,因为函数的名字原因,我们可以使用+操作符。
val list = listOf(1,2,6,4,5)
val newList = list + listOf(6,7)
println("$newList") // [1, 2, 6, 4, 5, 6, 7]
zip
说明:返回由pair组成的List,每个pair由两个集合中相同index的元素组成。这个返回的List的大小由最小的那个集合决定
val list = listOf(1,2,6,4,5)
println("${list.zip(listOf(8,9,10,11,12))}") // [(1, 8), (2, 9), (6, 10), (4, 11), (5, 12)]
unzip
说明:从包含pair的List中生成包含List的Pair
var s = listOf(1 to 2, 3 to 4, 5 to 6).unzip()
println(s) // ([1, 3, 5], [2, 4, 6])
元素操作
contains
说明:指定元素可以在集合中找到,则返回true,否则false
list.contains(1) // true
elementAt
说明:返回给定index对应的元素,如果index数组越界则会抛出IndexOutOfBoundsException
list.elementAt(1) // 2
elementAtOrElse
说明:返回给定index对应的元素,如果index数组越界则会根据给定函数返回默认值
val list = listOf(1,2,6,4,9)
println(list.elementAtOrElse(4, {it + 2})) // 9
println(list.elementAtOrElse(5, {it + 2})) // 超出边界 所以返回 7 = 5+2
elementAtOrNull
说明:返回给定index对应的元素,如果index数组越界则会返回null
list.elementAtOrNull(5) // null
first
说明:返回符合给定函数条件的第一个元素
list.first { it > 2 } // 3
firstOrNull
说明:返回符合给定函数条件的第一个元素,如果没有符合则返回null
list.first { it > 8 } // null
indexOf
说明:返回指定元素的第一个index,如果不存在,则返回-1
list.indexOf(2) // 1
indexOfFirst
说明:返回第一个符合给定函数条件的元素的index,如果没有符合则返回-1
list.indexOfFirst { it % 2 == 0 } // 1
indexOfLast
说明:返回最后一个符合给定函数条件的元素的index,如果没有符合则返回-1
list.indexOfLast { it % 2 == 0 } // 3
last
说明:返回符合给定函数条件的最后一个元素
list.last { it % 2 == 0 } // 4
lastIndexOf
说明:返回指定元素的最后一个index,如果不存在,则返回-1
val list = listOf(1,4,6,4,9)
println(list.lastIndexOf(4)) // 3
lastOrNull
说明:返回符合给定函数条件的最后一个元素,如果没有符合则返回null
val list = listOf(1,4,7,4,9)
println(list.lastOrNull { it > 6 }) // 9
single
说明:返回符合给定函数的单个元素,如果没有符合或者超过一个,则抛出异常
val list = listOf(1,4,7,4,9)
list.single { it > 4 } // 抛出异常
singleOrNull
说明:返回符合给定函数的单个元素,如果没有符合或者超过一个,则返回null
list.singleOrNull { it > 5} // null
Kotlin 协程
Kotlin 协程值得关注的功能点包括:
-
轻量
可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
正在执行的进程由于发生某时间(如I/O请求、申请缓冲区失败等)暂时无法继续执行。此时引起进程调度,OS把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态。
线程 在运行的过程中,因为某些原因而发生阻塞,阻塞状态的线程特点是:该线程放弃CPU的使用,暂停运行, 只有等到导致阻塞的原因消除之后才恢复运行。或是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException
内存泄漏更少
使用 结构化并发 机制在一个作用域内执行多个操作内置取消支持
取消功能会自动通过正在运行的协程层次结构传播Jetpack 集成
许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供用于结构化并发。
第一个协程
协程可以称为 轻量级线程。Kotlin 协程在 CoroutineScope 的上下文中通过 launch、async等协程构造器(CoroutineBuilder)来声明并启动。
fun main() {
GlobalScope.launch(context = Dispatchers.IO) {
//延时一秒
delay(1000)
log("launch")
}
//主动休眠两秒,防止JVM过快退出
Thread.sleep(2000)
log("end")
}
private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")
Output:
[DefaultDispatcher-worker-1] launch
[main] end
在上面例子中,通过 GlobalScope (即全局作用域)启动了一个协程,在延迟一秒后输出一行日志。从输出结果可以看出来,启动的协程是运行在协程内部的线程池中。虽然从表现结果上来看,启动一个协程类似于我们直接使用Thread来执行耗时任务,但实际上协程和线程有着本质上的区别。通过使用协程,可以极大地提高线程的并发率,避免以往的嵌套回调地狱,极大提高了代码的可读性。
一般情况下,应用程序会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数。
为了实现某些逻辑,经常会写出层层嵌套回调函数,如果嵌套过多,会极大地影响代码可读性和逻辑,这种情况也被称为回调地狱(callback hell)
以上代码就涉及到了协程的四个基础概念:
- suspend function:即挂起函数,delay函数就是协程库提供的一个用于实现非阻塞式延时的挂起函数。
-
CoroutineScope:即协程作用域,
GlobalScope
是 CoroutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期,所有协程都需要通过 CoroutineScope 来启动 -
CoroutineContext:即协程上下文,包含多种类型的配置参数。
Dispatchers.IO
就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上 -
CoroutineBuilder:即协程构建器,协程在 CoroutineScope 的上下文中通过
launch
、async
等协程构建器来进行声明并启动。launch
、async
等均被声明 CoroutineScope 的扩展方法。
suspend function
如果上述例子,试图直接在 GlobalScope
外调用 delay()
函数的话,IDE就会提示一个错误: Suspend function 'delay' should be called only from a coroutine or another suspend function
。意思是: delay()
函数是一个挂起函数,只能由协程或者由其它挂起函数来调用。
delay()
函数就使用了 suspend
进行修饰,用 suspend
修饰的函数就是挂起函数。
public suspend fun delay(timeMillis: Long)
读者在网上看关于协程的文章的时候,经常会看到这么一句话:挂起函数不会阻塞其所在线程,而是会将协程挂起,在特定的时候才再恢复协程
对于这句话,我的理解是: delay()
函数类似于 Java 中的 Thread.sleep()
,而之所以说 delay()
函数时非阻塞的,是因为它和单纯的线程休眠有着本质的区别。协程是运行与线程上的,一个线程可以运行多个(几千上万个)协程。线程的调度行为是由操作系统来管理的,而协程的调度行为是可以有开发者来指定并由编译器来实现的。协程能够细颗粒度的控制多个任务的执行时机和执行线程,当某个特定的线程上的所有协程被 suspend
后,该线程便可腾出资源去处理其他任务。
例如,当在 ThreadA
上运行的 CoroutineA
调用了 delay(1000L)
(1000Long)函数指定延迟一秒后再运行, ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA的延时而阻塞,而是能继续去执行其他任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用 Thread.sleep()
的话,线程就只能干等着而不会去执行其他任务,降低了线程的利用效率。
suspend function 的挂起与恢复
协程在常规函数的基础上,添加了两项操作用于处理长时间运行的任务。在 invoke
( call
) 和 return
之外,协程添加了 suspend
和 resume
:
-
suspend
用于暂停执行当前协程,并保存所有局部变量。 -
resume
用于让已暂停的协程从暂停处继续执行
suspend
函数只能由其它suspend
函数调用,或者是由协程来调用
以下示例展示了一项任务(假设 get 方法是一个网络请求任务)的简单协程实现:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
在上面的示例中, get()
仍在主线程上被调用,但它会在启动网络请求之前暂停协程。 get()
主体内通过调用 withContext(Dispatchers.IO)
创建了一个在IO线程池中运行的代码块,在该块内的任何代码都始终通过IO调度器执行。当网络请求完成后, get()
会恢复已暂停的协程,使得主线程协程可以直接拿到网络请求结果而不用使用回调来通知主线程。 Retrofit 就是以这种方式来实现对协程的支持的。
可以说,在Android平台上协程主要就用来解决两个问题:
- 处理耗时任务(Long running tasks),这种任务常常会阻塞住主线程。
-
保证主线程安全(Main-safety),即确保安全地从主线程调用任何
suspend
函数。
CoroutineScope
CoroutineScope
即协程作用域,用于对协程进行追踪。如果我们启动了多个协程但是没有一个可以对其进行统一管理的途径的话,那么就会导致我们的代码臃肿杂乱,甚至发生 内存泄漏 或者 任务泄漏。为了确保所有的协程都会被追踪,Kotlin不允许在没有使用 CoroutineScope
的情况下启动新的协程。 CoroutineScope
可被看作是一个具有超能力的 ExecutorService
的轻量级版本。它能启动新的协程,同时这个协程还具备上文所说的 suspend
和 resume
的优势。
所有的协程都需要通过 CoroutineScope
来启动 ,它会跟踪它使用 launch
或 async
创建的所有协程,你可以随时调用 scope.cancel()
取消正在运行的协程。CoroutineScope
本身并不运行协程,它只是确保不会失去对协程的追踪,即使协程被挂起也是如此。在Android中,某些KTX库为某些生命周期类提供了自己的 CoroutineScope
。例如, ViewModel
有 viewModelScope
,Lifecycle
有 lifecycleScope
。
CoroutineScope
大体上可以分为三种:
-
GlobalScope
。即全局协程作用域,在这个范围内启动的协程可以一直运行知道应用停止运行。 -
runBlocking
。一个顶层函数,和GlobalScope不一样,它会阻塞当前线程直到其内部所有相同作用域的协程执行结束。
顶层函数:在Kotlin中,顶层函数相当于Java中的静态函数。
- 自定义 CoroutineScope。可用于实现主动控制协程的生命周期范围,对于Android开发来说,最大意义之一就是可以避免内存泄漏。
内存泄漏:申请的内存空间没有及时释放或者丢了指针没法释放,造成可用内存越来越少。
GlobalScope
GlobalScope
属于 全局作用域 ,这意味着通过 GlobalScope
启动的协程的生命周期只受整个应用程序的生命周期的限制,只要整个应用程序还在运行且协程的任务还未结束,协程就可以一直运行。
GlobalScope
不会阻塞其所在线程,所以以下代码中主线程的日志会早于 GlobalScope
内部输出日志。此外, GlobalScope
启动的协程相当于守护线程,不会阻止JVM结束运行,所以如果将主线程的休眠时间改为300ms的话,就不会看到 launchA输出日志。
fun main() {
log("start")
GlobalScope.launch { // 在后台启动一个新的协程并继续
launch {
delay(299) // 非阻塞的等待
log("launch A")
}
launch {
delay(300)
log("launch B")
}
log("GlobalScope")
}
log("end")
Thread.sleep(310) // 阻塞主线程 来保证JVM的存活
}
private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")
Output:
[main] start
[main] end
[DefaultDispatcher-worker-1] GlobalScope
[DefaultDispatcher-worker-2] launch A
[DefaultDispatcher-worker-2] launch B
GlobalScope.launch
会创建一个顶级协程,尽管它很轻量级,但在运行时还是会消耗一些内存资源,且可以一直运行知道整个应用程序停止(只要任务还未结束),这可能会导致内存泄漏(因为 GlobalScope.launch
会消耗内存资源,有可能额外占用内存,导致内存空间没有及时释放),所以在日常开发中应该谨慎使用
GlobalScope
。
runBlocking
也可以使用 runBlocking
这个顶层函数来启动协程, runBlocking
函数的第二个参数即协程的执行体,该参数被声明为 CoroutineScope
的扩展函数,因此执行体就包含了一个隐式的 CoroutineScope
,所以在 runBlocking
内部可以来直接启动协程
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
runBlocking
的一个方便之处就是:只有当内部 相同作用域 的所有协程都运行结束后,声明在 runBlocking
之后的代码才能执行,即 runBlocking
会阻塞其所在线程。
runBlocking
内部启动的两个协程会各自做耗时操作,从输出结果可以看出,两个协程还是在交叉并发执行,且 runBlocking
会等到两个协程都执行结束后才会退出,外部的日志输出结果有明确的先后顺序。即 runBlocking
内部启动的协程是非阻塞式的,但 runBlocking
阻塞了其所在的线程。此外, runBlocking
只会等待相同作用域的协程完成才会退出,而不会等待 GlobalScope
等其它作用域启动的协程。
fun main() {
log("start")
runBlocking {
launch {
repeat(3){
delay(100)
log("launchA - $it")
}
}
launch {
repeat(3){
delay(100)
log("launchB - $it")
}
}
GlobalScope.launch {
repeat(3){
delay(120)
log("GlobalScope - $it")
}
}
}
log("end")
}
private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")
Output:
[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end
解释:
- 可以看到,
runBlocking
会把主线程阻塞后执行自己内部的协程,等执行完后在释放主线程 - 可以看到,launchA 和 launchB 的结果是交错的。因为内部的协程是交叉并发执行
- 可以看到,
GlobalScope
并没有执行完,那是因为runBlocking
只会等待相同作用域的协程完成才会退出,并不会等待GlobalScope
等其它作用域启动的协程。
注: runBlocking
会早于 GlobalScope
输出日志
CorountineBuilder
launch
launch
是一个作用于 CoroutineScope
的扩展函数,用于在不阻塞当前线程的情况下,并返回对该协程任务的引用,即Job对象。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch
函数包含三个参数:
- context. 用于指定协程的上下文
- start. 用于指定协程的启动方式。默认是
CoroutineStart.DEFAULT
,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为CoroutineStart.LAZY
来实现延迟启动。 - block. 用于传递协程的执行体,即希望交由协程执行的任务。
fun main() = runBlocking {
val launchA = launch {
repeat(3) {
delay(100)
log("launchA - $it")
}
}
val launchB = launch {
repeat(3) {
delay(100)
log("launchB - $it")
}
}
}
Output:
[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2