Kotlin概述
1、什么是Kotlin
[Kotlin官方文档] (https://kotlinlang.org/docs/home.html)
[中文网文档] (https://book.kotlincn.net/)
[简单教程] (https://www.twle.cn/l/yufei/kotlin/kotlin-basic-index.html)
KOtlin是一个现代多平台的静态编程语言,由JetBrains开发。Kotlin可以编译成Java字节码,也可以编译成Javascript和二进制代码方便在没有JVM的设备上运行。2、Kotlin的特点(优点)
- 兼容Java
- 相比于Java更安全,能静态检测常见的陷阱。如空指针异常
- 语法更简洁,更容易理解
- 完善的生态
- 底层封装Getter/Setter方法,可以直接调用
- 协程支持
- 更优秀的lazy初始化
- lambda表达式
- 扩展功能(装饰器模式)
- 自动类型判断
3、Kotlin的缺点
- 编译较慢
- Java转Kotlin有时候会破坏结构
- 由Kotlin接受Java返回的空指针(NUll)会出现KotlinNullPointerException
- 抛弃了static类型
- 抛弃了Checked Exception。在Kotlin中当有throws时即使不加try
- catch也可以编译通过,这会导致程序存在未知的风险。
第一个Hello World
fun main(args: Array<String>) { println("Hello World!") }
基础语法
Kotlin文件以.kt结尾.包
如同Java,代码的开头一般为包的声明。但Kotlin源文件不需要相匹配的目录和包,源文件可以放在任何文件目录。以上例中 test() 的全名是 com.runoob.main.test、Runoob 的全名是 com.runoob.main.Runoob。默认为default包。
```Kotlin
package com.runoob.main
import java.util.*
fun test() {}
class Runoob {}
### 注:
以下包会默认导入到每个Kotlin文件中
* kotlin.*
* kotlin.annotation.*
* kotlin.collections.*
* kotlin.comparisons.*
* kotlin.io.*
* kotlin.ranges.*
* kotlin.sequences.*
* kotlin.text.*
## 函数定义
### 方式1
函数定义使用关键字fun(function缩写),参数格式为:参数名:类型
```Kotlin
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 方法则必须明确写出返回类型
无返回值的函数(类似void)
fun printSum(a: Int, b: Int): Unit { //Unit可不写
print(a + b)
}
// 如果是返回 Unit类型,则可以省略(对于public方法也是这样):
public fun printSum(a: Int, b: Int) {
print(a + b)
}
方式2
除了传统的函数定义方式,Kotlin还支持类似js的隐式函数定义。
函数定义格式为:
var 函数名:()->返回类型
例如:
var myFun:()->Unit
已经定义好了函数,那么怎么实现它呢?
var myFun:()->Unit
myFun = {
println("我被实现了")
}
// 实现myFun
myFun()
// 调用myFun
与方式1不同的是,该定义方式会将代码块最后一行作为值return,如下。
var myFun:()->String
myFun = {
"Hello"
}
println(myFun())
既然已经了解它的语法,那么我们再来将它简化一下。
var myFun:()->String = {
"Hello"
}
println(myFun())
写了这么多,是不是会发现怎么没有传参,现在就说说。
在隐式函数中,参数直接写入类型即可
方式3
具有自动推断返回类型的匿名函数
val 函数名 ={ 形参1:类型, 形参2:类型 -> 表达式,... _> 表达式 }
var myFun = { x: Int, y: Int -> x + y }
当返回类型可能性为两种及以上,就会返回Any
类型
var myFun = { x: Int, y: Int -> if (x > y) x else 'y' }
it关键字
在函数(不管是不是隐式函数)只有一个形参时,函数会自带一个it
关键字来表示该形参。
不定长参数(类似String[] args)
函数的不定长参数可以用vararg关键字来定义:
fun vars(vararg v:Int){
for(vt in v){
print(vt)
}
}
// 测试
fun main(args: Array<String>) {
vars(1,2,3,4,5) // 输出12345
}
函数默认参数
Kotlin允许和python一样的默认参数,可以让函数更加简洁:
fun sum(a:Int,b:Int=0):Int{
return a+b
}
函数具名参数
和Python一样,在函数参数过长时,可以使用具名参数来选择参数:
fun login(username:String,password:String,phonenumber:String){}
login(username="小王")
// 指明要赋值的参数
lambda匿名函数
// 测试
fun main(args: Array<String>) {
val sum: (Int, Int) -> Int = {x,y -> x+y}
println(sum(1,2)) // 输出 3
}
内联函数
在Kotlin将匿名函数转为字节码过成中,匿名函数会被拆分成无数小对象,于是会参数多次调用造成性能浪费。
于是,使用inline
修饰调用匿名函数的函数即可将匿名函数转为一个宏对象(相当于C的#define)。
示例如下,不要在意套娃,知道意思即可。
回调函数callback
Kotlin中的回调函数与javacript基本一直
// Kotlin回调函数
fun loginAPi(username: String, password: String, callback: (Boolean, String) -> Unit) {
// 模拟登录
val isLoginSuccess = (username == "admin" && password == "123456")
callback(isLoginSuccess, if (isLoginSuccess) "登录成功" else "登录失败")
}
fun main() {
loginAPi("admin", "123456") { isLoginSuccess, message ->
if (isLoginSuccess) {
println("登录成功:$message")
} else {
println("登录失败:$message")
}
}
}
常量和变量
变量定义:var关键字
var <标识符> : <类型> = <初始化值>
常量定义:val关键字
val <标识符> : <类型> = <初始化值>
自定义数据类型
虽然Kotlin支持自动推导类型,但是也可以自己指定类型
val a: Int = 1 //指定类型为Int
val b = 1 // 系统自动推断变量类型为Int
val c: Int // 如果没有初始化变量,必须指明类型
注释
// 多行注释
/*
多行注释
*/
字符串模板
$
表示一个变量名或者变量值
var a = 1
println("a is $a")
$变量名 表示该变量的值
${表达式} 表示该表达式的返回值
NUll检查机制
Kotlin的空安全设计对于声明可为空的参数,在使用时要进行空判断处理,有两种处理方式,字段后加!!
像Java一样抛出空异常
,另一种字段后加?
可不做处理返回null
或配合?:
做空判断
处理
//表示age可为空
var age:String? = null
//表示如果出现空指针异常就正常抛出
val ages = age!!.toInt()
//相当于
if(age!=null){
// 因为age为null所以会抛出异常
val ages = age.toInt()
}
//不会抛出空指针异常,因为自动捕获了
// 在这里会出现空指针异常,此时ages1会取age的值,即null
val ages1 = age?.toInt()
//类似于
try {
val ages1 = age!!.toInt()
} catch (e: NullPointerException) {
val ages1 = null
}
// ?: 表示前面的参数如果为空就会返回:后的值
// 在这里因为age为null,即会返回0给age2
var age2 = age?:0
如果函数的返回值可能为空时,应该明确的用?标记其可能返回null
fun parseInt(str: String): Int? {
return null
}
综上所述,当大量需要
判断为null时会造成大量的代码冗余,当函数内使用全局变量
时无法使用?进行判空,此时就可以使用let()
函数,let可以在所有对象调用.
val food: String? = "meat"
//表示object不为null的条件下,才会去执行let函数体
food?.let {
print("非空")
}
food?.let{
// let也自带it
print(it)
}
food?.let{ a->
// 也可以自定义
print(a)
}
类型检测及自动类型转换
我们可以使用is关键字来判断某个实例是否是某个类型。
val a=1
println(a is Int)
println(a::class.java.typeName)
true
int
区间(range表达式)
Kotlin提供了一个区间操作符..,可以很方便的表示一个区间
for (i in 1..4) print(i) // 输出“1234”
for (i in 4..1) print(i) // 什么都不输出
if (i in 1..10) { // 等同于 1 <= i && i <= 10
println(i)
}
// 使用 step 指定步长
for (i in 1..4 step 2) print(i) // 输出“13”
for (i in 4 downTo 1 step 2) print(i) // 输出“42”
// 使用 until 函数排除结束元素
for (i in 1 until 10) { // 输出[1,10),排除了10
println(i)
}
Kotlin基本数据类型
字符不属于基本数据类型
类型 | 位宽 |
---|---|
Double | 64 |
Float | 32 |
Long | 64 |
Int | 32 |
Short | 16 |
Byte | 8 |
字面常量
- 十进制:123
- 长整型:132L
- 16进制(0x):0x0F
- 2进制(0b):0b00000001
- 不支持8进制
- Double浮点型:123.5
- Float浮点型:123.5f
数字比较
在Kotlin只有封装的数字类型,没有原始的数字类型,所以比较两个数字时,三个===表示比较对象地址,两个==表示比较值。
类型转换
由于数据类型不同的位宽,较小的类型不能隐式转换为较大的类型,所以需要显式转换。
val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b // 错误
可以使用toInt()方法进行强转
val b: Byte = 1 // OK, 字面值是静态检测的
val i: Int = b.toInt() // 错误
相应的,每种数据类型都有转换为其它类型的方法,如下:
toByte(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
toChar(): Char
在一些情况下,自动类型转换可以被允许,如下:
val l = 1L + 3 // Long + Int => Long
位操作符
相对于Int和Long,可以使用:
shl(bits) – 左移位 (Java’s <<)
shr(bits) – 右移位 (Java’s >>)
ushr(bits) – 无符号右移位 (Java’s >>>)
and(bits) – 与
or(bits) – 或
xor(bits) – 异或
inv() – 反向
字符
在Kotlin中,字符是由单双引号‘’ 括起来的,可以使用\转义字符。
字符可以显式toInt()转为Int
布尔
Boolean:true,false
布尔运算:||,&&,!
数组
数组是由Array实现。
创建数组的方式:
- arrayOf()
//[1,2,3] val a = arrayOf(1, 2, 3)
- 工厂函数
//[0,2,4] val b = Array(3, { i -> (i * 2) })
- 还有ByteArray, ShortArray, IntArray,用来表示各个类型的数组,省去了装箱操作,因此效率更高,其用法同Array一样
字符串
和Java一样,Kotlin中字符串也是由String类实现的。用[]可以很方便的获取某一个字符,也可以用for循环遍历字符串。
for (c in str) {
println(c)
}
Kotlin支持用三个”””括起来的字符串
val text = """
多行字符串
多行字符串
"""
println(text) // 输出有一些前置空格
多行字符串
多行字符串
使用trimMargin()方法可以去掉多余的空格,如下:
val text = """
|多行字符串
|多行字符串
""".trimMargin()
println(text) // 输出无前置空格
条件控制
if表达式
if表达式和其它语言大致相同,就不多赘述
// 传统用法
var max = a
if (a < b) max = b
// 使用 else
var max: Int
if (a > b) {
max = a
} else {
max = b
}
在Kotlin中可以将if表达式的结果赋值给一个变量,因此,Kotlin不支持三元操作符(? :)
// 作为表达式
val max = if (a > b) a else b
// 也可以这样写
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}
// 在Kotlin可以用in来判断变量是不是在某个区间里面
val x = 5
val y = 9
if (x in 1..8) {
println("x 在区间内")
}
when表达式
when为条件分支,类似其它语言的switch.
when(1){
1 -> println("1")
// 在Kotlin中else相当于swich的default
else -> println("else")
}
与switch不同的是,如果有很多分支需要用相同的方式处理,可以把多个分支用,分割,放在一起。
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
也可以检测一个值在(in)或者不在(!in)一个区间或者集合中:
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
val items = setOf("apple", "banana", "kiwi")
when {
"orange" in items -> println("juicy")
"apple" in items -> println("apple is fine too")
}
也可以使用is或者!is来检测特定的类型(注: 由于智能转换,你可以访问该类型的方法和属性而无需 任何额外的检测。)
when(x) {
is String -> x.startsWith("prefix")
else -> false
}
如果不提供参数,甚至可以用来取代if-else语句,如下:
var age = 8
when{
age>=18 -> println("adult")
age>=13 -> println("teenager")
age>=10 -> println("child")
else -> println("kid")
}
循环控制
for循环
Kotlin的for循环类似于python的for循环.
// 直接循环出对象
var items = arrayOf("apple", "banana", "orange")
for(item in items) print(item)
// 循环出索引index
var items = arrayOf("apple", "banana", "orange")
for(i in items.indices) print(i)
// 上面的循环只有索引,并没有对象。可以使用withIndex()方法来得到索引和对象
var items = arrayOf("apple", "banana", "orange")
for((item,index) in items.withIndex()) print("$index: $item\n")
while与do-while循环
while与其他语言一致
while( 布尔表达式 ) {
//循环内容
}
do-while循环也与其他语言一致
var a = 1
do {
println(a)
a++
} while (a < 10)
返回和跳转
在循环中 Kotlin 支持传统的 break 和 continue 操作符
for (i in 1..10) {
if (i==3) continue // i 为 3 时跳过当前循环,继续下一次循环
println(i)
if (i>5) break // i 为 6 时 跳出循环
}
Kotlin中的label标签
label标签的格式为:标签名@ 表达式。例如:
myLabel@ for…
```Kotlin
// 不带label的语句
val intArray = intArrayOf(1, 2, 3, 4, 5)
intArray.forEach{
if (it == 3) return
println(it)
}
/*
输出
1
2
*/
// 带label的语句
val intArray = intArrayOf(1, 2, 3, 4, 5)
intArray.forEach label@{
if (it == 3) return@label
println(it)
}
/
输出
1
2
4
5
/
使用label可以指定一个语句块的返回点,这个返回点可以在任何地方使用,而不仅仅是在语句块的最后。并且可以指定返回给谁。
```Kotlin
其实我没dong
类和对象
类
类的定义
Kotlin类可以包含:构造函数和初始化代码块、函数、属性、内部类、对象声明。
定义类用关键字class声明,后面紧跟类名
// 定义动物类
class Animal(){
}
// 定义空类
class Empty
//定义成员函数
class Animal(var name:String){
fun sayHello(){
println("Hello, $name")
}
}
重点: 实体类定义的语法糖
// 此语法糖会在test内部生成a,b,c变量
// 如果有val表示给该变量增加getter方法,var具有getter和setter,没有val或var则单纯只是一个变量
class test(val a:Int,var b:Int,c:Int)
类的属性
属性的定义
定义属性用var或者val关键字,格式为
class Animal {
var name: String = "Runoob"
}
我们class可以像普通函数那样,使用构造函数来创建实列
val animal = Animal() // 在Kotlin中没有new关键字
// 调用属性,只要用名称引用它就可以
animal.name
属性的get和set方法,可以通过get()和set()来访问和设置属性值。他俩都是可选的。val不允许设置set方法,因为它是只读的。
在get和set方法中,field变量是一个可见的变量,它是一个指向属性的引用。
Kotlin属性声明的完整格式如下:var 属性名称: 类型 get() = 表达式 set(value) = 表达式
设置get和set实例
class Animal { var name: String = "Animal" get() = field set(value) { field = value } }
非空属性必须在定义的时候初始化,kotlin提供了一种可以延迟初始化的方案,使用 lateinit 关键字描述属性(我不会)
主构造器
主构造器中不能包含任何代码,初始化代码可以放在初始化代码段init{}中。
class Animal constructor(name: String) { init { println("init中初始化代码") } }
…
次构造器
类也可以有二级构造函数,需要加前缀 constructor:
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
果类有主构造函数,每个次构造函数都要直接或间接通过另一个次构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this 关键字:
class Person(val name: String) {
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
…
抽象类
…
嵌套类
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
}
内部内
Kotlin 循环控制Kotlin 继承
Kotlin 类和对象
类定义
Kotlin 类可以包含:构造函数和初始化代码块、函数、属性、内部类、对象声明。
Kotlin 中使用关键字 class 声明类,后面紧跟类名:
class Runoob { // 类名为 Runoob
// 大括号内是类体构成
}
我们也可以定义一个空类:
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函数,因为它是只读的。
var allByDefault: Int? // 错误: 需要一个初始化语句, 默认实现了 getter 和 setter 方法
var initialized = 1 // 类型为 Int, 默认实现了 getter 和 setter
val simple: Int? // 类型为 Int ,默认实现 getter ,但必须在构造函数中初始化
val inferredType = 1 // 类型为 Int 类型,默认实现 getter
实例
以下实例定义了一个 Person 类,包含两个可变变量 lastName 和 no,lastName 修改了 getter 方法,no 修改了 setter 方法。
class Person {
var lastName: String = "zhang"
get() = field.toUpperCase() // 将变量赋值后转换为大写
set
var no: Int = 100
get() = field // 后端变量
set(value) {
if (value < 10) { // 如果传入的值小于 10 返回该值
field = value
} else {
field = -1 // 如果传入的值大于等于 10 返回 -1
}
}
var heiht: Float = 145.4f
private set
}
// 测试
fun main(args: Array<String>) {
var person: Person = Person()
person.lastName = "wang"
println("lastName:${person.lastName}")
person.no = 9
println("no:${person.no}")
person.no = 20
println("no:${person.no}")
}
console输出结果为:
lastName:WANG
no:9
no:-1
Kotlin 中类不能有字段。提供了 Backing Fields(后端变量) 机制,备用字段使用field关键字声明,field 关键词只能用于属性的访问器,如以上实例:
var no: Int = 100
get() = field // 后端变量
set(value) {
if (value < 10) { // 如果传入的值小于 10 返回该值
field = value
} else {
field = -1 // 如果传入的值大于等于 10 返回 -1
}
}
非空属性必须在定义的时候初始化,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")
}
}
注意:主构造器的参数可以在初始化代码段中使用,也可以在类主体n定义的属性初始化代码中使用。 一种简洁语法,可以通过主构造器来定义属性并初始化属性值(可以是var或val):
class People(val firstName: String, val lastName: String) {
//...
}
如果构造器有注解,或者有可见度修饰符,这时constructor关键字是必须的,注解和修饰符要放在它之前。
实例
创建一个 Runoob类,并通过构造函数传入网站名:
class Runoob constructor(name: String) { // 类名为 Runoob
// 大括号内是类体构成
var url: String = "http://www.runoob.com"
var country: String = "CN"
var siteName = name
init {
println("初始化网站名: ${name}")
}
fun printTest() {
println("我是类的函数")
}
}
fun main(args: Array<String>) {
val runoob = Runoob("菜鸟教程")
println(runoob.siteName)
println(runoob.url)
println(runoob.country)
runoob.printTest()
}
输出结果为:
初始化网站名: 菜鸟教程
菜鸟教程
http://www.runoob.com
CN
我是类的函数
次构造函数
类也可以有二级构造函数,需要加前缀 constructor:
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
如果类有主构造函数,每个次构造函数都要,或直接或间接通过另一个次构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this 关键字:
class Person(val name: String) {
constructor (name: String, age:Int) : this(name) {
// 初始化...
}
}
如果一个非抽象类没有声明构造函数(主构造函数或次构造函数),它会产生一个没有参数的构造函数。构造函数是 public 。如果你不想你的类有公共的构造函数,你就得声明一个空的主构造函数:
class DontCreateMe private constructor () {}
注意:在 JVM 虚拟机中,如果主构造函数的所有参数都有默认值,编译器会生成一个附加的无参的构造函数,这个构造函数会直接使用默认值。这使得 Kotlin 可以更简单的使用像 Jackson 或者 JPA 这样使用无参构造函数来创建类实例的库。
class Customer(val customerName: String = "")
实例
class Runoob constructor(name: String) { // 类名为 Runoob
// 大括号内是类体构成
var url: String = "http://www.runoob.com"
var country: String = "CN"
var siteName = name
init {
println("初始化网站名: ${name}")
}
// 次构造函数
constructor (name: String, alexa: Int) : this(name) {
println("Alexa 排名 $alexa")
}
fun printTest() {
println("我是类的函数")
}
}
fun main(args: Array<String>) {
val runoob = Runoob("菜鸟教程", 10000)
println(runoob.siteName)
println(runoob.url)
println(runoob.country)
runoob.printTest()
}
输出结果为:
初始化网站名: 菜鸟教程
Alexa 排名 10000
菜鸟教程
http://www.runoob.com
CN
我是类的函数
抽象类
抽象是面向对象编程的特征之一,类本身,或类中的部分成员,都可以声明为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: Int = 1
var v = "成员属性"
/**嵌套内部类**/
inner class Inner {
fun foo() = bar // 访问外部类成员
fun innerTest() {
var o = this@Outer //获取外部类的成员变量
println("内部类可以引用外部类的成员,例如:" + o.v)
}
}
}
fun main(args: Array<String>) {
val demo = Outer().Inner().foo()
println(demo) // 1
val demo2 = Outer().Inner().innerTest()
println(demo2) // 内部类可以引用外部类的成员,例如:成员属性
}
为了消除歧义,要访问来自外部作用域的 this,我们使用this@label,其中 @label 是一个 代指 this 来源的标签。
继承
在 Kotlin 中所有类都有一个共同的超类 Any,对于没有超类型声明的类它是默认超类:
class Example // 从 Any 隐式继承
Any 有三个方法:equals()、 hashCode() 与 toString()。因此,为所有 Kotlin 类都定义了这些方法。
默认情况下,Kotlin 类是最终(final)的——它们不能被继承。 要使一个类可继承,请用 open 关键字标记它:
open class Base // 该类开放继承
如需声明一个显式的超类型,请在类头中把超类型放到冒号之后:
open class Base(p: Int)
class Derived(p: Int) : Base(p)
如果派生类有一个主构造函数,其基类可以(并且必须)根据其参数在该主构造函数中初始化。
如果派生类没有主构造函数,那么每个次构造函数必须使用super 关键字初始化其基类型,或委托给另一个做到这点的构造函数。 请注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
覆盖方法
Kotlin对于具有显示修饰符open的成员方法,可以覆盖
open class Shape {
// 如果函数没有标注 open 如 fill(),那么子类中不允许定义相同签名的函数
open fun draw() {
println("这是继承对象里面的继承方法")
}
fun fill() {
println("这是继承对象里面的非继承方法")
}
}
class Circle : Shape() {
// draw() 函数上必须加上 override 修饰符。如果没写,编译器会报错。
override fun draw() {
println("我覆盖了父类的draw()方法")
}
}
fun main() {
var shape = Shape()
shape.draw()
shape.fill()
var circle = Circle()
circle.draw()
}
覆盖属性
属性与方法的覆盖机制相同
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
可以用var覆盖val属性,但是不能用val覆盖var属性。因为val只有一个get方法,可以再追加一个set方法。而var具有set方法,不可被覆盖。
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点
class Polygon : Shape {
override var vertexCount: Int = 0 // 以后可以设置为任何数
}
派生类初始化顺序
暂时不会
super超类调用
派生类中的代码可以使用super关键字调用其超类的属性和方法
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}
Drawing a rectangle
Filling the rectangle
black
在内部类访问外部类的超类,可以在内部类中用super@外部类来访问
```Kotlin
open class Person(val name: String) {
open fun sayName() {
println(“You name is $name”)
}
}
class Student(name: String) : Person(name) {
inner class Inner {
fun sayHello() {
[email protected]()
}
}
}
fun main() {
val student = Student(“Kotlin”)
student.Inner().sayHello()
}
```console
You name is Kotlin!
覆盖规则
如果一个类继承父类,且实现多个接口。父类和接口具有相同的方法,那么子类必须覆盖父类的方法,否则编译器会报错。
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()
}
}
fun main() {
val square = Square()
square.draw()
}
类的属性
属性声明
Kotlin中类的属性用var声明可读写变量,用val声明只读变量
class myClass{
// 简写
var readAndWarite = "可读写变量"
val readOnly = "只读变量"
}
fun main() {
val myClass = myClass()
// 调用属性直接用名称引用即可
println(myClass.readAndWarite)
println(myClass.readOnly)
}
属性的Getter和Setter
在类中声明一个属性的完整语法如下
var <属性名>[: <属性类型>] [= <属性初始值>]
[<getter方法>]
[<sette方法r>]
幕后字段(其实就是field标识符)
为什么会有幕后字段?
从属性的完整语法中可以看得出,和Java一样一个属性的定义实际上是调用了其getter和setter方法。public class Property{ private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
类似的,在Kotlin中,写一个属性的实质就是执行了属性的写访问器setter 。那么一个属性的默认的setter涨什么样子呢? 聪明的你可能一下就想到了,这还不简单,跟Java的 setXXX 方法差不多嘛(傲娇脸)。一下就写出来了,如下:
class Property { //错误的演示 var name = "" set(value) { this.name = value } }
不好意思,一运行就会报错,直接StackOverFlow了,内存溢出。
博客类属性幕后属性内存溢出报错
为什么呢?转换为Java代码看一下你就明白了,将Property类转为Java类:public final class Property { @NotNull private String name = ""; @NotNull public final String getName() { return this.name; } public final void setName(@NotNull String value) { Intrinsics.checkNotNullParameter(value, "value"); this.setName(value); } }
可以看到,setName方法又调用了setName,陷入了死循环,直到内存溢出,程序崩溃。在Kotlin中也一样,setter会陷入死循环。这就引出了Kotlin中的幕后字段
幕后字段是什么
千呼万唤始出来,什么是幕后字段? 没有一个确切的定义,在Kotlin中, 如果属性至少有一个settet或getter,那么Kotlin会自动提供幕后字段,用关键字field表示,幕后字段主要用于自定义getter和setter中,并且只能在getter 和setter中访问。幕后字段怎么用
回到上面的自定义setter例子中,怎么给属性赋值呢?答案是给幕后字段field赋值,如下:class Property { var name = "" get() = field set(value) { field = value } }
幕后属性
- 为什么会需要幕后属性
有时候有这种需求,我们希望一个属性:对外表现为只读,对内表现为可读可写,我们将这个属性称为幕后属性。 幕后属性语法
如:
class Property { private var _table: Map<String, Int>? = null public val table: Map<String, Int> get() { if (_table == null) { _table = HashMap() // 类型参数已推断出 } return _table ?: throw AssertionError("Set to null by another thread") } }
编译期常量
运行时常量(val):编译期不能确定它的值,代码中会保留变量对它的引用。
编译期常量(const val):编译期就知道值,并且会把代码中所有对它的引用都替换成它的值。
Kotlin 中 val常量 仅仅只是类似 Java 中的 final 常量而已,而 const val 常量,才是真正对应 Java 中的 final 常量。因为 Java 中的 final 常量是编译期常量。
延迟初始化属性与变量
- 为什么要延迟初始化
有的时候,我并不想声明一个类型可空的对象,而且我也没办法在对象一声明的时候就为它初始化,那么这时就需要用到Kotlin提供的延迟初始化。 - 延迟初始化属性语法
Kotlin中有两种延迟初始化的方式。一种是lateinit var,一种是by lazy。- lateinit var
lateinit var只能用来修饰类属性,不能用来修饰局部变量,基本数据类型
lateinit var的作用也比较简单,就是让编译期在检查时不要因为属性变量未被初始化而报错(我是好人)。Kotlin相信当开发者显式使用lateinit var 关键字的时候,他一定也会在后面某个合理的时机将该属性对象初始化的(然而,谁知道呢,也许他用完才想起还没初始化)。
- lateinit var
- 检测一个lateinit var是否已初始化
要检测一个 lateinit var 是否已经初始化过,请在 该属性的引用 blue上使用 .isInitialized:if (foo::bar.isInitialized) { println(foo.bar) }
接口
KOtlin的接口可包含抽象方法的声明和具体方法的实现。它可以有属性,但是必须为抽象的且不允许有初始值。接口不会保存属性的值,实现时必须重新定义属性的值。
第一个接口
interface MyInterface {
fun bar() // 未实现
fun foo(){ // 已实现
println("foo")
// 可选方法体
}
}
这样我们就完成了一个接口的声明,他的foo()已经有了默认实现,而bar()还没有。
实现接口
KOtlin对象可以实现多个接口,用逗号分隔。
Kotlin 必须实现接口中所有没有默认实现的方法
格式为
class 类名 : 接口1, 接口2,... {
// 实现接口的方法
}
上面接口的实现
class InterfaceImp:MyInterface {
// 这里不重写foo方法,因为它有默认实现
override fun bar() {
println("bar")
}
}
fun main(args: Array<String>) {
val obj = InterfaceImp()
obj.foo()
obj.bar()
}
接口的属性
接口中的属性只能是抽象的,不能有初始值。
接口不会保存属性的值,实现时必须重新定义属性的值。
// 接口
interface MyInterface {
var name:String // 抽象的,不能直接赋值
}
// 实现接口
class InterfaceImp:MyInterface {
override var name:String = "重写"
}
fun main(args: Array<String>) {
val obj = InterfaceImp()
println(obj.name)
}
实现多个接口时,同名方法的重写
日常 Kotlin 开发时,可能要求一个类实现多个接口,不同接口中有相同的方法名的问题。
Kotlin实现接口,重写接口时,默认不会调用接口的方法,需使用super关键字显示调用。
// 接口
interface A {
fun foo() { print("A") } // 已实现
fun bar() // 未实现,没有方法体,是抽象的
}
interface B {
fun foo() { print("B") } // 已实现
fun bar() { print("Bbar") } // 已实现
}
// 实现接口
class ABImp : A,B {
override fun foo() {
super<A>.foo()
super<B>.foo()
} // 重写
override fun bar() {
super<B>.bar()
} // 重写
}
fun main(args: Array<String>) {
val a = ABImp()
a.foo()
a.bar()
}
拓展
拓展就是在已经定义好的类中,添加新的属性和方法
,且不需要继承等。拓展不会对原类造成任何影响。
拓展函数定义格式
拓展函数的定义格式为
fun 被拓展类名.函数名(参数列表) {
// 方法体
}
一个简单的拓展函数:
拓展函数中this关键字表示调用拓展函数的对象
。在这个例子中也就是a。
拓展一个空对象
伴生对象(内部类)
伴生对象是在类的内部定义的类,拓展也可以为伴生对象添加属性和方法。
简单的伴生对象拓展:
拓展属性
拓展属性格式为:
val 被拓展类名.属性名:属性类型
get() {
// 返回属性值
}
set(value) {
// 设置属性值
}
一个简单的拓展属性
其它注意点
若扩展函数和成员函数一致,则使用该函数时,会优先使用成员函数。
数据类
数据类指只保存数据的类
,不保存任何其他逻辑。
语法格式
该类以data
关键字开头,并且需要定义一个构造函数,编译器会自动从主构造函数中根据声明的属性推导出相关函数。
data class <类名> <(主构造函数参数列表)> [:继承类和实现接口] [(/*类体*/)]
主构造函数参数列表要求 至少有一个
且必须使用var
或者val
修饰,否则编译器会报错.
栗如:
数据类功能
- 自动声明与构造函数入参同名的属性字段
- 自动为每个属性实现set/get方法
- 数据类自带
equals()
和hashCode()
方法,用来判断两个数据类对象是否相等 - 数据类自带
copy()
方法,用来创建复制对象 - 数据类自带
toString()
方法,用来返回对象的字符串表示如果上面提到的函数在数据类中已经被明确定义,或从超类中继承而来则不再会生成。
数据类声明标准
为了保证生成代码的一致性以及有意义以,数据类需要遵守以下条件:
- 主构造函数至少包含一个参数
- 所有的主构造函数的参数必须标识为val或var
- 数据类不可以声明为抽象
abstract
、开放open
、密封sealed
、内部inner
- 数据类不能继承其他类,但可实现接口。
数据类Copy
可使用复制函数复制对象并修改部分属性
解析函数
解析函数能够将对象的属性提取出来并赋给一个值
密封类
密封类用于表示受限制的类层次结构,类中的值只能是受限制类中的类型,不允许拥有其它类型
。它和枚举的区别在于枚举中的常量是作为单个实例存在的,而密封类的子类是可以包含多个状态的实例。
通俗来说,数据类就是受限制类的集合,里面的受限制类都继承于密封类。不同于其它常见类,数据类不能被所在的文件外继承
,但是密封类的子类的拓展可以在程序任何地方继承的。
声明密封类
包装类由sealed
标识符标识。
Kotlin的所有子类必须在同一个文件声明(Kotlin1.1前必须嵌套在密封类栗)。
密封的类本身是抽象的,它不能直接实例化,可以有抽象成员。密封类不允许有非私有构造函数(它们的构造函数默认是私有的)。
密封类的好处
当你在 when
表达式中使用密封类时,使用密封类的主要好处就发挥了作用。如果可以验证语句是否涵盖所有情况,则不需要向语句中添加 else 子句。但是,只有当你使用 when
作为表达式(使用结果)而不是作为语句时,这个方法才有效。
泛型
Kotlin 中的类可以有类型参数,与 Java 类似:
声明泛型
声明泛型类
class Box<T>(t: T) {
var value = t
}
声明泛型函数
Kotlin 泛型函数的声明与 Java 相同,类型参数要放在函数名的前面
fun <T> boxIn(value: T) = Box(value)
// 以下都是合法语句
val box4 = boxIn<Int>(1)
val box5 = boxIn(1) // 编译器会进行类型推断
实例化泛型类
val box<Int> = Box<Int>(1)
如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径, 就可以省略类型参数:
val box = Box(1) // 1 具有类型 Int,所以编译器推算出它是 Box<Int>
向泛型类Box传入整形和字符串型示例:
实例化泛型函数
在调用泛型函数时,如果可以推断出类型参数,可以省略泛型参数
以下范例创建了泛型函数 doPrintln,函数根据传入的不同类型做相应处理
泛型约束
泛型约束可以约束某一个参数允许的类型
。
没看懂,待研究
。。。
枚举类
Kotlin枚举类和Java类似,枚举类用enum
标识符标识。
enum class Color{
RED,BLACK,BLUE,GREEN,WHITE
}
初始化枚举常量
枚举常量可以有初始化值,初始化值可以是数字、字符串或者其他枚举常量。
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
集合
Map 相关操作
Map类似于JSON,下面是JSON和Map的对比
{ "name":"微博" , "url":"www.weibo.com" }
mapOf("name" to "微博", "url" to "www.weibo.com")
取键与值
在Map中取值有很多方法,:
- get()
val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print(numbersMap.get("name"))
- []
val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print(numbersMap["name"])
- getValue()
getValue找不到值会抛出异常val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print(numbersMap.getValue("name"))
- getOrElse()
该方法若是找不到对应的键值,则返回Lambda的返回值.接收两个参数:- 参数一: 需要被查找的键名
- 参数二: 一个Lambda表达式
输出val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print(numbersMap.getOrElse("name"){})
微博
因为传入了不存在的键,所以输出val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print(numbersMap.getOrElse("不存在的键"){"不存在则返回这个"})
不存在则返回这个
- getOrDefault()
该方法若是找不到键就直接返回默认值,接收两个参数:
- 参数一: 需要查找的键
- 参数二: 默认值
val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print(numbersMap.getOrDefault("name","默认值"))
若要对所有key进行操作则可以使用属性
keys
,若要对所有value进行操作则可以使用属性values
.val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") print("所有keys为:${numbersMap.keys}") print("所有values为:${numbersMap.values}")
输出结果为
所有keys为:[name, url]所有values为:[微博, www.weibo.com]
过滤
Map可以使用filter()来过滤map或其它集合,官网文档的说明是:基本的过滤函数是 filter()。当使用一个谓词
来调用时,filter() 返回与其匹配的集合元素。对于 List 和 Set,过滤结果都是一个 List,对 Map 来说结果还是一个 Map。
a’ba’a’ba,什么谓词,根本看不懂好吧!!!,接下来我就按自己的理解描述一遍.
过滤就是集合中的每个元素
依次和一个表达式或函数比较,若比较结果为true
则将该元素添加进一个新Map,最后将新Map
返回.
- 下面是一个例子
filter()只能检查元素的值,若要使用元素在集合中的位置,则使用val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11) // 这里lambda表达式接收的参数分别是key和value,若key的最后一个字符是1则返回,最后会返回一个新的map val newMap = numbersMap.filter { (key,value) -> key.endsWith("1") } print(newMap)
filterIndexed()
. - filterIndexed()
filterIndexed()在回调函数中传入的参数是元素的索引和元素的值,Map无此方法,因为map的索引值就是键.示例代码如下:
输出结果如下val numbersList = listOf("one", "two", "three", "four") val newList = numbersList.filterIndexed{ index, value -> // 这里的条件为索引值不为0且长度小于5 (index != 0) && (value.length < 5) } print(newList)
[two, four]
- 前面都是结果为true的过滤集合,欸!我就是想使用false,那么可以使用
filterNot()
方法来实现.val numbersList = listOf("one", "two", "three", "four") val newList = numbersList.filterNot { it.length <= 3 } print(newList)
- 还未进行描述的参考这里
plus 与 minus 操作
进行集合plus
(+
)运算的时候:
- 若Map与Pair
无
相同键,则Pair的键值对拼接在Map后面val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3) println(numbersMap + Pair("four", 4)) // 输出:{one=1, two=2, three=3, four=4}
- 若Map与Pair
有
相同键,则Pair的键的值替换Map的值.val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3) println(numbersMap + Pair("one", 10)) // 输出:{one=10, two=2, three=3}
进行集合minus
(-
)运算的时候,减数可以是单个键
,或键的集合
,最后返回单个键或集合减去的集合
添加或删除Map操作
Map的操作规则:
- 添加键值对
添加单个键值对使用put()方法val map = mutableMapOf("one" to 1, "two" to 2) map.put("three",3) print(map) // 输出结果为: {one=1, two=2, three=3}
- 添加键值对集合
添加多个键值对使用putAll
,它的参数可以是Map
或Pair
,Iterable
、Sequence
或Array
val map = mutableMapOf("one" to 1, "two" to 2) map.putAll(mapOf("three" to 3, "four" to 4)) print(map) // 输出结果为: {one=1, two=2, three=3, four=4}
- plusAssign (+=) 操作符。
val map = mutableMapOf("one" to 1, "two" to 2) map += mapOf("three" to 3, "four" to 4 ) print(map) // 输出结果为: {one=1, two=2, three=3, four=4}
- [] 操作符为 set() 的别名。
val map = mutableMapOf("one" to 1, "two" to 2) map["three"] = 3 print(map) // 输出结果为: {one=1, two=2, three=3}
如果给定键已存在于 Map 中,则 put() 与 putAll() 都将覆盖值。
删除键值对
- remove() 函数,参数可以是
单个键
或整个键值对
,当使用键值对时只有键和值都匹配才会删除val map = mutableMapOf("one" to 1, "two" to 2) map.remove("one") print(map)
- 通过键或值删除键值对
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3, "threeAgain" to 3) numbersMap.keys.remove("one") println(numbersMap) numbersMap.values.remove(3) println(numbersMap) // 输出结果为 {two=2, three=3, threeAgain=3} {two=2, threeAgain=3}
- minusAssign (-=) 操作符
val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3) numbersMap -= "two" println(numbersMap) numbersMap -= "five" //不会删除任何条目 println(numbersMap) // 输出结果为{one=1, three=3} {one=1, three=3}
Kotlin进阶语法
Kotlin语言反引号``函数
在Kotlin中,可以用 ``符号来声明一个非合法的函数名,这样的函数名就可以用于函数调用。
另外可以有效的解决调用Java中函数名是Kotlin关键字的问题
。
fun `一个测试函数`(){
println("成功调用")
}
fun main() {
`一个测试函数`()
}
标准函数和静态方法
标准函数let,with,run,apply,also
标准函数在被编译时不会产生多余的函数,而是会被直接拆分为语句进行运行.
run
第一种用法可以看成js里面的匿名函数,且自动返回最后一行
fun main() {
print(run{2+1})
/*
* 相当于js中的
* var a = function(){return 2+1}
* a()
* /
}
第二种用法可以简化一个对象的频繁调用
class `计算器`(){
var value1 = 0
var value2 = 0
fun `加法`() = value1+value2
fun `乘法`() = value1*value2
}
fun main() {
val `计算器` = 计算器()
var `结果` = 计算器.run{
// 相当于这里的ithis就是计算器,而因为是this所以就没this
value1 = 1
value2 = 2
加法()
}
print(结果)
// 如果不使用run方法,则代码相对而言会变得复杂
计算器.value1 = 1
计算器.value2 = 2
val `结果2` = 计算器.加法()
print(结果2)
}
with
with的用法和run的第二个版本非常接近,在本质上和run的第二版本是一样的.
class `计算器`(){
var value1 = 0
var value2 = 0
fun `加法`() = value1+value2
fun `乘法`() = value1*value2
}
fun main() {
val `计算器` = 计算器()
// 仅仅需要修改此处即可
var `结果` = with(计算器){
value1 = 1
value2 = 2
加法()
}
print(结果)
}
with的另一种用法是用于配置对象的属性,而对象的方法则在别处调用
class `计算器`(){
var value1 = 0
var value2 = 0
fun `加法`() = value1+value2
fun `乘法`() = value1*value2
}
fun main() {
val `计算器` = 计算器()
// 这里仅仅用于配置属性
with(计算器){
value1 = 1
value2 = 2
}
print(计算器.加法())
}
apply
与run和with不同,apply最终返回的不是最后一行代码,它返回的是传入的类.也就是说你传入了计算机类,返回的还是计算机类
class `计算器`(){
var value1 = 0
var value2 = 0
fun `加法`() = value1+value2
fun `乘法`() = value1*value2
}
fun main() {
//开始
val `结果` = 计算器().apply{
value1 = 1
value2 = 2
}
print(结果)
// 结果为计算器@37f8bb67,返回的是计算机类
// 结束
}
let
run会返回最后一行,而let则是run的替代,它不会返回
fun main() {
// 这是一个可null的字符串列表
val `字符串列表` = listOf<String?>("我","是",null,"学校")
for(str in 字符串列表){
// 在这里同样可以使用run但是let不会返回值且写的代码更少
str?.let{print(it)}
}
}
also
also则是apply的替代
is,!is,as,as?运算符
is 与 !is 操作符
kotlin中API提供的 is 运算符类似于Java中的 instanceof 关键字的用法.我们可以在运行时通过使用 is
操作符或其否定形式 !is
来检测对象是否符合给定类型
:
fun main() {
print(1 is Int)
print(1 is Int)
// true
// false
}
is具有自动类型转换功能
as 与 !as 操作符
as运算符用于执行引用类型的显式类型转换
。如果类型不兼容,使用as?
运算符就会返回值null
.Kotlin中父类是禁止转换为子类
型的。
open class `水果`
class `苹果`(price:Int) : 水果()
fun main() {
val `苹果` = 苹果(23)
val `水果` = 水果()
// 子类可以转为父类,所以可以成功
苹果 as 水果
// 但父类不能转为子类,所以会报错
// 水果 as 苹果
// 但是使用as?不会报错而是会返回null\
水果 as? 苹果
}
协程
概述
- 什么是协程
协程是线程的另外一种形态.相较于线程来讲,协程具有占用空间小,线程切换速度快,非阻塞式,消除回调地狱等特点.协程是一种异步方法,他会在后台开一个线程执行任务,在执行完毕后会自动回调给主线程. - 什么时候使用协程
在有大量IO
操作业务的情况下,我们采用协程替换线程,可以到达很好的效果,一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。
在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。
在延迟执行或循环执行的函数时使用线程.
协程只有和异步IO结合起来才能发挥出最大的威力。
协程的简单使用
- 延迟1秒输出World!,代码如下:
```Kotlin
import kotlinx.coroutines.*
fun main() {
// 在后台启动一个新协程,并继续执行之后的代码
GlobalScope.launch {
// 非阻塞式地延迟一秒
delay(1000L)
// 延迟结束后打印
println(“World!”)
}
//主线程继续执行,不受协程 delay 所影响
println(“Hello,”)
// 主线程阻塞式睡眠2秒,以此来保证JVM存活
Thread.sleep(2000L)
}
输出结果:
```log
Hello,
World!
本质上,协程可以称为轻量级线程
。协程在 CoroutineScope (协程作用域)的上下文
中通过 launch、async 等协程构造器
(coroutine builder)来启动。
在上面的例子中,在 GlobalScope ,即全局作用域内启动了一个新的协程,这意味着该协程的生命周期只受整个应用程序的生命周期的限制,即只要整个应用程序还在运行中,只要协程的任务还未结束,该协程就可以一直运行delay()
是一个挂起函数
(suspending function),挂起函数只能由协程或者其它挂起函数进行调度。挂起函数不会阻塞线程,而是会将协程挂起,在特定的时候才再继续运行
- 等价线程代码
可以将以上的协程改写为常用的thread
形式,可以获得相同的结果
```kotlin
import kotlin.concurrent.thread
fun main() {
thread {
Thread.sleep(1000L)
println(“World!”)
}
println(“Hello,”)
Thread.sleep(2000L)
}
协程和子线程是运行在线程上面的,相较于子线程,一个线程能运行更多的协程(可以是成千上万个).
线程的调度是由系统决定的,但协程可以由`开发者指定`.
### 分清阻塞代码和非阻塞代码
在上面的代码中,使用了阻塞代码`Thread.sleep()`和非阻塞代码`delay()`.在实际开发中很有可能会忘记这两个函数谁是阻塞,谁是非阻塞.这时候就可以使用`runBlocking`来明确都为阻塞线程.
```Kotlin
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
// runBlocking用于阻塞线程,该函数内代码执行完毕才会释放
runBlocking {
delay(2000L)
}
}
也可以直接将整合函数作为阻塞线程代码如下:
import kotlinx.coroutines.*
fun main() {
test()
println("HELLO")
}
// 调用该方法会阻塞线程
fun test() = runBlocking {
delay(1000L)
println("World!")
}
输出:
World!
HELLO
等待作业协程函数
有时候并不希望协程马上被执行,这时就可以将它赋值给变量然后调用join()启动协程
import kotlinx.coroutines.*
fun main() {
val a = test()
println("HELLO")
// 因为只能在协同程序或其他挂起函数调用挂起函数“join”
runBlocking {
a.join()
}
}
// 定义一个协程函数
fun test() = GlobalScope.launch {
delay(1000L)
println("World!")
}
结构化并发
什么是结构化并发,简单来说就是带有结构和层级的并发
。
在Java中我们知道想并发编程就使用多线程,但是线程和线程之间却是没有父子关系的,而协程可以做一样的并发编程,这些协程却是有父子关系的。
- 说起来可能不好理解,我们直接看个例子:
```Kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob: Job
// 初始化
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
// 父job嵌套3个子job
parentJob = launch {
// 子job会被初始化但不会被运行
job1 = launch {
delay(1000L)
}
job2 = launch {
delay(3000L)
}
job3 = launch {
delay(5000L)
}
}
delay(500L)
// 上面已经初始化,所以job不为null
parentJob.children.forEachIndexed { index, job ->
when (index) {
0 -> println("job1 === job is ${job1 === job}")
1 -> println("job2 === job is ${job2 === job}")
2 -> println("job3 === job is ${job3 === job}")
}
}
// 这里会运行job,这里会挂起大约5秒钟
parentJob.join()
}
取消结构化,减少内存泄漏的风险。
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val parentJob: Job
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
parentJob = launch {
job1 = launch {
delay(1000L)
}
job2 = launch {
delay(3000L)
}
job3 = launch {
delay(5000L)
}
}
delay(500L)
parentJob.children.forEachIndexed { index, job ->
when (index) {
0 -> println("job1 === job is ${job1 === job}")
1 -> println("job2 === job is ${job2 === job}")
2 -> println("job3 === job is ${job3 === job}")
}
}
parentJob.cancel() // 变化在这里
}
作用域构建器
除了官方提供的协程构造器,还能使用coroutineScope
来声明自己的作用域,这个作用域直到启动的所有子协程都结束后才结束.
- 代码演示如下:
```Kotlin
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println(“Task from runBlocking”)
}
coroutineScope { // Creates a coroutine scope
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // This line will be printed before the nested launch
}
println("Coroutine scope is over") // This line is not printed until the nested launch completes
}
输出结果如下:
```log
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
RunBlocking
和 coroutineScope
看起来很像,因为它们都需要等待其内部所有相同作用域的子协程结束后才会结束自己。区别在于 runBlocking 方法会阻塞
当前线程,而 coroutineScope 只是挂起并释放底层线程以供其它协程使用,是非阻塞。runBlocking 是一个普通函数
,而 coroutineScope 是一个挂起函数
调用协程函数
将launch 内部的代码块重写为一个独立的函数,需要将之声明为挂起函数。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// 这个方法需要加上suspend关键字
suspend fun doWorld() {
delay(1000L)
println("World!")
}
使用 async 并发
如果 doSomethingUsefulOne() 和 doSomethingUsefulTwo() 这两个函数之间没有依赖关系,并且我们希望通过同时执行这两个操作(并发)以便更快地得到答案,此时就需要用到 async
了
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//sampleStart
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
//sampleEnd
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
需要注意,协程的并发总是显式的
惰性启动 async
可选的,可以将 async
的 start 参数设置为 CoroutineStart.lazy
使其变为懒加载模式。在这种模式下,只有在主动调用 Deferred 的 await()
或者 start()
方法时才会启动协程。运行以下示例:
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
//sampleStart
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// some computation
one.start() // start the first one
two.start() // start the second one
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
//sampleEnd
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}