Kotlin 与 Java 的关系 Java运行过程:Java源代码 -> 编译 -> 生成class文件 -> JVM解释执行
Java虚拟机并不关心class文件是如何生成的,因此也可以使用Kotlin生成class文件,JVM也可以解释执行。
Kotlin可以无缝使用Java第三方的开源库。
Q:Android为什么推荐Kotlin?Kotlin与Java比有哪些优势?
A:
空安全
Lambda表达式和高阶函数
Kotlin更简洁,引入了数据类、get、set方法等
拓展函数
支持协程
完全兼容Java
val 与 var 类比JavaScript中的const和let。
val用于声明常量,相当于加了final,其引用不可变,赋值后无法修改;var用于声明变量。
空安全 Q:哪些情况会出现NPE(NullPointException)?
A:
强制!!
lateinit但未初始化
与Java通信,Java返回的空值
泛型擦除导致
Any、Unit
Any等同于Java中的Object,是所有类的父类
Unit等同于void,当函数无返回值时会自动加上返回值Unit
if Kotlin中的if是带有返回值的,返回值即花括号内的最后一行表达式的值:
1 2 3 4 5 val max = if (a > b) { a } else { b }
字符串 字符串模板 $后花括号内可以是一个表达式:
1 2 val str = "你好" println("$str ,${str.replace("你好" , "再见" )} !" )
多行字符串 三对双引号内的字符串可换行:
1 2 3 4 5 6 7 8 val poem = """ 关关雎鸠, 在河之洲。 窈窕淑女, 君子好逑。 """ .trimIndent()println(poem)
其中trimIndent函数的作用是去除输入行的公共最小缩进(空行不影响)。输出为
1 2 3 4 关关雎鸠, 在河之洲。 窈窕淑女, 君子好逑。
有时为了让代码更清晰,我们可以使用|符号结合trimMargin函数来标记实际字符串的开始位置:
1 2 3 4 5 6 7 8 val poem = """ | 蒹葭苍苍, |白露为霜。 | 所谓伊人, |在水一方。 """ .trimMargin()println(poem)
其中trimMargin函数的作用是将多行字符串中的每行内容使用特定的前导字符(默认为|)进行标记,然后去除这些字符及其前面的空白。输出为
1 2 3 4 蒹葭苍苍, 白露为霜。 所谓伊人, 在水一方。
也可指定不同的前导字符:
1 2 3 4 5 6 7 8 val poem = """ >郎骑竹马来, > 绕床弄青梅。 >同居长干里, > 两小无嫌猜。 """ .trimMargin(">" )println(poem)
when 相当于switch,不过用起来更方便。
1 2 3 4 5 6 7 fun getScore (name: String ) = when (name) { "张三" -> 85 "李四" -> { return 91 } else -> 0 }
区间 闭区间
左闭右开区间
降序闭区间
循环 for - in 循环 1 2 3 4 5 6 7 8 9 10 for (i in 0 until 10 ) { println(i) } for (i in 0. .10 step 2 ) { println(i) }
构造函数 Kotlin中有主次构造函数之分。
主构造函数没有函数体,直接定义在类名的后面。若想在主构造函数中编写一些逻辑,将代码写在init中:
1 2 3 4 5 6 7 class Student (val name, val grade) { init { } } Student('张三' , 86 )
次构造函数有函数体,且必须调用主构造函数。
1 2 3 4 5 6 7 8 9 10 11 class Student (val name, val grade) { init { } constructor (name: String) : this (name, 0 ) { } } Student('张三' , 86 )
继承 Kotlin中非抽象类默认都是不可以被继承的,相当于给类加上了final。要声明为可继承类,需要加上open关键字。
同时子类中的构造函数必须调用父类中的构造函数。
1 2 3 4 5 6 7 8 9 10 11 open class Person (val name) { } class Student (val name, val grade) : Person(name) { init { } } Student('张三' , 86 )
接口 1 2 3 4 5 6 7 8 9 10 interface Rectangle { fun getPerimeter () fun getArea () } class Square (edge: Double ) : Shape(), Rectangle { override fun getPerimeter () = edge * 4 override fun getArea () = edge * edge }
单例 Java中实现单例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Singleton { private static Singleton instance; private Singleton () {} public synchronized static Singleton getInstance () { if (instance == null ) { instance = new Singleton (); } return instance; } public void singletonTest () { System.out.println("singletonTest方法被调用" ); } } Singleton singleton = Singleton.getInstance();singleton.singletonTest();
Kotlin中实现单例十分简便,只需要将关键字class改为object即可:
1 2 3 4 5 6 7 object Singleton { fun singletonTest () { println("singletonTest is called." ) } } Singleton.singletonTest()
拓展函数 我们可以为某一个数据类型添加自定义的方法。
举个例子,如对时间进行格式化,一种普通做法是写一个工具类,用伴生对象实现类似静态方法的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class DateTimeUtil { companion object { private const val DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss" fun format (dateTime: LocalDateTime ) : String { val formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT) return formatter.format(dateTime) } } } fun main () { val dateTime = LocalDateTime.now() println("$dateTime " ) val formattedDateTime = DateTimeUtil.format(dateTime) println("$formattedDateTime " ) }
除此之外还有另一种更为优雅的方法:
1 2 3 4 5 6 7 8 9 10 11 fun LocalDateTime.format () : String { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ) return formatter.format(this ) } fun main () { val dateTime = LocalDateTime.now() println("$dateTime " ) println(dateTime.format()) }
这里调用的format函数是我们自定义的。
kotlin与dart都有这种特性。
Kotlin的拓展函数只是语法糖,本质是静态解析,编译期根据声明类型决定调用哪个函数。
可见性
List、Set、Map 1 2 3 4 5 6 7 8 9 10 11 12 13 14 val list = listOf("春秋左氏传" , "春秋公羊传" , "春秋谷梁传" ) val mutableList = mutableListOf("诗经" , "尚书" , "礼记" , "易经" ) mutableList.add("春秋" ) val set = setOf("白起" , "王翦" , "廉颇" , "李牧" )val mutableSet = mutableSetOf("齐桓公" , "晋文公" , "秦穆公" , "宋襄公" )mutableSet.add("楚庄王" ) val map = mapOf("儒" to "孔子" , "法" to "韩非子" , "墨" to "墨子" , "道" to "庄子" )val mutableMap = mutableMapOf("齐" to "临淄" , "楚" to "郢都" , "燕" to "蓟城" , "韩" to "新郑" , "赵" to "邯郸" , "魏" to "大梁" )mutableList.add("秦" to "咸阳" )
Lambda表达式 本质是匿名函数,即可当做值来传递的一段代码。
完整结构:val lambda = { param1: Type, param2: Type -> 函数体 },最后一行表达式的值作为整个Lambda表达式的返回值。
1 2 3 4 5 6 7 8 9 10 button.setOnClickListener({ data -> println(data ) }) button.setOnClickListener() { data -> println(data ) } button.setOnClickListener { data -> println(data ) } button.setOnClickListener { println(it) }
inline & noinline & crossinline(TODO) Lambda的实现有两种情况:普通的非inline和inline。非inline的Lambda,本质是生成了一个匿名的函数对象,包含了Lambda中的方法,因此有创建对象的性能开销。inline则是编译期间直接展开代码。
Q:与Java中的Lambda区别?
A:
Java的Lambda本质是实现接口,而Kotlin的Lambda是一个函数对象。
另外Kotlin的Lambda可以非局部返回,如:
1 2 3 4 5 fun test () { listOf(1 , 2 , 3 ).forEach { return } }
Java则不行。
数据类 数据类的copy方法是浅拷贝,只是复制引用,没有递归复制对象,深拷贝需要自行实现。
协程 Kotlin Jetpack 实战 | 09. 图解协程原理
别再死记硬背了!用‘红绿灯’和‘存档读档’理解Kotlin协程的挂起与恢复
协程是用户态 的调度单元,不由操作系统调度,可以在一个线程上运行多个协程。线程运行在内核态,协程运行在用户态。
suspend函数可以挂起,挂起不是阻塞,线程不会停。挂起就是把函数拆成多个状态,通过Continuation串起来执行。
编译后的suspend函数内部基于状态机实现,Kotlin协程在编译期会把suspend函数转换为一个状态机。在launch {...}协程启动时会创建一个Continuation对象,用于保存恢复执行所需的所有上下文,最终触发invokeSuspend进入状态机。通过一个label变量记录状态机的分支状态,随后根据label状态决定执行when的哪个分支。例如刚开始label = 0进入第一个分支,则令label = 1,执行到耗时操作如IO时,协程用withContext(Dispatchers.IO) {...}(Dispatchers.IO是一个线程池调度器)将IO操作调度到IO线程池中的某个线程执行。同时当前挂起函数返回CoroutineSingletons.COROUTINE_SUSPENDED,表示协程挂起,让线程继续执行其他任务。当异步任务完成后,再通过Continuation.resumeWith(Result)触发挂起函数,将之前的continuation传入复用恢复上下文,再根据label跳转到对应分支执行后续的代码。
协程的整个过程本质是CPS(Continuation Passing Style)转换,将同步代码拆分成多个状态执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 viewModelScope.launch { logUserFrinedList() } suspend fun logUserFrinedList () { log("start" ) val user = getUser() log(user) val friends = getFriends(user) log(friends) } suspend fun getUser () : User { return suspendCancellableCoroutine { cont -> api.getUser().enqueue(object : Callback<User> { override fun onResponse (call: Call <User >, response: Response <User >) { cont.resume(response.body()!!) } override fun onFailure (call: Call <User >, t: Throwable ) { cont.resumeWithException(t) } }) } } suspend fun getUser (user: User ) ...
上面的logUserFrinedList()方法进行编译后,变为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 fun logUserFrinedList (completion: Continuation <Any ?>) : Any? { lateinit var user: User lateinit var friends var result = continuation.result var suspendReturn: Any? = null val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED val continuation = if (completion is LogUserFrinedList) { completion } else { LogUserFrinedList(completion) } class LogUserFrinedList (completion: Continuation<Any?>?) : ContinuationImpl(completion) { var label: Int = 0 var result: Any? = null var mUser: Any? = null var mFriends: Any? = null override fun invokeSuspend (_result: Result <Any ?>) : Any? { result = _result label = label or Int .Companion.MIN_VALUE return logUserFrinedList(this ) } } when (continuation.label) { 0 -> { throwOnFailure(result) log("start" ) continuation.label = 1 suspendReturn = getUser(continuation) if (suspendReturn == sFlag) { return suspendReturn } else { result = suspendReturn } } 1 -> { throwOnFailure(result) user = result as User log(user) continuation.mUser = user continuation.label = 2 suspendReturn = getFriends(user, continuation) if (suspendReturn == sFlag) { return suspendReturn } else { result = suspendReturn } } 2 -> { throwOnFailure(result) user = continuation.mUser as User friends = continuation.mFriends loop = false } } return Unit }
Q:为什么suspend必须在协程中使用?
A:
suspend函数依赖Continuation,而这个对象是由协程提供的,因此suspend函数必须在协程中调用。
Q:为什么采用Continuation而不是回调?
A:
如果用传统的回调方式,每个异步操作都需要创建新的回调对象,且需要手动保存需要的局部变量,参数需要一层层带着。而用Continuation则整个协程只创建一个Continuation对象,编译器会自动提升局部变量到Continuation对象,比用回调方式更加高效便利。
Q:线程切换是怎么实现的?
A:
withContext会挂起当前协程,并在新的Dispatcher上恢复执行,切线程本质是恢复发生在另一个Dispatcher上。
Q:Thread.sleep()和delay()的区别
A:
Thread.sleep()是让线程停住,而delay()是挂起,不是阻塞,线程可以干别的,只是这段代码暂停。
协程的取消 TODO
协程的生命周期 & Job TODO
协程中的线程安全 Q:为什么协程可以安全更新UI?
A:
Dispatchers.Main保证在主线程执行。