kotlin 基础速描
跟刷极客时间<快速上手Kotlin开发>的流水账
资料来源:
极客时间
https://johnnyshieh.me/posts/java-generics/
https://www.kotlincn.net/docs/reference/generics.html
https://kaixue.io/更新
1
2
3
4
5
6
720.01.20 初始化
20.01.23 又是个大坑😥
20.01.29 集合操作符.
20.01.30 作用域函数/运算符重载/中缀表达式
20.02.08 泛型...把 java 泛型翻了一遍..
20.02.12 协程,还暂时只是使用.🤕.
20.02.14 重新补充基础内容
导语
- 总归要转到 kotlin ,趁这段封锁的时间熟悉一下.
- 配合 kotlin doc 食用更佳.ps: 官网的代码可以直接编辑执行.
- (20.01.29)药效貌似不错啊.
- (20.02.10)自始至终 kt 给我感觉更类似 python 而不是其他的 jvm 语言.
- (20.02.12)协程快吐了,1.3以前的api变动很多,理解上也有难度.
- (20.02.13)剩下的 io 库暂时用不到, KTX 是 Jetpack 的部分.暂时补充一下基础内容.整理等靠后吧.码上开学的入门不错,就是更新好慢啊,最后是19.10月份了.
第一章 基础
变/常量
- var 表示变量,val 表示常量.
- kt 的变量没有默认值,需要显式声明.
- kotlin 编译器可以识别变量类型时,可省略变量类型.
var = "das"
- 当部分变量没法在声明时初始化时,需要 lateinit 修饰,告诉编译器不再对此变量检查初始化.
- kotlin 编译器可以识别变量类型时,可省略变量类型.
- kotlin 为空类型安全.例如 String 即不为空,而 String? 可以为空.两者是不同的变量类型.
- String 可以直接赋值给 String? ,反之不能,可以使用强制 name = name2!!.
- 当调用一个可空变量的方法时需要使用
?.
而不是.
,这样 kt 会对变量判断非空再调用.例test?.one()
- 但有时必须返回一个值,需要
?:
兜底.val l = str?.length ?: -1
str 为空,返回 -1. - 或者使用非空断言,
test!!.one()
,告诉编译器不再检查非空.
- 默认是 pubulic .
类型: 数字、字符、布尔值、数组与字符串
- 数字:
- 整数: Byte Short Int Long ,默认是 Int 类型.显式指定 Long:
500L
- 浮点数: Float 和 Double.kt 中两者没有隐式的类型转换.默认为 Double 类型.Float 需要显式指定
500.12f
- 进制: 二进制
0b010
,十进制102
,十六进制0x0F
.没有八进制. - 特点:
- 数字这里有个特性是可以加下划线更易读.
199999.9899
=199_999.989_9
,无论整数小数,无论进制. - 表示范围较小类型并不是较大类型的子类型,需要强制类型转换.
- 数字这里有个特性是可以加下划线更易读.
- 整数: Byte Short Int Long ,默认是 Int 类型.显式指定 Long:
- 字符:
- 字符不能直接当作数字.(写惯了 c 有点不适应).如确实需要调用 toInt 方法.
- 特殊字符可以用反斜杠转义.
- 字符串
- String 类型,不可变类型.
- 两种表示:
"xx"
转义字符串可以有转义字符,"""xx"""
原始字符串,中间一起都是字符串,不包括转译,可以有空格换行等等. - 可以使用索引运算符访问: s[i],可以用 for 循环迭代.
- 可以使用 + 操作符连接字符串.只要第一个元素是字符串也可连接其他类型,调用其他类型的 toString 方法.
- 原始字符串.trimMargin() 可以去掉每一行的空格.
- 布尔类型与 java 相同.
- 数组: 与 java 差别有点大.
- Kotlin 数组使用
Array<T>
类来表示. - 声明数组
arrayOf(1, 2, 3)
或者arrayOfNulls(5)
全空.- Array 的构造函数.
val asc = Array(5) { i -> (i * i).toString() }
- 支持索引, forEach,in,in.
- 最重要的一点,kt 的数组是不可变类型,这意味着 kt 的数组不支持协变.即
Array<String>
并不是Array<Any>
的子类.无法把Array<String>
直接赋值给Array<Any>
.(但是可以通过类型投影做到,见泛型.Array<out Any>
) - 除了基本的
Array<T>
类,kt 还提供了无需装箱的 ByteArray,ShortArray,IntArray.但这些类与 Array 并没有继承关系.
- Kotlin 数组使用
- 数字:
集合
kt 中集合依旧是: List Set Map 3 种.
List
- 创建一个 List 比 java 简单 ,
val strs: List<String> = listOf("a", "b", "c")
. - 支持协变,这倒是与数组相反.即可以把
List<String>
类型赋值给List<Any>
. - 其他与数组基本相同.
- 到底用数组还是 List 视情况而定.性能需求苛刻,元素类型是基本类型时用数组.
- 创建一个 List 比 java 简单 ,
Set
- 创建 Set
val strSet = setOf("a", "b", "c")
. - 支持协变
- 创建 Set
Map
- 创建 Map
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
. - get 或者方括号键值读取.
val value1 = map.get("key1") val value2 = map["key2"]
. - put 或者方括号键值修改(要求是可变集合).
map.put("key1", 2) map["key1"] = 2
.
- 创建 Map
可变/不可变集合
- 上面对 Map 的修改,Map 必须要由 mutableMapOf() 创建,须要是可变集合.
- mutable 前缀的函数创建的是可变的集合,没有 mutbale 前缀的创建的是不可变的集合.不可变集合可以通过 toMutable*() 系函数转换成可变的集合.
- listOf() , mutableListOf(); setOf() , mutableSetOf(); mapOf() , mutableMapOf();
集合类还有一个 Sequence 类似 Iterable ,但是是惰性的,只有调用取值时才运算.
条件控制,比 java 更简化.
- if 基本用法没变,可以作为表达式赋值给变量
val max = if (a > b) a else b
. - 没有三元运算符
- switch:default 替换成了 when:else,配合 lambda 很简洁.
还支持多个条件或.
in 判断集合,is 判断类型,甚至省略 when 后面的参数,每一个都是布尔表达式.
例子
1
2
3
4
5
6
7
8
9
10
11
12when (x) {
1 -> { println("1") }
2 -> { println("2") }
1, 2 -> print("x == 1 or x == 2")
in 1..10 -> print("x 在区间 1..10 中")
is String -> print("true")
else -> { println("else") }
}
when {
str1.contains("a") -> print("字符串 str1 包含 a")
str2.length == 3 -> print("字符串 str2 的长度为 3")
}
- if 基本用法没变,可以作为表达式赋值给变量
kotlin 函数
参数(几乎与 python 相同)
- 变量:标识类型.: fun test(str:String):String {} 表面入参 str 返回一个 String 类型
- 参数可以有默认值.
- 每一个都是命名参数,例如在其他参数都有默认值时,可以通过参数命单独传入这一个参数的值.
- 参数很多时可以通过参数的位置传入值(位置参数).
- 当然传入值时,位置参数要在命名参数的前面.
没有返回值 java 返回 void 而 kt 返回 Unit
默认是 public ,但 override 修饰除外.
函数体只有1行时,可以直接赋值.
1
fun test_1(str: String ="test1") = println("fun $str")
顶级函数: 与 java 的一切皆对象不同,kt 的函数可以直接写在文件而不依附于对象.
- 这样声明的函数是全局的.
- 顶级函数属于包.两个不同文件的同名顶级函数,调用时 IDE 会自动加上包前缀.
函数嵌套: kotlin函数可以进行嵌套(与python相同).效果与 java 匿名内部类相似. 但只推荐需要递归,或隐藏内部函数时使用.
拓展函数: 静态的向类和对象中添加函数/变量等.第三方SDK或无法控制类的对象时使用.
拓展函数编译后静态方法,其中一个参数是 被拓展的类的对象.当其之类调用时,子类会被强转为父类.例
1
2/* 类名.拓展方法名*/
fun File.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)java调用,因为是拓展的File类,编译后拓展的函数实际上编译到了 FilesKt 类,调用时需要传入 kt 类的对象,和默认的参数
1
2
3File file = new File(".gitignore");
String text = FilesKt.readText(file, Charsets.UTF_8);
System.out.print(text);拓展函数最后被编译成一个 public static 的方法静态的添加方法,不具备运行时的多态.
1
2
3
4
5
6
7
8
9
10open class Animal
class dog:Animal()
fun Animal.name() = "animal"
fun dog.name() = "dog"
fun Animal.pr(animal: Animal) = println(animal.name())
//调用最后结果是 animal
dog().pr(dog())
Lambda 表达式(比java8的有限支持好用太多了).默认被编译为一个匿名内部类.
- lambda 表达式总是括在大括号中,完整语法形式的参数声明放在花括号内,并有可选的类型标注,函数体跟在一个 -> 符号之后.
val sum = { x: Int, y: Int -> x + y }
- lambda 表达式没有参数,可以省略 ->
- lambda 是函数最后一个参数,可以将大括号放在小括号后面.
- lambda 是函数唯一的参数,可以省略小括号
- lambda 参数数量上限是22个,因为只有 kotlin 标准库才可以使用 kotlin 的包名,超过22个需要只能自行添加 java 类实现.
高阶函数: 参数是一个函数的函数.
kt 的函数没有返回值时会有一个默认的返回值 Unit ,当函数作为参数时要显式的声明 Unit.
引用在类中的方法: 类名::方法名.
1
2
3
4
5
6
7
8
9
10inline fun onlyif(isdebug:Boolean,block:()->Unit){
if (isdebug) block()
}
val runnable = Runnable { println("runnable::run") }
val function: () -> Unit
function = runnable::run
onlyif(true,function)lambda 表达式在编译时是匿名内部类,大量使用会造成创建大量临时对象.使用 inline 关键字可以在编译时就将匿名内部类拆解成语句调用.但过度使用 inline 会加大编译器的负担,同时使得代码块过长.因此一般仅在高阶函数配合 lambda 时使用.
kotlin与java
kotlin 可以直接在文件中定义一个方法.编译后生成一个默认以文件名为类名的类.kt文件的方法为该类的一个内部静态方法,java 调用时通过 文件名.方法名 调用.
以 object 创建一个匿名内部类. kotlin中可以直接 类名.方法 调用. 对应java中: 类名.INSTANCE.方法. 这也是 kotlin 中声明单例的一种方法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15object Test{
fun hello(msg:String = "default"){
println(msg)
}
}
//kt
fun main() {
Test.hello("kotlin")
}
//java
public class test_java {
public static void main(String[] args) {
Test.INSTANCE.hello("java");
}
}Class在kotlin与java中格式并不相同.kotlin加载java的class需要声明
class.java
而 kotlin本身的类并不是 Class 而是 KClass .1
2
3
4
5
6
7
8
9
10testJavaClass(JaveMain::class.java)
testJavaClass(KotlinMain::class)
fun testJavaClass(clazz: Class<JaveMain>){
println(clazz.simpleName)
}
/*kotlin 的 class 为 KClass*/
fun testJavaClass(clazz: KClass<KotlinMain>){
println(clazz.simpleName)
}kotlin与java关键字上有部分冲突.此时需要加反引号转译. java 中 in 是变量 kotlin 中调用
println(JaveMain.
in)
kotlin中没有封装类,只有基本数据类型.所有 java 的封装类在 kotlin 中都会转换成基本数据类型.
kotlin 空指敏感,与java 互调时,不确定java的返回值是否为空时,需要使用kotlin可接收空值的数据类型.(String?)
kotlin 没有静态变量,静态方法.kotlin中可以通过 @JvmStatic 注解修饰方法,这样 java 中调用时不再需要 INSTANCE 关键字.
第二章 类与对象
基本
- kt 中默认的类是 public final ,需要显示声明 open .
- 继承的父类和实现的接口都在 : 后面,但是没有先后顺序.
构造函数
主构造函数的参数可以跟在类名后面的括号中,函数体在类内部
init
代码块.在主构造函数声明的变量在类中会生成一份拷贝,直接访问.
kt 支持次级构造函数,需要通过
constructor
声明,且每一个次级构造函数都需要基层一个主构造函数.可以是这个类本身 (this) 或者父类 (super) 的构造函数例:
1
2
3
4
5
6
7
8
9
10interface inter {}
open class baseclass() {}
class testclass(str: String = "test") : inter, baseclass() {
init {
println(str)
}
constructor(int: Int = 0) : this("1") {
println(int)
}
}
访问修饰符
- private: 私有,成员仅该类可以访问
- protected: 保护,成员该类和其之类可以访问
- public: 公开,成员随便访问.
- internal: kt 特有,一个模块内可以访问到.(类似c头文件的static?)
强制类型转换
java 中可以通过 instanceof 判断,然后
(tmp)test.function()
kt 中 is 关键词判断后可以直接调用无需强制类型转换的声明.同时可以不使用 is 判断,直接使用 as 强转.
1
2
3
4
5
6if (activity is NewActivity) {
//无需强转
activity.action()
}
(activity as NewActivity).action() //强转,可能报错.
(activity as? NewActivity)?.action()//强转成功再调用,更安全.
kotlin 中的 object .
- Object 在 java 中是所有类的基类,但在 kt 中 object(小写) 是个关键字,两者毫无关系.
- object 的意思就是声明类同时创建一个类的对象.替换 class.
- 上文提到了 object 在声明单例对象时非常常用.
伴生对象
通过
companion object{}
可以声明一个类的伴生对象.然后通过 类名.方法名 直接调用方法.类似 java 类的静态方法.实际上在编译后伴生对象被编译成一个名为 Companion 的静态对象,静态对象中包含了方法.所有在 java 中调用 kt 伴生对象的方式就是: 类名.Companion.方法名
例
1
2
3
4
5
6
7
8
9
10//类的内部
companion object{
fun isEmpty(str: String): Boolean{
return "" == str
}
}
//kt调用
testclass.isEmpty("test")
//java调用
testclass.Companion.isEmpty("test");单例模式,可以使用上文提到的 @JvmStatic 注解.或是使用推荐的伴生对象实现方式.
1
2
3
4
5
6
7
8
9
10class single private constructor(){
companion object{
fun get():single{
return Hodler.instance
}
}
private object Hodler{
var instance = single()
}
}
动态代理
动态代理含义: Java三种代理模式:静态代理、动态代理和cglib代理
kt 动态代理自然比 java 语法要简单很多. 关键词
by
.不再需要 InvocationHandler 和 Proxy .值得注意的是 kt 的动态代理在编译时已经编译为了静态资源,而不是像 java 通过反射进行.kt 的动态代理要比 java 快.(至于 java 的反射,有一篇已经吃灰的草稿,后面整理整理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19interface Animal{
fun bark()
}
class dog:Animal{
override fun bark() {
println("dog")
}
}
class zoo(animal: Animal):Animal by animal{
// override fun bark() {
// println("zoo")
// }
}
fun main() {
zoo(dog()).bark()
}
数据类
- java 中涉及用户的类通常是是 JavaBeans,只有属性没有方法,总是避免不了大量的 setter/getter 有时还有 toString hashCode 等等.
- kt 中在在 class 前添加
data
关键字,属性会自动生成- componentN() 函数( kt 的解构,见下一章)
- setter/getter
- equals/hashCode
- toString
- copy (特例不能显式实现)
- 如果数据类的父类中有 equals hashCode toString 显式的实现,则在数据类中编译器会直接采用父类的方法而不会重写.
- 主构造函数的参数必须是 val 或者 var.
- 数据类是 final 类型,无法被 open 修饰,因此其他类无法继承数据类.
密闭类
kt 中也有枚举类,用法与 java 完全一致,不再赘述.kt 的密闭类可以看作是枚举类的升级版
关键词
sealed
可以有子类,但是子类必须写在密闭类内部,或者同一个文件中.子类可以继承密闭类.用处最多的是配合 when 使用.
1
2
3
4
5
6
7
8
9
10
11sealed class PlayerViewType {
object GERRN : PlayerViewType()
object BLUE : PlayerViewType()
class VIP(var view: String, var button: String) : PlayerViewType()
}
fun getPleyerView(type: PlayerViewType) = when (type) {
PlayerViewType.BLUE -> Blue()
PlayerViewType.GERRN -> Green()
is PlayerViewType.VIP -> Vip(type.view, type.button)
}
第三章 高级特性
解构
是指 kt 中可以把一个对象解构成多个变量,一次性赋值.
简单示例
1
2
3
4
5
6
7
8
9
10class person(var name:String,var id:Int){
operator fun component1() = name
operator fun component2() = id
}
fun main() {
var a = person("1", 1)
var (_, id) = a
println("$id")
}解构可以一次性创造多个变量,可解构的类中要声明
componentN()
函数,且要用 operator 关键字标记.这个例子中 name 对应的就是 component1 ,id 对应的是 component2 .当然还可以有 34… .data class 中已经自动生成了 componentN().所以上面的例子中 person 的声明可以简化成
data class person(var name:String,var id:Int)
.对于不想使用的变量可以使用下划线代替,不需要为其命名.下划线对应的 componentN 不会调用.
data class 配合解构可以优雅的实现从函数返回多个值.
解构另一个重要的引用就是 map 的遍历(让我想起了 python 中方便的遍历)
1
2
3
4
5val map: Map<String, Int> = mapOf<String, Int>("a" to 1, "b" to 2)
for ((k,v) in map){
println("k:$k,v:$v")
}
循环
与 java 不同 kt 没有我们熟悉的
for(i=0;i<10;i++)
这样的循环语句.kt 更加类似 python ,抽象的层次更高.要实现我们熟悉的循环,下面是几个典型的例子.
for (i in 1..10 step 1)
与for (i in 1 until 10)
..
是until
的重载. 两者完全等同,两者最后都是 IntRange 函数的调用.1..10
表示 1到10 的闭区间,包括10.step
步长选择,步长只能为正数.
for (i in 10 downTo 1 step 2)
- 表示从 10 到 1 闭区间.
- 步长必须为正值.
repeat(10){repeat(10){ print("$it") }
repeat()
是高阶函数.- 实际上在传入的闭包重复执行 10 次
- 执行的序号可以通过 it 访问.
for 循环还有一个用途是对象的遍历.
for(i in list)
最简单的写法.- 如果要带上索引,需要解构的写法使用
list.withIndex
for ((index,i) in list.withIndex())
集合操作符
对集合进行链式操作.有用过 rxjava 的大概能理解链式操作的便利.kt 中集合操作符配合 lambda 表达式,比 rxjava 更简洁.
集合操作符有几个分类,这里也没有列举全,具体参考 集合操作概述
集合转换,从现有集合中创建新的集合
- map{}: lambda 将作用于原集合的每一个元素.常用于原集合的类型转换.
- mapIndexed{ idx, value -> xx }: 如果 lambda 中同时需要索引.
- mapNotNull{} / mapIndexedNotNull{} : 如果需要过滤掉空元素.
- zip{a, b -> xxx}: 双路合并.
- unzip(): 拆解双路,可与解构连用.
- flatten(): 拆解集合中的嵌套返回单一的集合.
- flatMap(): 拆解并返回一个list?
- joinToString(): 拼接集合内的字符,返回一个字符串.可以在 lambda 中通过 it 关键词对集合元素处理.
- 自定义参数 (separator = " | ", prefix = "start: ", postfix = “: end”,limit = 10, truncated = “<…>”)
- separator 是每个集合元素的间隔,prefix 字符串开头,postfix 字符串结尾.
- limit 限制输出的字符串长度,truncated 截断后附加的字符串
- 自定义参数 (separator = " | ", prefix = "start: ", postfix = “: end”,limit = 10, truncated = “<…>”)
- joinTo(str): 凭借集合字符到 str,自定义参数同上.
集合过滤,按照 lambda 中表达式的 true/false 决定集合元素的取舍.
- filter{}: lambda 中为 true 元素留, false 舍.不改变原集合的类型.
- filterIndexed{index,s -> xx}: 如果需要用到索引.
- filterNot{}: 与 filter 完全相反.
- filterIsInstance(): 没太看懂.
- filterNotNull(): 返回所有非空.
- partition{}: 划分(翻译成分组更好一点吧).将原有集合分成 lambade 返回 true 和 false 的两部分.可以通过解构取得这两个子集合.
- any{}: 如果有元素的 lambda 为 true,则返回 true.
- none{}: 如果所有元素的 lambda 都为 false,返回 true.如果 lambda 为空,则原集合有元素返回 false,没有元素返回 true.
- all{}: 如果所有元素的 lambda 都为 true,则返回 true.注意 如果原集合为空 all 始终返回 true.如果 lambda 为空,则原集合有元素返回 true,没有元素返回 false.
+
/-
操作符.这两个比较特殊.直接对应的集合的 + / -,不再赘述.分组操作符: 可以按照 lambda 的返回对原集合 元素 进行分组.(相当于嵌套?)
- groupBy{}: 最基础的分组操作符.按照 lambda 的返回值进行分组.还有拓展的条件
- groupBy(keySelector = { it.first() }, valueTransform = { it.toUpperCase() })
- keySelector: 指定分组的键值.
- valueTransform: 对元素进行处理.
- 对每个分组进行操作
- eachCount(): 计算每个分组的数量
- reduce() / fold() / aggregate() 暂时没搞懂…
- groupBy{}: 最基础的分组操作符.按照 lambda 的返回值进行分组.还有拓展的条件
取子集操作符.顾名思义不对元素过滤,按照 索引 取指定的子集.(索引从 0 开始)
- slice(1…2): 选取集合 [1 2] 对应位置的子集.这里取子集的规则与循环那里类似.
- (1…3): [1,3] 闭区间
- (0…4 step 2): [0,4] 闭区间,步长为 2 .实际上是 0 2 4.
- (setOf(3, 5, 0)): 直接指定索引.
- take(): 从第一个元素开始连续选取几个元素.
- takeWhile{}: 从第一个元素开始,如果 lambda 为 true 保留,否则丢弃.直到第一个 lambda 为 false 终止.
- takeLast(): 从最后一个元素开始,倒着选取几个元素.
- takeLastWhile{}: 从最后一个元素开始,倒序 如果 lambda 为 true 保留,否则丢弃.直到第一个 lambda 为 false 终止.
- drop(): 从第一个元素开始,丢弃几个元素,并返回剩下的集合.
- dropWhile{}: 从第一个元素开始, lambda 返回 true 丢弃,直到第一个 lambda 返回 false 终止,返回剩余集合.
- dropLast(): 从最后一个元素开始,倒着丢弃几个元素,并返回剩下的集合.
- dropLastWhile{}: 从最后一个元素开始,倒序 lambda 返回为 true 丢弃,直到第一个 lambda 返回 false 终止,返回剩余集合.
- chunked(3){}: 按照索引步长为 3,对集合进行划分.后面可以跟 lambda 表示对一个组内的元素执行的操作.
- windowed(3): 按照滑动窗口取索引进行分组.默认情况下,窗口大小 3 ,从第一个元素开始,取窗口类的3个元素.然后向下滑动 1 ,再取下一个窗口.可以自定义参数.
- windowed(3, step = 2, partialWindows = true): 窗口大小为 3,每次的步长为 2.partialWindows 是是否输出 最后小于步长的元素分组.
- slice(1…2): 选取集合 [1 2] 对应位置的子集.这里取子集的规则与循环那里类似.
取单个元素:根据索引取单个元素.索引从 0 开始.
- elementAt(3): 取第 4 个元素.(从 0 开始),超出索引报错.
- elementAtOrNull(5): 超出索引返回 null.
- elementAtOrElse(5){}: 超出索引返回 lambda 的返回值.
- first(): 返回第一个元素,索引0.
- first{}: 返回第一个 lambda 为 true 的元素.
- find{} = firstOrNull{} : 同上,但没有符合元素时返回 null .
- last(): 返回最后一个元素
- last{}: 从最后一个元素开始,倒序,返回第一个 lambda 为 true 的元素.
- findLast{} = lastOrNull{}: 同上,但没有符合元素时返回 null .
- random(): 随便取一个元素😂.
- contains(a): a 是否在集合中,存在则返回 true.不存在则返回 false. 还有更简单调用 xx in 集合.
- containsAll(listOf(“a”, “b”)): 一次检查多个元素.
- isEmpty() / isNotEmpty() : 不解释.
集合排序: 依照规则对集合元素排序.
- sorted(): 默认调用比较的对象实现 compareTo 方法.用户自定义类需要继承 Comparable 接口,实现 compareTo 方法.
- reversed(): 默认调用比较的对象实现 compareTo 方法,逆序排序.返回一个包含结果的新集合,不影响原集合.
- asReversed(): 效果同上,但原集合是不可变对象时,可能跟快更轻巧.
- sortedWith(): 自定义一个比较器,每次比较即调用自定义的 Comparator .比较器还可以简单的使用 compareBy 和 lambda 生成.
compareBy { it.length }
- sortedBy{}: 对于不可比较的对象,或者希望自定义顺序排序.依照 lambda 的结果顺序排序(实际上与 sortedWith() 差别不大)
- sortedByDescending{}: 对于不可比较的对象,或者希望自定义顺序排序.依照 lambda 的结果逆序排序
- shuffled(): 随机排序.
聚合操作符: 大概是处理全部的元素,输出结果.
- min() / max(): 最值
- average(): 平均值
- count(): 元素个数.
- maxBy{} / minBy{}: 根据 lambda 的结果返回最大,最小值.
- maxWith() / minWith(): 对于不可比较对象和想自定义排序的,加载一个比较器.可以用 compareBy 和 lambda 生成.
- sum(): 总和
- sumBy{}: lambda 结果求和,元素必须为 Int.
- sumByDouble{}: lambda 结果求和,元素必须为 Double.
- fold(2){sum, element -> xx}: sum 和 element 的 lambad 结果不断代入下一层循环的第一个参数 sum.默认第一轮是 sum = 2.
- reduce { sum, element -> xx}: sum 和 element 的 lambad 结果不断代入下一层循环的第一个参数 sum,默认 sum 是集合索引为 0 的元素.
- reduceRight() / foldRight(): 同上,但是顺序从最后一个元素开始逆序.
- reduceIndexed{idx, sum, element -> xx} / foldIndexed(2){idx, sum, element -> xx}: 带索引.
- reduceRightIndexed{idx, sum, element -> xx} / foldRightIndexed(2){idx, sum, element -> xx}: 同上,但是从最后一个元素开始逆序.
作用域函数
- 不同于集合操作符只能作用于集合对象,作用域函数可以作用于任意对象.
- 作用域函数只是在对象的上下文执行 lambda 闭包,没有引入新的功能.只是通过作用域函数代码不必再新命名变量,更加讲解易读.(越简洁,出错可能性越少)
- run{}: 返回 lambda 执行的结果.没有闭包参数,使用 this 代指调用对象.
- let{}: 返回 lambda 执行的结果.有闭包参数.可显式声明
str:String -> xx
,也可以使用it
代指. - apply{} 不返回 lambda 执行结果,返回调用对象,可链式调用.没有闭包参数,使用 this 代指调用对象.
- also{}: 不返回 lambda 执行结果,返回调用对象,可链式调用.有闭包参数.可显式声明
str:String -> xx
,也可以使用it
代指. - takeIf{}: 返回判断的结果,lambda 为 true 返回此对象,否则返回 null.
- takeUnless{}: 返回判断的结果,lambda 为 fasle 返回此对象,否则返回 null.
- with(s){}: 与上面的不同,with 是顶级函数而不是拓展函数.对 s 进行一系列操作.
- repeat: 同循环.
运算符重载
- 我们可以对 kt 的运算符重载,方便代码的编写.且重载运算符不会引入函数调用的开销.
- kt 中运算符总数有上限,一个运算符实际对应一个函数调用,对应关系在编译时已经完成.
- 以
a + b
为例,编译器遇到+
首先查找 a 的类型 T,在 T 中查找函数名为 plus() 参数为 T 类型的函数.且要求 plus() 被operator
修饰.将运算符变成对应函数的调用. - 一元运算符
- +a | a.unaryPlus()
- -a | a.unaryMinus()
- !a | a.not()
- a++ | a.inc() +
- a-- | a.dec() +
- 二元运算符
- a + b | a.plus(b)
- a - b | a.minus(b)
- a * b | a.times(b)
- a / b | a.div(b)
- a % b | a.rem(b)
- a…b | a.rangeTo(b)
- a in b | b.contains(a)
- a !in b | !b.contains(a)
- 索引
- a[i] | a.get(i)
- a[i, j] | a.get(i, j)
- a[i_1, ……, i_n] | a.get(i_1, ……, i_n)
- a[i] = b | a.set(i, b)
- a[i, j] = b | a.set(i, j, b)
- a[i_1, ……, i_n] = b | a.set(i_1, ……, i_n, b)
- 调用
- a() | a.invoke()
- a(i) | a.invoke(i)
- a(i, j) | a.invoke(i, j)
- a(i_1, ……, i_n) | a.invoke(i_1, ……, i_n)
- 广义赋值
- a += b | a.plusAssign(b)
- a -= b | a.minusAssign(b)
- a *= b | a.timesAssign(b)
- a /= b | a.divAssign(b)
- 其他
- a == b | a?.equals(b) ?: (b === null)
- a != b | !(a?.equals(b) ?: (b === null))
- a > b | a.compareTo(b) > 0
- a < b | a.compareTo(b) < 0
- a >= b | a.compareTo(b) >= 0
- a <= b | a.compareTo(b) <= 0
中缀表达式
- 运算符的数量的不足,可以通过定义中缀表达式缓解.
- 例:
infix fun Int.io(string: String){}
- 中缀的函数必须由 infix 修饰.
.
前面是函数接收者的类.只有此类型的对象才可以调用中缀表达式.(后面参数没有类型限制)- 必须只有一个参数,且参数不得接受可变数量的参数不能有默认值.
- 可以使用 this 引用函数接收者的方法.
- 调用
4 io "123"
当然也可以直接通过函数调用4.io("123")
. - 要注意的是中缀表达式的优先级高于布尔操作符.低于算术操作符.
反引号
- 解决关键字冲突
- 强制不合法字符变为合法字符.
- 例如函数名声明为
fun `123`
,调用时也是`123`()
但是这个函数 在 java 中无法调用 .无论是`123`()
还是123()
都无法调用. - 还是慎用.除非你确定不想让 java 调用这个方法.
对象的比较
- 这个在初次接触 kt 时,被
==
和===
坑了一把… - java 中引用的比较是
a == b
,值的比较是a.equals(b)
,而在 kt 中值的比较是a == b
,引用的比较是a === b
.==
在 kt 和 java 中作用正好相反.
- 这个在初次接触 kt 时,被
类型别名
- 给当前的类型起个别名,可以是方法,类,泛型等等.
typealias xx = xx
,不会引入新类型,在编译时已经处理,不会影响运行速度.- kt 中非常多的集合类方法都是通过 typealias 来的.
DSL(暂时跳过)
泛型: 使用泛型的初衷,大概希望把与数据结构无关的逻辑抽象出来,泛型是一个数据类型的标记. (明明写的是 kt 的泛型,java 写了一堆…😤…)
java 泛型基本使用
<T>
基本形式,单独使用 T 相当于 Object.<T extends Number & Interface1>
限制 T 只能为 Number 或者 Number 的子类,并且必须实现 Interface1.<?>
通配符,直接使用代表 object .<? extends SuperType>
上界通配符,限定传入的对象必须是 SuperType 或其子类.SuperType 可以是类或者接口.<? super SubType>
下界通配符,限定传入必须是 SubType 或其父类.SuperType 可以是类或者接口.- PECS 原则: Producer Extends Consumer Super,简而言之,只读 extends 只写 super
- 可以有泛型类,构造函数也可以是泛型,接口可以是泛型,静态成员不能有泛型,但可以声明静态泛型方法.
java 类型擦除: java 对泛型的支持并不包括运行时支持,所有泛型参数在编译后都会被擦除.
<T>
当作 object,这样导致了一些 java 泛型的限制.- 基本类型不能转换成 object,所以泛型不能是基本类型.
- 无论是
Pair<String>
还是Pair<Integer>
运行时getClass()
得到的始终是同一个Pair.class
.因为在编译时两者都转换成了Pair<Object>
. - 不能实例化 T 类型,要实例化 T 可以使用
Class<T>
通过反射来,使用时候传入具体的 xx.class.
<T extends Number>
T 会替换成 Number 类型.具体使用时比如我们传入了 Integer 类型,编译器还会加上强制类型转换(Integer) xxx
.
java 泛型限制
- 不能是基本类型
- 不能实例化 T ,但可以通过反射
Class<T>
简介实现. - 可以声明泛型数组,但不能直接创建泛型数组,必须强制转型.
- 静态成员不能有泛型.
- 不能继承或间接继承 Throwable 类.
- 重载方法时,不能重载编译后参数类型为相同原始的方法
kt 泛型: kt 编译后也是 jvm 字节码,这意味着 kt 的泛型也不是类似 c# 的运行时的真泛型.但是 kt 相对 java 对泛型的支持还是激进了很多.
kt 泛型基本使用,与 java 没啥太大区别
class Box<T> {}
fun <K, V> compare(p1: Pair<K, V>, p2: Pair<K, V>){}
compare(Pair(1, "1"), Pair(2, "2"))
类型可以推断,可以省略<String,Inter>
泛型约束
<T>
默认代表Any?
类型,可空.如果确认泛型不为空,应该使用<T: Any>.
与 java 不同,kt 的
<>
只能声明一个上界.多个上界需要 where.1
2
3fun <T> test(list: List<T>)
where T : Comparable<T>,
T : CharSequence {}
型变: 对参数类型进行子类型转换.
- 例如
List<Integer>
与List<Number>
两者在代码中依旧是不同的类,没有继承的关系.但是在 java 中通过通配符可以在两者之间建立继承关系.- 协变:
List<? extends Number>
可以传入List<Integer>
.这样List<Integer>
是List<? extends Number>
的子类. Integer 是 Number 的子类.顺序的转换. - 逆变:
List<? super Integer>
可以传入List<Number>
.这样List<Number>
是List<? super Integer>
的子类,但是 Number 却是 Integer 的父类.这样的转换称为逆变.
- 协变:
- 型变有两种
- 使用时型变(类型投影): 泛型用在参数 属性 局部变量或返回值.在 java 就是 extend/super.在 kt 中对应 in/out .两者使用原则一致,不加赘述.
- 声明时型变: 泛型类型和泛型函数.java 不允许这样使用.kt 可以.
- 例
- out: 修饰后传入对象为 T 或 T 的子类(协变).类似 extend 传入对象只读.T 通常用在成员函数的输出类型,而不用在输入.
interface Test<out T> {fun nextT(): T}
- in: 修饰后传入对象为 T 或 T 的父类(逆变).类似 super 传入对象 只写.T 通常用在成员函数的输入类型,而不用在输出.
interface Test<in T> {fun compareTo(other: T)}
- out: 修饰后传入对象为 T 或 T 的子类(协变).类似 extend 传入对象只读.T 通常用在成员函数的输出类型,而不用在输入.
- 例如
星投影: 使用泛型时候如果参数类型未知,使用时用
*
代替参数类型.java 中可以使用原始类型(Raw Type) 但是总归存在隐患.星投影比 java 的原始类型更加安全.使用 java 的原始类型,编译器
1
2
3ArrayList<String> list = new ArrayList<>(5);
ArrayList unkownList = list; //原始类型
unkownList.add(1); //错误,但编译器提示不能检查此段代码星投影可以根据原泛型的声明,编译时检查代码,提示错误.
1
2
3val list: ArrayList<String> = ArrayList(5)
val unkownList: ArrayList<*> = list //星投影
unkownList.add(1) //编译器提示错误.Foo <out T : TUpper>
: T 具有上界 TUpper 的协变类型参数.Foo <*>
等价于Foo <out TUpper>
. 当 T 未知时,可以安全地从Foo <*>
读取 TUpper 的值.Foo <in T>
: T 是逆变类型参数,Foo <*>
等价于Foo <in Nothing>
. T 未知时,无法安全写入Foo <*>
.Foo <T : TUpper>
: T 具有上界 TUpper 的不型变类型参数.Foo<*>
读取时等价于Foo<out TUpper>
, 写入时等价于Foo<in Nothing>
.对有多个泛型参数的方法或类,每个参数的星投影独立生效,互不影响.
"真"泛型: 因为类型擦除, java 中无法在运行时获取泛型 T 的class.但是 kt 中可以内联函数 + reified 修饰泛型,变通的实现.要加引号.
例
1
2
3inline fun <reified T> Check():{println(T:)}
fun main() { Check<String>() }
//输出 class java.lang.String(此处可能是错的)编译器会将该方法内联到调用的地方,跟直接没有泛型方法这一层是一样的.这样实际上编译时候就 T 的具体类型已经知道了.
要注意的是 reified 的内联函数不能被 Java 调用.
第四章 kotlin 语法背后
val 与 var 区别
- var 有 set 和 get .val 只有 get .
- val 修饰的变量是无法赋值,但是可以通过重写 get 方法使得返回值发生变化.
真*常量
- val 前面使用 const 修饰.在编译时变量的值已经确定无法修改,这是常规意义上的常量.
- const 只能修饰在 object 类型的属性,或者是 top-leve 类型.
- 具体是匿名内部类或者伴生对象.top-leve 就是变量直接定义在文件而不是类中.
空安全: kt 这一点深得我心,工作接触 c 最触头的就是空指针.
- 避免空指针无非只有两者途径.kt 中两者兼有.
- 运行时处理,每次使用引用对象时进行判空.
- 编译时处理,不能为空的类型为空直接编译报错.
- 避免空指针无非只有两者途径.kt 中两者兼有.
内联函数 与 lambda
在高阶函数中提到了一个关键词
inline
可以将 lambda 直接转化为对应语句.相当于把 lambda 匿名内部类这一层去掉了.这里就有一个问题,如果在高阶函数的 lambda 中有 return 会怎样?
inline
把 lambda 这一层去掉了,那么 return 相当于直接写在了调用这个高阶函数的上层函数中.上层函数直接中断返回了.实际上一般的 lambda 中不允许出现 return 语句,只能返回自身
test1 {return@test1}
.只有 inline 修饰的内联函数的 lambda 才允许使用 return .1
2
3
4
5
6
7
8
9
10
11
12
13fun main() {
test1 {
println("test1")
//不允许错误
return
}
println("test2")
}
inline fun test1(crossinline t1: () -> Unit) {
t1.invoke()
return
}对于需要中断 lambda 自身,但是并不希望中断上级函数的,可以通过 crossinline 修饰 lambda.这样 lambda 的调用依旧会被内联,但是 lambda 本身不再执行内联,lambda 内部返回与没有 inline 修饰时相同.
1
inline fun test1(crossinline t1: () -> Unit) {}
当高阶函数的返回值是一个 lambda 时,必须使用 noinline 修饰,拒绝内联,否则会出错.
1
inline fun test1(noinline t1: () -> Unit): () -> Unit { return t1 }
第五章 kotlin 扩展库
协程
协程还没有统一的定义.kt 的协程也有一堆神仙在打架,暂时专注在用法上.底层有机会翻完源码再撕.kt 的协程在 1.3 版本正式加入,1.3 以前的一些 api 已经不再适用.
暂时的看法是: kt-jvm 的协程当作轻量级线程.
- kt-jvm 始终是依托 jvm 虚拟机,应该是无法突破 jvm 的限制(可能,大概,也许).kt-jvm 协程的本质可能还是j ava 线程的友好封装?(存疑).
- 如何解决并发任务? 想起了被 java 回调地狱支配的恐惧,后来 rxjava 链式操作解放了我们. kt 协程更进一步,按照官方文档说是可以通过看似同步的代码实现非阻塞式的异步任务,挺绕的,但是好用.
添加协程支持
在 project 的 build.gradle 添加一个环境变量
ext.kotlin_coroutines = '1.3.3'
(目前最新版本是 1.3.3)在 module 的 build.gradle 添加核心库和平台库(这里是 android)
1
2implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
启动协程
- runBlocking(){}: 线程切换到协程,但会阻塞当前所在的线程,一般仅在单元测试时使用.
- GlobalScope.xx: 不会阻塞线程,但 GlobalScope 启动的协程与应用的生命周期一致,同样不推荐
- launch(){}: 最常用的启动协程的方式,返回一个 Job 类型的对象.
- async/await(){}: 启动协程并返回执行的结果,返回一个 Deferre 对象.
- 自定义 CoroutineContext: 推荐.之后的方法有 launch/async/await.此处就不展开了,等待刷完 jetpack 再补充.
协程的启动方式.在启动参数的第二个 start: CoroutineStart.
- DEFAULT: 默认的启动方式,当前线程什么时候有空,什么时候启动.
- LAZY: 没有手动调用启动时不会启动.例如 Job 类型对象就是调用 start 或 join 方法.
- ATOMIC: 原子类型? 只要启动就停不下来.(啥时候用?)
- UNDISPATCHED: 未定义.用于自定义启动参数.
- block: suspend CoroutineScope.() -> Unit : 要执行的闭包,真正的协程内容.
协程取消(基本是 Job 类型的对象)
最简单的是
.cancel()
取消后协程不再返回..join()
: 等待协程结束. 或者使用.cancelAndJoin()
等价于.cancel()
和.join()
连用.1
2
3
4
5
6
7
8
9
10val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1200L) // 延迟一段时间
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
println("main: over")但是对于执行计算任务的协程,
cancel()
以后还正常输出到结束. 对于计算任务的协程取消需要在协程内检查 isActive.(通知协程自尽)1
2
3
4
5
6
7
8
9
10
11val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("循环: $")
nextPrintTime += 500L }}}
delay(1300L) // 等待一段时间
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now I can quit.")取消协程时会抛出
CancellationException
异常,因此可与 try-finally 连用,在结束协程时释放某些资源.在 finally 代码块中,如果必须调用阻塞式的代码,可以使用withContext(NonCancellable) {……}
包装.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并等待它结束
println("main: Now I can quit.")还有一种情况是协程运行超时,例如网络请求有有一个最长超时时间.可以利用 withTimeout() 实现协程超时自杀.自杀后会抛出 TimeoutCancellationException 异常.通过 try-finally 进行一些关闭资源的工作.如果不需要这个处理,可以采用 withTimeoutOrNull() 避免抛出异常.两者功能相同.
1
2
3
4
5
6
7
8val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // 在它运行得到结果之前取消它
}
println("Result is $result")
async 实现并发
假设有这样两个函数
1
2
3
4
5
6
7
8
9suspend fun One(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 13
}
suspend fun Two(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}计算两者的和
val tmp = One()+Two()
.这样代码是串行执行,总共要 2s.有了协程后,我们能不能把两个函数扔到两个协程里面,同时开始执行?这样可以节省一半的时间.肯定是可以的,通过协程的 async 包装,await 获取结果就能实现并行.最后输出运行时间是 1035 ms.
1
2
3
4
5
6val time = measureTimeMillis {
val one = async { One() }
val two = async { Two() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")但是这样非常不方便异常处理.假设 val one = async { One() } 这一行因为 One() 内部的 bug 而抛出异常.正常情况下程序应当停止创建和启动 one 这个协程,并抛出异常的信息给开发者,之后就跳过这个代码块执行其他内容.但是就这里的代码而言,程序还是会继续向下执行并创建和启动 one 这个协程,虽然最终结果都是抛出异常终止了代码块的执行.
正确的风格: 使用 coroutineScope 将协程执行部分包装成一个异步函数,当代码块遇到异常时会直接终止,抛出异常.主函数调用依然是并行执行.
1
2
3
4
5suspend fun concurrentSum(): Int = coroutineScope {
val one = async { One() }
val two = async { Two() }
one.await() + two.await()
}
协程上下文与调度器
上下文是 Job 类型对象启动参数的第一个,用在协程直接切换传递参数,就是协程的调度器.决定协程运行在那个线程.
调度器
Dispatchers.Default: 默认调度,使用 jvm 的共享线程池,适合 cpu 密集型.
Dispatchers.Main: 运行在主线程,Android 更新 UI 等.
Dispatchers.IO: 适合 IO 密集型.
newSingleThreadContext(“xxx”): 开启一个新线程来执行协程,这样代价很高,一般会复用线程以节省资源.当协程和线程不再需要时必须关闭.
Dispatchers.Unconfined: 这个特殊一点,是非受限调度器.它仅仅运行到协程代码块中第一次挂起的地方.之后协程在那个线程恢复完全由被调用的挂起函数来决定,即不限制恢复的线程.程序第二次会输出运行在 delay() 的 DefaultExecutor 的线程上.
1
2
3
4
5launch(Dispatchers.Unconfined) { // 非受限的——将和主线程一起工作
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
子协程
- 当一个协程被其它协程在 CoroutineScope 中启动的时,新协程将通过 CoroutineScope.coroutineContext 来继承上下文,并且这个新协程的 Job 将会成为父协程的子协程.当父协程被取消时,其所有子协程也会被递归的取消.
- 当使用 GlobalScope 启动是个特例,这里新协程没有父协程.因此它与这个启动的作用域无关且独立运作.
协程作用域
上文提到使用 GlobalScope 启动的协程生命周期和整个应用的生命周期一致.而在具体的 Android 应用中协程直观上应该是和使用它的组件生命周期一致.因此直接使用 GlobalScope 并不是一个很好的协程启动方式.而且必须考虑在应用退出函数中取消掉所有协程,而以 GlobalScope 启动的协程必须一个一个取消,协程数量一旦上去了,非常麻烦.
最好是自行创建一个 CoroutineScope 来管理协程的生命周期.
实现 CoroutineScope() 接口: 使用委托模式同时指定调度器
1
2
3
4
5class MainActivity : AppCompatActivity() , CoroutineScope by CoroutineScope(Dispatchers.Default){
fun destroy() {
cancel() // activity 退出时
}
}MainScope() 方法:
1
2
3
4
5
6class MainActivity : AppCompatActivity() {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
}
管道 channel
kt 的 channel 专门用于协程之间通信.最简单的声明一个 channel 类型变量,一个协程 send 一个协程 receive .
channel 还可以使用 for 循环遍历.但所有元素均 send 完毕,调用 close 关闭.
channel 是一个典型的生产者/消费者模式,通常使用 produce 来构建 channel 的生产者协程,返回一个 ReceiveChannel<> 类型.
1
2
3
4
5
6
7
8fun Test() = GlobalScope.produce {
for (x in 1..5) send(x * x)
}
fun main() = runBlocking<Unit> {
val test = Test()
for (y in test) println(y)
}配合协程 channel 可以产生类似流一样效果.生产,过滤,消费.
channel 可以是一对多,多对多,多对一.效果一致,先进先出的队列.
channel 可以指定缓存池
Channel<>(5)
,在 produce 中通过 capacity 参数指定GlobalScope.produce(capacity = 5) {}
.当缓存池满时,send 协程会暂时挂起,直到缓存池有空余.缓存池为空,receive 协程会挂起,直到缓存池有元素.