参考:码上开学
val 「只读变量」 var 「可变变量」
基本类型拆装箱
Kotlin 里的 Int 和 Java 里的 int 以及 Integer 不同,主要是在装箱方面不同。
1.Java 里的 int 是 unbox 的,而 Integer 是 box 的:
☕️
int a = 1;
Integer b = 2; // 👈会被自动装箱 autoboxing
Java
Kotlin 里,Int 是否装箱根据场合来定:
🏝️
var a: Int = 1 // unbox
var b: Int? = 2 // box
var list: List<Int> = listOf(1, 2) // box
2.Java 中的数组和 Kotlin 中的数组的写法也有区别:
☕️
int[] array = new int[] {1, 2};
Java
而在 Kotlin 里,上面的写法是这样的:
🏝️
var array: IntArray = intArrayOf(1, 2)
// 👆这种也是 unbox 的
Java 里的基本类型,类比到 Kotlin 里面,条件满足如下之一就不装箱:
- 1.不可空类型。
- 2.使用 IntArray、FloatArray 等。
Kotlin 处理强转错误的方式
🏝️
fun main() {
var activity: Activity = NewActivity()
// 👇'(activity as? NewActivity)' 之后是一个可空类型的对象,所以,需要使用 '?.' 来调用
(activity as? NewActivity)?.action()
}
思考:
1.子类重写父类的 override 函数,能否修改它的可见性? 2.以下的写法有什么区别?
🏝️
activity as? NewActivity
activity as NewActivity?
activity as? NewActivity?
🍎
@Test
fun test() {
val str = "hello"
//str as Context
//str as? Context
str as? Context?
/*
1.直接抛异常 : java.lang.ClassCastException: java.lang.String cannot be cast to android.content.Context
2.通过
3.通过
此时 : val str="hello" -> val str=null
str as? Context? 和 str 是等效的
*/
}
val自定义 getter
不过 val 和 final 还是有一点区别的,虽然 val 修饰的变量不能二次赋值,但可以通过自定义变量的 getter 函数,让变量每次被访问时,返回动态获取的值:
🏝️
👇
val size: Int
get() { // 👈 每次获取 size 值时都会执行 items.size
return items.size
}
不过这个属于 val 的另外一种用法,大部分情况下 val 还是对应于 Java 中的 final 使用的。
Kotlin 中的 object
- 创建一个类,并且创建一个这个类的对象。这个就是 object 的意思:对象。
- 创建单例 , 只需要把 class 换成 object 就可以了。
- object 实现的单例是一个饿汉式的单例,并且实现了线程安全。
- Java 中的静态初始化
static{ ... } 🏝️ Java 中的静态变量和方法,在 Kotlin 中都放在了 companion object 中。因此 Java 中的静态初始化在 Kotlin 中自然也是放在 companion object 中的,像类的初始化代码一样,由 init 和一对大括号表示: 🏝️ class Sample { 👇 companion object { 👇 init { ... } } }
那在实际使用中,在 object、companion object 和 top-level 中该选择哪一个呢?简单来说按照下面这两个原则判断:
- 如果想写工具类的功能,直接创建文件,写 top-level「顶层」函数。
- 如果需要继承别的类或者实现接口,就用 object 或 companion object。
Kotlin常量和Java常量的区别
发现不同点有:
Kotlin 的常量必须声明在对象(包括伴生对象)或者「top-level 顶层」中,因为常量是静态的。
Kotlin 新增了修饰常量的 const 关键字。
除此之外还有一个区别:
Kotlin 中只有基本类型和 String 类型可以声明成常量。
原因是 Kotlin 中的常量指的是 「compile-time constant 编译时常量」,它的意思是「编译器在编译的时候就知道这个东西在每个调用处的实际值」,因此可以在编译时直接把这个值硬编码到代码里使用的地方。
Java 中的常量可以认为是「伪常量」,因为可以通过上面这种方式改变它内部的值。而 Kotlin 的常量因为限制类型必须是基本类型,所以不存在这种问题,更符合常量的定义。
Kotlin mutable 前缀注意问题
listOf() 创建不可变的 List,mutableListOf() 创建可变的 List。可通过 toMutableList 由不可变转成可变集合:
🏝️
val strList = listOf("a", "b", "c")
👇
strList.toMutableList()
👉 toMutable*() 返回的是一个新建的集合,原有的集合还是不可变的,所以只能对函数返回的集合修改。
可见性修饰符
- public :公开,可见性最大,哪里都可以引用。
- private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。
- protected:保护,相当于 private + 子类可见。
- internal:内部,仅对 module 内可见。
Kotlin 中如果不写可见性修饰符,就表示公开,和 Java 中 public 修饰符具有相同效果。在 Kotlin 中 public 修饰符「可以加,但没必要」。
internal 表示修饰的类、函数仅对 module 内可见,这里的 module 具体指的是一组共同编译的 kotlin 文件,常见的形式有:
- Android Studio 里的 module
- Maven project
private Java 中的 private 表示类中可见,作为内部类时对外部类「可见」。 Kotlin 中的 private 表示类中或所在文件内可见,作为内部类时对外部类「不可见」
🌰
在 Java 中,外部类可以访问内部类的 private 变量:
☕️
public class Outter {
public void method() {
Inner inner = new Inner();
👇
int result = inner.number * 2; // success
}
private class Inner {
private int number = 0;
}
}
Java
在 Kotlin 中,外部类不可以访问内部类的 private 变量:
🏝️
class Outter {
fun method() {
val inner = Inner()
👇
val result = inner.number * 2 // compile-error: Cannot access 'number': it is private in 'Inner'
}
class Inner {
private val number = 1
}
}
高阶函数和Lambda表达式
课件 👉 https://kaixue.io/kotlin-lambda/ Bilibili 👉 https://www.bilibili.com/video/av96768809
概念
「参数或者返回值为函数类型的函数」,在 Kotlin 中就被称为「高阶函数」—— Higher-Order Functions。 ⭐ Kotlin中双冒号函数,匿名函数和Lambda表达式本质上都是函数类型的对象。
高阶函数
双冒号 :: 把声明好的函数变成函数类型的对象 :
fun b(param: Int): String {
return ""
}
val d = ::b
val e = d
对象是不能加个括号来调用的,对吧?但是函数类型的对象可以。为什么?因为这其实是个假的调用,它是 Kotlin 的语法糖,实际上你对一个函数类型的对象加括号、加参数,它真正调用的是这个对象的 invoke() 函数:
d(1) // 实际上会调用 d.invoke(1)
(::b)(1) // 实际上会调用 (::b).invoke(1)
包括双冒号加上函数名的这个写法,它是一个指向对象的引用,但并不是指向函数本身,而是指向一个我们在代码里看不见的对象。这个对象复制了原函数的功能,但它并不是原函数。
协程
一种线程框架。
创建协程
🏝️
// 方法一,使用 runBlocking 顶层函数
runBlocking {
getImage(imageId)
}
// 方法二,使用 GlobalScope 单例对象
// 👇 可以直接调用 launch 开启协程
GlobalScope.launch {
getImage(imageId)
}
// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
// 👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getImage(imageId)
}
-
方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
-
方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。
-
方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。
withContext
withContext 这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。
🏝️
launch(Dispatchers.Main) { // 👈 在 UI 线程开始
val image = getImage(imageId)
avatarIv.setImageBitmap(image) // 👈 执行结束后,自动切换回 UI 线程
}
// 👇
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
...
}
合并网络请求并更新UI
🏝️ 「协作式任务」
coroutineScope.launch(Dispatchers.Main) {
// 👇 async 函数之后再讲
val avatar = async { api.getAvatar(user) } // 获取用户头像
val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
val merged = suspendingMerge(avatar, logo) // 合并结果
// 👆
show(merged) // 更新 UI
}
项目中配置对 Kotlin 协程的支持
- 项目根目录下的 build.gradle :
buildscript {
...
// 👇
ext.kotlin_coroutines = '1.3.1'
...
}
- Module 下的 build.gradle :
dependencies {
...
// 👇 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
// 👇 依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
...
}
launch 与 async
接下来我们主要来对比 launch 与 async 这两个函数。
-
相同点:它们都可以用来启动一个协程,返回的都是 Coroutine,我们这里不需要纠结具体是返回哪个类。
-
不同点:async 返回的 Coroutine 多实现了 Deferred 接口。
关于 Deferred 更深入的知识就不在这里过多阐述,它的意思就是延迟,也就是结果稍后才能拿到。
我们调用 Deferred.await() 就可以得到结果了。
接下来我们继续看看 async 是如何使用的,先回忆一下上期中获取头像的场景:
🏝️
coroutineScope.launch(Dispatchers.Main) {
// 👇 async 函数启动新的协程
val avatar: Deferred = async { api.getAvatar(user) } // 获取用户头像
val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
// 👇 👇 获取返回值
show(avatar.await(), logo.await()) // 更新 UI
}
可以看到 avatar 和 logo 的类型可以声明为 Deferred ,通过 await 获取结果并且更新到 UI 上显示。
await 函数签名如下:
🏝️
public suspend fun await(): T
「挂起」的本质
一个稍后会被自动切回来的线程调度操作。
挂起的对象是协程。
从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。
suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。
我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:
1.线程
前面我们提到,挂起会让协程从正在执行它的线程上脱离,具体到代码其实是:
协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。
那线程接下来会做什么呢?
如果它是一个后台线程:
- 要么无事可做,被系统回收
- 要么继续执行别的后台任务
跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。
如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务。
一个常见的场景是,获取一个图片,然后显示出来:
🏝️
// 主线程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 获取图片
avatarIv.setImageBitmap(image) // 显示出来
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
这段执行在主线程的协程,它实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码:
🏝️
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
当这个协程被挂起的时候,就是主线程 post 的这个 Runnable 提前结束,然后继续执行它界面刷新的任务。
2.协程
它从 suspend 函数开始脱离启动它的线程,继续执行在 Dispatchers 所指定的 IO 线程。
紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。
这个「切回来」是什么意思?
我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;
当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。
「非阻塞式」挂起指的是用看起来阻塞的代码写出非阻塞式操作的。
协程练习
使用协程下载一张图,并行进行两次切割
- 一次切成大小相同的 4 份,取其中的第一份
- 一次切成大小相同的 9 份,取其中的最后一份
得到结果后,将它们展示在两个 ImageView 上。
GlobalScope.launch(Dispatchers.Main) {
Log.w("coroutines","launch ${Thread.currentThread().name}")
val bitmap = getSuspendImage()
Log.w("coroutines","launch getImage Ok")
iv1.setImageBitmap(bitmap)
val bitmapHalf2 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 2, bitmap.height / 2)
iv2.setImageBitmap(bitmapHalf2)
val bitmapHalf3 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 3, bitmap.height / 3)
iv3.setImageBitmap(bitmapHalf3)
}
private suspend fun getSuspendImage(): Bitmap = withContext(Dispatchers.IO) {
Log.w("coroutines","suspend ${Thread.currentThread().name}")
OkHttpClient()
.newCall(Request.Builder()
.url("http://oss.tuyuing.com/TUYU/trend/20190930/trend257401569854904487.jpeg")
.get()
.build())
.execute().body?.byteStream().use {
Log.w("coroutines","suspend getImage Ok")
BitmapFactory.decodeStream(it)
}
}
Log :
W/coroutines: launch main
W/coroutines: suspend DefaultDispatcher-worker-1
W/coroutines: suspend getImage Ok
W/coroutines: launch getImage Ok
效果图 :
扩展函数和扩展属性
扩展函数和扩展属性(Extension Functions / Properties), 在不改变既有代码的情况下扩展函数或扩展对象。
(Int::toFloat)(1)
Int::toFloat.invoke(1)
1.toFloat()
1.toFloat().invoke()
🌰 Global.kt
//扩展函数
fun String.method1(i: Int) {}
//扩展属性
val Float.dp
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
MainActivity.kt
val str: String.(Int) -> Unit = String::method1
val str2: (String, Int) -> Unit = String::method1
fun strTest() {
str("hello", 1)
str.invoke("hello", 1)
"".method1(1)
}
//also
fun strTest2() {
fun String.method1(i: Int) {
}
val str: String.(Int) -> Unit = String::method1
val str2: (String, Int) -> Unit = String::method1
"hello".method1(100)
}
其中val str2: (String, Int) -> Unit = String::method1
的 (String, Int)
第一个参数是调用者,往后是参数
注: Kotlin不允许使用既是成员函数又是扩展函数的函数。
此外 , 普通的无Receiver的函数也可以赋值给有Receiver的变量 :
Global.kt
fun method2(s:String,i: Int) {} 比 method1 多了一个 String 参数
MainActivity.kt
val str3: String.(Int) -> Unit = ::method2
val str4: (String, Int) -> Unit = ::method2
.