Kotlin 使用笔记(进行中...)

Catalogue
  1. 1 单例模式
  2. 2 代码块
  3. 3 访问限制符
  4. 4 内联函数
  5. 5 委托
  6. 6 构造器执行顺序
  7. 7 作用域函数
  8. 8 lambda表达式、匿名函数以及函数类型三者之间的联系与区别
  9. 9 理解「带接收者类型的函数类型 」
  10. 10 闭包
  11. 11 平台类型
  12. 12 协程
  13. 13 如何跳出循环和跳出当前循环体

本篇文章将长期记录并解答笔者在学习使用 Kotlin 过程中所遇到的问题点,参考资料会以 扔物线 老师的 码上开学系列教程Kotlin 官方教程 为主,同时也会配合参考其他优秀文章。

1 单例模式

Java 的单例模式通常实现起来稍显繁琐,包含大量的模板代码,具体使用可参考这篇文章
而 Kotlin 实现单例模式是非常便捷的,只需使用 object 关键字即可。

1
2
3
4
5
6
7
// 👇 class 替换成了 object
object A {
val number: Int = 1
fun method() {
println("A.method()")
}
}

通过 Android studio 转换工具将上面代码转成 Java 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class A {
private static final int number = 1;
public static final A INSTANCE;

public final int getNumber() {
return number;
}

public final void method() {
String var1 = "A.method()";
boolean var2 = false;
System.out.println(var1);
}

static {
A var0 = new A();
INSTANCE = var0;
number = 1;
}
}

可以看到,实际上这种通过 object 实现的单例是一个饿汉式的单例,并且实现了线程安全。

2 代码块

Java 一共有四大代码块,可参考这篇文章,需要注意的是,文章中最后提到的同步代码块中对静态代码块的解释有误,如果要了解 java 中的同步代码块知识,可以移步到笔者之前写的这篇文章
Kotlin 中的构造代码块和静态代码块有了一些变化,先来看构造代码块。
Java 是这样写的:

1
2
3
4
5
6
7
public class User {
{
// 初始化代码块,先于下面的构造器执行
}
public User() {
}
}

而 Kotlin 是这样写的:

1
2
3
4
5
6
7
class User {
init {
// 初始化代码块,先于下面的构造器执行
}
constructor() {
}
}

对于静态代码块, Java 是这样的:

1
2
3
4
5
public class Sample {
static {
...
}
}

而 Kotlin 是这样写的:

1
2
3
4
5
6
7
class Sample {
companion object {
init {
...
}
}
}

3 访问限制符

Java 的访问限制符可以参考这篇文章访问控制修饰符部分。其中,如下几个知识点需要注意一下:

1、子类与基类在同一包中:被声明为 protected 或 default 的变量、方法和构造器能被同一个包中的任何其他类访问。
2、子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的 protected 方法。同时,在子类中既不能访问其从基类继承而来的 default 方法,也不能访问基类实例的 default 方法。(变量、构造器同理。)
3、子类可以对从父类继承的方法加宽访问范围。访问控制符的访问范围有大到小排序是:public > protect > default > private。

对于Kotlin的访问控制符,直接参考码上开学吧。

4 内联函数

首先我们来看使用内联函数的一个例子:

1
2
3
4
5
6
7
8
inline fun log() {
println("log1")
println("log2")
}

fun main(args: Array<String>) {
log()
}

反编译成 Java 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static final void log() {
String var1 = "log1";
System.out.println(var1);
var1 = "log2";
System.out.println(var1);
}

public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var1 = "log1";
System.out.println(var1);
var1 = "log2";
System.out.println(var1);
}

而我们去掉 inline 关键字后,再反编译成 Java 代码,如下:

1
2
3
4
5
6
7
8
9
10
11
public static final void log() {
String var0 = "log1";
System.out.println(var0);
var0 = "log2";
System.out.println(var0);
}

public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
log();
}

如上可以看到,使用内联函数可以减少一层函数的调用栈,但如果有多个地方调用内联函数的话,就会隐式地增加编译后代码的行数。那么如果正确使用内联函数呢,或者说它到底有什么用?有如下几个使用场景:

1、函数参数包含函数类型的函数。

1
2
3
4
5
class View {
inline fun setOnClickListener(listener: (View) -> Unit) {
listener(this)
}
}

如果不使用内联函数,由于 setOnClickListener 使用的是函数类型的参数,那么在调用时会产生一个额外的对象,具体的过程可以反编译成 Java 文件后看看。

2、泛型具体化。
平时我们使用泛型时,是不能直接对泛型类型进行操作的。比如下面的操作会导致编译不通过:

1
2
3
fun <T> excute() {
func(T::class.java) //编译器报错:Cannot use 'T' as reified type parameter. Use a class instead.
}

大意是:不能使用泛型 T 作为具体的类型参数。如果依照 Java 的解决方式,可以这样写:

1
2
3
fun <T> excute(class: Class<T>) {
func(class)
}

不过, Kotlin 中有更加方便的用法,也就是使用 inline 关键字达到让泛型具体化的目标:

1
2
3
4
inline fun <reified T> excute() {
func(T::class.java)
}
//PS:使用时还需在泛型声明前加上 refied 关键字,才能让泛型真正具体化。

Retrofit 最新拓展中,内部已经实现了一个内联函数用来代替旧版本创建实例的方式。具体可参考 Retrofit.create() 拓展方法。

5 委托

参考地址>>
上面的文章中,笔者认为只需要理解从开篇到“把属性储存在映射中”部分的内容,剩下的内容读者自行斟酌其重要性吧。

6 构造器执行顺序

Kotlin 的构造器执行顺序和 Java 一致,可参考这篇文章>>

7 作用域函数

  • 返回自身
    applyalso中选
    1.作用域中使用 this 作为参数,选择 apply。
    2.作用域中使用 it 作为参数,选择 also。

  • 不需要返回自身
    runlet中选择
    1.作用域中使用 this 作为参数,选择 run。
    2.作用域中使用 it 作为参数,选择 let。

apply 适合对一个对象做附加操作的时候。
let 适合配合空判断的时候。
with 适合对同一个对象进行多次操作的时候。它的最大的作用是可以返回任何对象,这是其它几个作用域函数不能实现的。官方详解>>

8 lambda表达式、匿名函数以及函数类型三者之间的联系与区别

官方文档参考>>

9 理解「带接收者类型的函数类型 」

概念:官方文档(只需看相关的)>>
实践(来自 Android KTX Core):SharedPreferences 源码>>

10 闭包

其实闭包并不是 Kotlin 中的新概念,在 Java 8 中就已经支持。我们以 Thread 为例,来看看什么是闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个 Thread 的完整写法
Thread(object : Runnable {
override fun run() {
...
}
})

// Runnable 是函数式接口,可以使用 lambda 表达式简化为
Thread({
...
})

// 使用闭包原则,再简化为
Thread {
...
}

形如 Thread {…} 这样的结构中 {} 就是一个闭包

在 Kotlin 中有这样一个语法糖:当函数的最后一个参数是 lambda 表达式时(又名:拖尾 lambda 表达式),可以将 lambda 写在括号外。这就是它的闭包原则

在这里需要一个类型为 Runnable 的参数,而 Runnable 是一个 Java 接口,且只定义了一个函数 run,这种情况可以转换成传递一个 lambda 表达式(第二段),因为是最后一个参数,根据闭包原则我们就可以直接写成 Thread {…}(第三段) 的形式。

注意:
Kotlin 对于“函数式接口可以使用 lambda 表达式”这个用法仅仅是针对接口是 Java 类型的情况,如果接口是 Kotlin 写的,则在 Kotlin 中调用以接口作为参数的方法时必须通过关键字’object’创建匿名类的对象来作为其参数。

参考链接>>

11 平台类型

在 kotlin 中使用其他平台(如:Java)的类型时,如果该类型未用可空性注解标注,则 Kotlin 会自动将这其解释成平台类型。

在类型后面加上一个感叹号的类型就是平台类型,平台类型不能手动声明。

Kotlin 对平台类型的空检测会放宽, 因此它们的安全保证与在 Java 中相同,即要空检查。

官方文档(看到可空性注解)>>

12 协程

使用请参考 码上开学

源码解析请参考如下两篇文章:
1、Kotlin Primer·第七章·协程库(上篇)
2、Kotlin Primer·第七章·协程库(中篇)

另外,为了加深源码以及使用的理解,可配合下面知乎的三篇文章阅读:
1、Kotlin协程启动模式
2、Kotlin协程调度
3、Kotlin协程生命的尽头—协程取消

协程 + ViewModel 使用案例:
1、Retrofit加kotlin协程为何如此优雅
2、Retrofit+协程+Jetpack架构组件实现简单网络请求

PS:链接2的文章原标题提到 MVVM ,此表述有误。 MVVM 中的 ViewModel 和官方提供的 ViewModel 本质上是不同的概念。

源码阅读建议
首先,通过如下两篇文章了解Lifecycle的使用:
文章1
文章2
之后,依据 LiveData、ViewModel、Retrofit 以及协程之间搭配使用的案例,可以仔细的去看下源码(还是很好理解的),这对于加深理解很有帮助。主要侧重几个点:
1、LiveData 是如何监听数据改变的?setValue 和 postValue 的区别?
2、ViewModel 为什么能在屏幕发生旋转后,仍然能够拿到原来的 ViewModel 实例?
3、ViewModel 是如何与界面的生命周期绑定的?
4、viewModelScope 为什么能在界面销毁时自动取消任务?
5、Retrofit 2.6 为什么能与协程一起工作?
6、Retrofit 使用协程以后如何在界面销毁自动取消请求?

13 如何跳出循环和跳出当前循环体

forEach 中如何跳出循环和跳出当前循环体>>