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
1 | fun main(args: Array<String>) { |

基础语法
Kotlin文件以.kt结尾.
包
如同Java,代码的开头一般为包的声明。但Kotlin源文件不需要相匹配的目录和包,源文件可以放在任何文件目录。以上例中 test() 的全名是 com.runoob.main.test、Runoob 的全名是 com.runoob.main.Runoob。默认为default包。
1 | package com.runoob.main |
注:
以下包会默认导入到每个Kotlin文件中
- kotlin.*
- kotlin.annotation.*
- kotlin.collections.*
- kotlin.comparisons.*
- kotlin.io.*
- kotlin.ranges.*
- kotlin.sequences.*
- kotlin.text.*
函数定义
方式1
函数定义使用关键字fun(function缩写),参数格式为:参数名:类型
1 | fun sum(a:Int,b:Int):Int{ |
当表达式作为函数体,返回类型自动判断:
1 | fun sum(a: Int, b: Int) = a + b |
无返回值的函数(类似void)
1 | fun printSum(a: Int, b: Int): Unit { //Unit可不写 |
方式2
除了传统的函数定义方式,Kotlin还支持类似js的隐式函数定义。
函数定义格式为:
1 | var 函数名:()->返回类型 |
已经定义好了函数,那么怎么实现它呢?
1 | var myFun:()->Unit |
与方式1不同的是,该定义方式会将代码块最后一行作为值return,如下。
1 | var myFun:()->String |
既然已经了解它的语法,那么我们再来将它简化一下。
1 | var myFun:()->String = { |
写了这么多,是不是会发现怎么没有传参,现在就说说。
在隐式函数中,参数直接写入类型即可
方式3
具有自动推断返回类型的匿名函数
1 | val 函数名 ={ 形参1:类型, 形参2:类型 -> 表达式,... _> 表达式 } |
当返回类型可能性为两种及以上,就会返回Any类型
1 | var myFun = { x: Int, y: Int -> if (x > y) x else 'y' } |
it关键字
在函数(不管是不是隐式函数)只有一个形参时,函数会自带一个it关键字来表示该形参。
不定长参数(类似String[] args)
函数的不定长参数可以用vararg关键字来定义:
1 | fun vars(vararg v:Int){ |
函数默认参数
Kotlin允许和python一样的默认参数,可以让函数更加简洁:
1 | fun sum(a:Int,b:Int=0):Int{ |
函数具名参数
和Python一样,在函数参数过长时,可以使用具名参数来选择参数:
1 | fun login(username:String,password:String,phonenumber:String){} |
lambda匿名函数
1 | // 测试 |
内联函数
在Kotlin将匿名函数转为字节码过成中,匿名函数会被拆分成无数小对象,于是会参数多次调用造成性能浪费。
于是,使用inline修饰调用匿名函数的函数即可将匿名函数转为一个宏对象(相当于C的#define)。
示例如下,不要在意套娃,知道意思即可。
回调函数callback
Kotlin中的回调函数与javacript基本一直
1 | // Kotlin回调函数 |
常量和变量
变量定义:var关键字
1 | var <标识符> : <类型> = <初始化值> |
常量定义:val关键字
1 | val <标识符> : <类型> = <初始化值> |
自定义数据类型
虽然Kotlin支持自动推导类型,但是也可以自己指定类型
1 | val a: Int = 1 //指定类型为Int |
注释
1 | // 多行注释 |
字符串模板
$ 表示一个变量名或者变量值
1 | var a = 1 |
$变量名 表示该变量的值
${表达式} 表示该表达式的返回值
NUll检查机制
Kotlin的空安全设计对于声明可为空的参数,在使用时要进行空判断处理,有两种处理方式,字段后加!!像Java一样抛出空异常,另一种字段后加?可不做处理返回null或配合?:做空判断处理
1 | //表示age可为空 |
如果函数的返回值可能为空时,应该明确的用**?**标记其可能返回null
1 | fun parseInt(str: String): Int? { |
综上所述,当大量需要判断为null时会造成大量的代码冗余,当函数内使用全局变量时无法使用?进行判空,此时就可以使用let()函数,let可以在所有对象调用.
1 | val food: String? = "meat" |
类型检测及自动类型转换
我们可以使用is关键字来判断某个实例是否是某个类型。
1 | val a=1 |
1 | true |
区间(range表达式)
Kotlin提供了一个区间操作符**..**,可以很方便的表示一个区间
1 | for (i in 1..4) print(i) // 输出“1234” |
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只有封装的数字类型,没有原始的数字类型,所以比较两个数字时,三个**===表示比较对象地址,两个==**表示比较值。
类型转换
由于数据类型不同的位宽,较小的类型不能隐式转换为较大的类型,所以需要显式转换。
1 | val b: Byte = 1 // OK, 字面值是静态检测的 |
可以使用toInt()方法进行强转
1 | val b: Byte = 1 // OK, 字面值是静态检测的 |
相应的,每种数据类型都有转换为其它类型的方法,如下:
1 | toByte(): Byte |
在一些情况下,自动类型转换可以被允许,如下:
1 | val l = 1L + 3 // Long + Int => Long |
位操作符
相对于Int和Long,可以使用:
1 | shl(bits) – 左移位 (Java’s <<) |
字符
在Kotlin中,字符是由单双引号**’’** 括起来的,可以使用\转义字符。
字符可以显式toInt()转为Int
布尔
Boolean:true,false
布尔运算:||,&&,!
数组
数组是由Array实现。
创建数组的方式:
- arrayOf()
1 | //[1,2,3] |
- 工厂函数
1 | //[0,2,4] |
- 还有ByteArray, ShortArray, IntArray,用来表示各个类型的数组,省去了装箱操作,因此效率更高,其用法同Array一样
字符串
和Java一样,Kotlin中字符串也是由String类实现的。用[]可以很方便的获取某一个字符,也可以用for循环遍历字符串。
1 | for (c in str) { |
Kotlin支持用三个”””括起来的字符串
1 | val text = """ |
1 |
|
使用trimMargin()方法可以去掉多余的空格,如下:
1 | val text = """ |
条件控制
if表达式
if表达式和其它语言大致相同,就不多赘述
1 | // 传统用法 |
在Kotlin中可以将if表达式的结果赋值给一个变量,因此,Kotlin不支持三元操作符(? :)
1 | // 作为表达式 |
when表达式
when为条件分支,类似其它语言的switch.
1 | when(1){ |
与switch不同的是,如果有很多分支需要用相同的方式处理,可以把多个分支用**,**分割,放在一起。
1 | when (x) { |
也可以检测一个值在(in)或者不在(!in)一个区间或者集合中:
1 | when (x) { |
也可以使用is或者!is来检测特定的类型(注: 由于智能转换,你可以访问该类型的方法和属性而无需 任何额外的检测。)
1 | when(x) { |
如果不提供参数,甚至可以用来取代if-else语句,如下:
1 | var age = 8 |
循环控制
for循环
Kotlin的for循环类似于python的for循环.
1 | // 直接循环出对象 |
while与do-while循环
while与其他语言一致
1 | while( 布尔表达式 ) { |
do-while循环也与其他语言一致
1 | var a = 1 |
返回和跳转
在循环中 Kotlin 支持传统的 break 和 continue 操作符
1 | for (i in 1..10) { |
Kotlin中的label标签
label标签的格式为:标签名@ 表达式。例如:
myLabel@ for…
1 | // 不带label的语句 |
使用label可以指定一个语句块的返回点,这个返回点可以在任何地方使用,而不仅仅是在语句块的最后。并且可以指定返回给谁。
1 | 其实我没dong |
类和对象
类
类的定义
Kotlin类可以包含:构造函数和初始化代码块、函数、属性、内部类、对象声明。
定义类用关键字class声明,后面紧跟类名
1 | // 定义动物类 |
重点: 实体类定义的语法糖
1 | // 此语法糖会在test内部生成a,b,c变量 |
类的属性
属性的定义
定义属性用var或者val关键字,格式为
1 | class Animal { |
我们class可以像普通函数那样,使用构造函数来创建实列
1 | val animal = Animal() // 在Kotlin中没有new关键字 |
属性的get和set方法,可以通过get()和set()来访问和设置属性值。他俩都是可选的。val不允许设置set方法,因为它是只读的。
在get和set方法中,field变量是一个可见的变量,它是一个指向属性的引用。
Kotlin属性声明的完整格式如下:
1 | var 属性名称: 类型 |
设置get和set实例
1 | class Animal { |
非空属性必须在定义的时候初始化,kotlin提供了一种可以延迟初始化的方案,使用 lateinit 关键字描述属性(我不会)
主构造器
主构造器中不能包含任何代码,初始化代码可以放在初始化代码段init{}中。
1 | class Animal constructor(name: String) { |
…
次构造器
类也可以有二级构造函数,需要加前缀 constructor:
1 | class Person { |
果类有主构造函数,每个次构造函数都要直接或间接通过另一个次构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this 关键字:
1 | class Person(val name: String) { |
…
抽象类
…
嵌套类
1 | class Outer { // 外部类 |
内部内
Kotlin 循环控制Kotlin 继承
Kotlin 类和对象
类定义
Kotlin 类可以包含:构造函数和初始化代码块、函数、属性、内部类、对象声明。
Kotlin 中使用关键字 class 声明类,后面紧跟类名:
1 | class Runoob { // 类名为 Runoob |
我们也可以定义一个空类:
1 | class Empty |
类的属性
属性定义
类的属性可以用关键字 var 声明为可变的,否则使用只读关键字 val 声明为不可变。
1 | class Runoob { |
我们可以像使用普通函数那样使用构造函数创建类实例:
1 | val site = Runoob() // Kotlin 中没有 new 关键字 |
要使用一个属性,只要用名称引用它即可
1 | site.name // 使用 . 号来引用 |
Kotlin 中的类可以有一个 主构造器,以及一个或多个次构造器,主构造器是类头部的一部分,位于类名称之后:
1 | class Person constructor(firstName: String) {} |
如果主构造器没有任何注解,也没有任何可见度修饰符,那么constructor关键字可以省略。
1 | class Person(firstName: String) { |
getter 和 setter
属性声明的完整语法:
1 | var <propertyName>[: <PropertyType>] [= <property_initializer>] |
getter 和 setter 都是可选
如果属性类型可以从初始化语句或者类的成员函数中推断出来,那就可以省去类型,val不允许设置setter函数,因为它是只读的。
1 | var allByDefault: Int? // 错误: 需要一个初始化语句, 默认实现了 getter 和 setter 方法 |
实例
以下实例定义了一个 Person 类,包含两个可变变量 lastName 和 no,lastName 修改了 getter 方法,no 修改了 setter 方法。
1 | class Person { |
1 | console输出结果为: |
Kotlin 中类不能有字段。提供了 Backing Fields(后端变量) 机制,备用字段使用field关键字声明,field 关键词只能用于属性的访问器,如以上实例:
1 | var no: Int = 100 |
非空属性必须在定义的时候初始化,kotlin提供了一种可以延迟初始化的方案,使用 lateinit 关键字描述属性:
1 | public class MyTest { |
主构造器
主构造器中不能包含任何代码,初始化代码可以放在初始化代码段中,初始化代码段使用 init 关键字作为前缀。
1 | class Person constructor(firstName: String) { |
注意:主构造器的参数可以在初始化代码段中使用,也可以在类主体n定义的属性初始化代码中使用。 一种简洁语法,可以通过主构造器来定义属性并初始化属性值(可以是var或val):
1 | class People(val firstName: String, val lastName: String) { |
如果构造器有注解,或者有可见度修饰符,这时constructor关键字是必须的,注解和修饰符要放在它之前。
实例
创建一个 Runoob类,并通过构造函数传入网站名:
1 | class Runoob constructor(name: String) { // 类名为 Runoob |
输出结果为:
1 | 初始化网站名: 菜鸟教程 |
次构造函数
类也可以有二级构造函数,需要加前缀 constructor:
1 | class Person { |
如果类有主构造函数,每个次构造函数都要,或直接或间接通过另一个次构造函数代理主构造函数。在同一个类中代理另一个构造函数使用 this 关键字:
1 | class Person(val name: String) { |
如果一个非抽象类没有声明构造函数(主构造函数或次构造函数),它会产生一个没有参数的构造函数。构造函数是 public 。如果你不想你的类有公共的构造函数,你就得声明一个空的主构造函数:
1 | class DontCreateMe private constructor () {} |
注意:在 JVM 虚拟机中,如果主构造函数的所有参数都有默认值,编译器会生成一个附加的无参的构造函数,这个构造函数会直接使用默认值。这使得 Kotlin 可以更简单的使用像 Jackson 或者 JPA 这样使用无参构造函数来创建类实例的库。
1 | class Customer(val customerName: String = "") |
实例
1 | class Runoob constructor(name: String) { // 类名为 Runoob |
1 | 输出结果为: |
抽象类
抽象是面向对象编程的特征之一,类本身,或类中的部分成员,都可以声明为abstract的。抽象成员在类中不存在具体的实现。
注意:无需对抽象类或抽象成员标注open注解。
1 | open class Base { |
嵌套类
我们可以把类嵌套在其他类中,看以下实例:
1 | class Outer { // 外部类 |
内部类
内部类使用 inner 关键字来表示。
内部类会带有一个对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。
1 | class Outer { |
为了消除歧义,要访问来自外部作用域的 this,我们使用this@label,其中 @label 是一个 代指 this 来源的标签。
继承
在 Kotlin 中所有类都有一个共同的超类 Any,对于没有超类型声明的类它是默认超类:
1 | class Example // 从 Any 隐式继承 |
Any 有三个方法:equals()、 hashCode() 与 toString()。因此,为所有 Kotlin 类都定义了这些方法。
默认情况下,Kotlin 类是最终(final)的——它们不能被继承。 要使一个类可继承,请用 open 关键字标记它:
1 | open class Base // 该类开放继承 |
如需声明一个显式的超类型,请在类头中把超类型放到冒号之后:
1 | open class Base(p: Int) |
如果派生类有一个主构造函数,其基类可以(并且必须)根据其参数在该主构造函数中初始化。
如果派生类没有主构造函数,那么每个次构造函数必须使用super 关键字初始化其基类型,或委托给另一个做到这点的构造函数。 请注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:
1 | class MyView : View { |
覆盖方法
Kotlin对于具有显示修饰符open的成员方法,可以覆盖
1 | open class Shape { |
覆盖属性
属性与方法的覆盖机制相同
1 | open class Shape { |
可以用var覆盖val属性,但是不能用val覆盖var属性。因为val只有一个get方法,可以再追加一个set方法。而var具有set方法,不可被覆盖。
1 | interface Shape { |
派生类初始化顺序
暂时不会
super超类调用
派生类中的代码可以使用super关键字调用其超类的属性和方法
1 | open class Rectangle { |
1 | Drawing a rectangle |

在内部类访问外部类的超类,可以在内部类中用super@外部类来访问
1 | open class Person(val name: String) { |
1 | You name is Kotlin! |

覆盖规则
如果一个类继承父类,且实现多个接口。父类和接口具有相同的方法,那么子类必须覆盖父类的方法,否则编译器会报错。
1 | open class Rectangle { |
类的属性
属性声明
Kotlin中类的属性用var声明可读写变量,用val声明只读变量
1 | class myClass{ |
属性的Getter和Setter
在类中声明一个属性的完整语法如下
1 | var <属性名>[: <属性类型>] [= <属性初始值>] |
幕后字段(其实就是field标识符)
为什么会有幕后字段?
从属性的完整语法中可以看得出,和Java一样一个属性的定义实际上是调用了其getter和setter方法。1
2
3
4
5
6
7
8
9
10
11public class Property{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}类似的,在Kotlin中,写一个属性的实质就是执行了属性的写访问器setter 。那么一个属性的默认的setter涨什么样子呢? 聪明的你可能一下就想到了,这还不简单,跟Java的 setXXX 方法差不多嘛(傲娇脸)。一下就写出来了,如下:
1
2
3
4
5
6
7class Property {
//错误的演示
var name = ""
set(value) {
this.name = value
}
}不好意思,一运行就会报错,直接StackOverFlow了,内存溢出。

博客类属性幕后属性内存溢出报错
为什么呢?转换为Java代码看一下你就明白了,将Property类转为Java类:1
2
3
4
5
6
7
8
9
10
11
12
13
14public final class Property {
private String name = "";
public final String getName() {
return this.name;
}
public final void setName( 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赋值,如下:1
2
3
4
5
6
7class Property {
var name = ""
get() = field
set(value) {
field = value
}
}
幕后属性
为什么会需要幕后属性
有时候有这种需求,我们希望一个属性:对外表现为只读,对内表现为可读可写,我们将这个属性称为幕后属性。幕后属性语法
如:
1
2
3
4
5
6
7
8
9
10class 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:1
2
3if (foo::bar.isInitialized) {
println(foo.bar)
}
接口
KOtlin的接口可包含抽象方法的声明和具体方法的实现。它可以有属性,但是必须为抽象的且不允许有初始值。接口不会保存属性的值,实现时必须重新定义属性的值。
第一个接口
1 | interface MyInterface { |
这样我们就完成了一个接口的声明,他的foo()已经有了默认实现,而bar()还没有。
实现接口
KOtlin对象可以实现多个接口,用逗号分隔。
Kotlin 必须实现接口中所有没有默认实现的方法
格式为
1 | class 类名 : 接口1, 接口2,... { |
上面接口的实现
1 | class InterfaceImp:MyInterface { |
接口的属性
接口中的属性只能是抽象的,不能有初始值。
接口不会保存属性的值,实现时必须重新定义属性的值。
1 | // 接口 |
1 | // 实现接口 |
实现多个接口时,同名方法的重写
日常 Kotlin 开发时,可能要求一个类实现多个接口,不同接口中有相同的方法名的问题。
Kotlin实现接口,重写接口时,默认不会调用接口的方法,需使用super关键字显示调用。
1 | // 接口 |
1 | // 实现接口 |
拓展
拓展就是在已经定义好的类中,添加新的属性和方法,且不需要继承等。拓展不会对原类造成任何影响。
拓展函数定义格式
拓展函数的定义格式为
1 | fun 被拓展类名.函数名(参数列表) { |
一个简单的拓展函数:
拓展函数中this关键字表示调用拓展函数的对象。在这个例子中也就是a。
拓展一个空对象
伴生对象(内部类)
伴生对象是在类的内部定义的类,拓展也可以为伴生对象添加属性和方法。
简单的伴生对象拓展:
拓展属性
拓展属性格式为:
1 | val 被拓展类名.属性名:属性类型 |
一个简单的拓展属性
其它注意点
若扩展函数和成员函数一致,则使用该函数时,会优先使用成员函数。
数据类
数据类指只保存数据的类,不保存任何其他逻辑。
语法格式
该类以data关键字开头,并且需要定义一个构造函数,编译器会自动从主构造函数中根据声明的属性推导出相关函数。
1 | 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 类似:
声明泛型
声明泛型类
1 | class Box<T>(t: T) { |
声明泛型函数
Kotlin 泛型函数的声明与 Java 相同,类型参数要放在函数名的前面
1 | fun <T> boxIn(value: T) = Box(value) |
实例化泛型类
1 | val box<Int> = Box<Int>(1) |
如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径, 就可以省略类型参数:
1 | val box = Box(1) // 1 具有类型 Int,所以编译器推算出它是 Box<Int> |
向泛型类Box传入整形和字符串型示例:
实例化泛型函数
在调用泛型函数时,如果可以推断出类型参数,可以省略泛型参数
以下范例创建了泛型函数 doPrintln,函数根据传入的不同类型做相应处理
泛型约束
泛型约束可以约束某一个参数允许的类型。
没看懂,待研究
。。。
枚举类
Kotlin枚举类和Java类似,枚举类用enum标识符标识。
1 | enum class Color{ |
初始化枚举常量
枚举常量可以有初始化值,初始化值可以是数字、字符串或者其他枚举常量。
1 | enum class Color(val rgb: Int) { |
集合
Map 相关操作
Map类似于JSON,下面是JSON和Map的对比
1 | { "name":"微博" , "url":"www.weibo.com" } |
1 | mapOf("name" to "微博", "url" to "www.weibo.com") |
取键与值
在Map中取值有很多方法,:
- get()
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
- []
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
- getValue()
getValue找不到值会抛出异常
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
- getOrElse()
该方法若是找不到对应的键值,则返回Lambda的返回值.接收两个参数:- 参数一: 需要被查找的键名
- 参数二: 一个Lambda表达式
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
输出微博
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
因为传入了不存在的键,所以输出不存在则返回这个
5. getOrDefault()
该方法若是找不到键就直接返回默认值,接收两个参数:
- 参数一: 需要查找的键
- 参数二: 默认值
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
若要对所有key进行操作则可以使用属性
keys,若要对所有value进行操作则可以使用属性values.
1 | val numbersMap = mapOf("name" to "微博", "url" to "www.weibo.com") |
输出结果为
1 | 所有keys为:[name, url]所有values为:[微博, www.weibo.com] |
过滤
Map可以使用filter()来过滤map或其它集合,官网文档的说明是:基本的过滤函数是 filter()。当使用一个谓词来调用时,filter() 返回与其匹配的集合元素。对于 List 和 Set,过滤结果都是一个 List,对 Map 来说结果还是一个 Map。
a’ba’a’ba,什么谓词,根本看不懂好吧!!!,接下来我就按自己的理解描述一遍.
过滤就是集合中的每个元素依次和一个表达式或函数比较,若比较结果为true则将该元素添加进一个新Map,最后将新Map返回.
- 下面是一个例子
1 | val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11) |
filter()只能检查元素的值,若要使用元素在集合中的位置,则使用filterIndexed().
2. filterIndexed()
filterIndexed()在回调函数中传入的参数是元素的索引和元素的值,Map无此方法,因为map的索引值就是键.示例代码如下:
1 | val numbersList = listOf("one", "two", "three", "four") |
输出结果如下
1 | [two, four] |
- 前面都是结果为true的过滤集合,欸!我就是想使用false,那么可以使用
filterNot()方法来实现.
1 | val numbersList = listOf("one", "two", "three", "four") |
- 还未进行描述的参考这里
plus 与 minus 操作
进行集合plus(+)运算的时候:
- 若Map与Pair
无相同键,则Pair的键值对拼接在Map后面1
2
3val 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的值.1
2
3val 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()方法
1 | val map = mutableMapOf("one" to 1, "two" to 2) |
- 添加键值对集合
添加多个键值对使用putAll,它的参数可以是Map或Pair,Iterable、Sequence或Array
1 | val map = mutableMapOf("one" to 1, "two" to 2) |
- plusAssign (+=) 操作符。
1 | val map = mutableMapOf("one" to 1, "two" to 2) |
- [] 操作符为 set() 的别名。
1 | val map = mutableMapOf("one" to 1, "two" to 2) |
如果给定键已存在于 Map 中,则 put() 与 putAll() 都将覆盖值。
删除键值对
- remove() 函数,参数可以是
单个键或整个键值对,当使用键值对时只有键和值都匹配才会删除
1 | val map = mutableMapOf("one" to 1, "two" to 2) |
- 通过键或值删除键值对
1 | val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3, "threeAgain" to 3) |
- minusAssign (-=) 操作符
1 | val numbersMap = mutableMapOf("one" to 1, "two" to 2, "three" to 3) |
Kotlin进阶语法
Kotlin语言反引号``函数
在Kotlin中,可以用 **``**符号来声明一个非合法的函数名,这样的函数名就可以用于函数调用。
另外可以有效的解决调用Java中函数名是Kotlin关键字的问题。
1 | fun `一个测试函数`(){ |
标准函数和静态方法
标准函数let,with,run,apply,also
标准函数在被编译时不会产生多余的函数,而是会被直接拆分为语句进行运行.
run
第一种用法可以看成js里面的匿名函数,且自动返回最后一行
1 | fun main() { |
第二种用法可以简化一个对象的频繁调用
1 | class `计算器`(){ |
with
with的用法和run的第二个版本非常接近,在本质上和run的第二版本是一样的.
1 | class `计算器`(){ |
with的另一种用法是用于配置对象的属性,而对象的方法则在别处调用
1 | class `计算器`(){ |
apply
与run和with不同,apply最终返回的不是最后一行代码,它返回的是传入的类.也就是说你传入了计算机类,返回的还是计算机类
1 | class `计算器`(){ |
let
run会返回最后一行,而let则是run的替代,它不会返回
1 | fun main() { |
also
also则是apply的替代
is,!is,as,as?运算符
is 与 !is 操作符
kotlin中API提供的 is 运算符类似于Java中的 instanceof 关键字的用法.我们可以在运行时通过使用 is 操作符或其否定形式 !is 来检测对象是否符合给定类型:
1 | fun main() { |
is具有自动类型转换功能
as 与 !as 操作符
as运算符用于执行引用类型的显式类型转换。如果类型不兼容,使用as?运算符就会返回值null.Kotlin中父类是禁止转换为子类型的。
1 | open class `水果` |
协程
概述
- 什么是协程
协程是线程的另外一种形态.相较于线程来讲,协程具有占用空间小,线程切换速度快,非阻塞式,消除回调地狱等特点.协程是一种异步方法,他会在后台开一个线程执行任务,在执行完毕后会自动回调给主线程. - 什么时候使用协程
在有大量IO操作业务的情况下,我们采用协程替换线程,可以到达很好的效果,一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。
在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。
在延迟执行或循环执行的函数时使用线程.
协程只有和异步IO结合起来才能发挥出最大的威力。
协程的简单使用
- 延迟1秒输出World!,代码如下:
1 | import kotlinx.coroutines.* |
输出结果:
1 | Hello, |
本质上,协程可以称为轻量级线程。协程在 CoroutineScope (协程作用域)的上下文中通过 launch、async 等协程构造器(coroutine builder)来启动。
在上面的例子中,在 GlobalScope ,即全局作用域内启动了一个新的协程,这意味着该协程的生命周期只受整个应用程序的生命周期的限制,即只要整个应用程序还在运行中,只要协程的任务还未结束,该协程就可以一直运行delay() 是一个挂起函数(suspending function),挂起函数只能由协程或者其它挂起函数进行调度。挂起函数不会阻塞线程,而是会将协程挂起,在特定的时候才再继续运行
2. 等价线程代码
可以将以上的协程改写为常用的 thread 形式,可以获得相同的结果
1 | import kotlin.concurrent.thread |
协程和子线程是运行在线程上面的,相较于子线程,一个线程能运行更多的协程(可以是成千上万个).
线程的调度是由系统决定的,但协程可以由开发者指定.
分清阻塞代码和非阻塞代码
在上面的代码中,使用了阻塞代码Thread.sleep()和非阻塞代码delay().在实际开发中很有可能会忘记这两个函数谁是阻塞,谁是非阻塞.这时候就可以使用runBlocking来明确都为阻塞线程.
1 | import kotlinx.coroutines.* |
也可以直接将整合函数作为阻塞线程代码如下:
1 | import kotlinx.coroutines.* |
输出:
1 | World! |
等待作业协程函数
有时候并不希望协程马上被执行,这时就可以将它赋值给变量然后调用join()启动协程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 import kotlinx.coroutines.*
fun main() {
val a = test()
println("HELLO")
// 因为只能在协同程序或其他挂起函数调用挂起函数“join”
runBlocking {
a.join()
}
}
// 定义一个协程函数
fun test() = GlobalScope.launch {
delay(1000L)
println("World!")
}
结构化并发
什么是结构化并发,简单来说就是带有结构和层级的并发。
在Java中我们知道想并发编程就使用多线程,但是线程和线程之间却是没有父子关系的,而协程可以做一样的并发编程,这些协程却是有父子关系的。
- 说起来可能不好理解,我们直接看个例子:
1 | import kotlinx.coroutines.* |
取消结构化,减少内存泄漏的风险。
1 | import kotlinx.coroutines.* |
作用域构建器
除了官方提供的协程构造器,还能使用coroutineScope来声明自己的作用域,这个作用域直到启动的所有子协程都结束后才结束.
- 代码演示如下:
1 | import kotlinx.coroutines.* |
输出结果如下:
1 | Task from coroutine scope |
RunBlocking 和 coroutineScope 看起来很像,因为它们都需要等待其内部所有相同作用域的子协程结束后才会结束自己。区别在于 runBlocking 方法会阻塞当前线程,而 coroutineScope 只是挂起并释放底层线程以供其它协程使用,是非阻塞。runBlocking 是一个普通函数,而 coroutineScope 是一个挂起函数
调用协程函数
将launch 内部的代码块重写为一个独立的函数,需要将之声明为挂起函数。
1 | import kotlinx.coroutines.* |
使用 async 并发
如果 doSomethingUsefulOne() 和 doSomethingUsefulTwo() 这两个函数之间没有依赖关系,并且我们希望通过同时执行这两个操作(并发)以便更快地得到答案,此时就需要用到 async 了
1 | import kotlinx.coroutines.* |
需要注意,协程的并发总是显式的
惰性启动 async
可选的,可以将 async 的 start 参数设置为 CoroutineStart.lazy 使其变为懒加载模式。在这种模式下,只有在主动调用 Deferred 的 await() 或者 start() 方法时才会启动协程。运行以下示例:
1 | import kotlinx.coroutines.* |