kotlin 分享入门

2019 年 Google I/O 大会宣布优先采用 kotlin 进行 Android 开发后,新项目的示例、组件也越来越多使用 kotlin 而非 java,目前正超过 60% 的专业 Android 开发者在使用,所以掌握 koltin 是 Android 开发者必备的能力之一。

一、为什么要会 kotlin

2011 年 7 月,JetBrains 推出面向 JVM 的语言 – Kotlin,2017年,Google I/O 大会宣布 Kotlin 成为 Android 开发的官方语言,2019 年宣布 kotlin 优先。 kotlin 语言的特点有:

  • kotlin 兼容 java,支持 Android 无缝切换 kotlin
  • 比 java 更安全,能够静态检测常见问题,如 NPE 等
  • 比 java 更简洁,可以用更少的代码来实现更多的功能

Google Home 将新功能开发工作迁移到 Kotlin 后,代码库大小减少了 33%,NPE 崩溃次数减少了 30%。

关于 java 切换 kotlin 的原因,可能还有 java 的版权和专利问题,2010 年 Oracle 收购 Sun(含有 java 版权和专利)后,对 Google 提起了 java 专利的诉讼,经过常达十年多的官司,最终在 2021 年 Google 胜诉,对 java 代码的使用是 “合理使用”,其次 Google 对 jdk 的依赖也从 oracle jdk 切换到了开源的 open jdk。

二、kotlin 的基本语法

1、基本语法

1)基本类型

java 定义变量需要指定具体类型,并把类型放在变量之前:

1
2
String str = "nullableStr";
final String finalStr = "notnullFinalStr";

kotlin 定义变量用 val 和 var 声明,具体变量类型可放在变量名后用 : 类型 定义,甚至可不指定由赋值推导类型。val 只允许赋值一次,用 var 可重新赋值,其次代码结尾去除 ;

1
2
3
4
5
6
// var 可变,val 不可变
// 先声明变量,再声明类型,用冒号分隔,类型可缺省由赋值推导
// 结尾不用加分号
var nullableStr: String? = "nullableStr"
var notnullStr: String = "notnullStr"
val notnullFinalStr = "notnullFinalStr" // val 相当于 finnal var

2) 字符串模版

java 字符串拼接需要用 +String.format 占位符拼接:

1
Log.log(TAG + ", testBasic:" + ",nullable:" + (str == null ? "" : str.substring(1, str.length())) + ", notnull:" + finalStr.substring(1, finalStr.length()));

kotlin 在字符串内直接编写表达式,使用 $ 拼接变量或用 {} 拼接表达式,提高代码可读性:

1
Log.log("$TAG testBasic: nullable: ${nullableStr!!.substring(1,nullableStr.length)}, notnull:${notnullFinalStr.substring(1, notnullFinalStr.length)}"

3)NPE 问题解决

在 java 语言中,空指针是最常见的问题之一,无法确定外部调用的值,即使返回非空也只能通过注解的方式让调用方知悉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Nullable
public String getTestNameNullable(@Nullable TestData testData) {
if (testData != null) {
TestEntity testEntity = testData.getEntity();
if (testEntity != null) {
return testEntity.getName();
}
}
return null;
}

@NotNull
public String getTestName(@Nullable TestData testData) {
if (testData != null) {
TestEntity testEntity = testData.getEntity();
if (testEntity != null) {
return testEntity.getName();
}
}
return "";
}

由于注解传值是不影响调用方编译阶段的,而运行阶段出现的空指针不一定在必现的场景,因此把空指针控制在静态编译期间将非常有效减少空指针的产生。

kotlin 对于可空的值必须用 ? 来表示,否则传的一定是非空值,不然静态编译会提示错误。

1
2
3
4
5
6
7
fun getTestNameNullable(testData: TestData?): String? {
return testData?.entity?.name
}

fun getTestName(testData: TestData?): String {
return testData?.entity?.name ?: ""
}
  • ?.操作符:代替.操作符,对 nullable 类型做安全的解引用,如果 receiver 为 null 则直接返回 null,什么也不做。

  • ?:操作符:相当于常用的三元操作符,nullable ?: other 等价于 nullable != null ? nullable : other。

4)懒加载

对于一些变量的声明时,如果它是可空的,后续想使用时,可以使用 ?. 进行调用,如果确定它是非空的,可以用 !! 忽略编译检查,但调用处处弥漫着 !! 让代码非常丑陋。

1
2
3
4
5
6
private var testVar: String? = null

fun testBasic() {
testVar = "testVar"
testVar!!.substring(1, testVar!!.length)
}

如果我们非常明确它是非空的,但需要在之后的初始化方法里才能赋值,此时又不想声明可空的 ?!! 调用,那我们可以在声明时加上懒加载标志,让它在编译期间被忽略,当成非空处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private lateinit var testLazyVar: String  // 忽略编译检查,由开发者决定
private val testLazyVal by lazy {
// 第一次调用时初始化赋值,后续只是get该值
Log.log("testLazyVal")
"testLazyVal"
}

fun testBasic() {
// var lazy init
testLazyVar = "testLazyVar"
testLazyVar.substring(1, testLazyVar.length)

// val lazy init
testLazyVal.subSequence(1, testLazyVal.length)
testLazyVal.subSequence(1, testLazyVal.length)
}

懒加载对声明两种变量的使用:

  • var:=> lateinit var

  • val:=> val by lazy {},表达式内的只会在第一次调用时执行,之后只是调用变量的 get 获取

5)控制流

java switch 支持基本数据类型、枚举、字符串,但只允许常量,不允许变量判断:

1
2
3
4
5
6
7
8
9
10
11
12
String type = "1";
switch (type) {
case "1":
Log.log(TAG + ", testControlStatement:" + type);
break;
case "2":
Log.log(TAG + ", testControlStatement:" + type);
break;
default:
Log.log(TAG + ", testControlStatement:" + type);
break;
}

kotlin 没有 switch,但新增了 when else, 它可以覆盖任意类型的分支判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// when else 任意类型判断
val type = "1"
when (type) {
"1" -> Log.log("$TAG testControlStatement:$type")
"2" -> Log.log("$TAG testControlStatement:$type")
else -> Log.log("$TAG testControlStatement:$type")
}

// 表达式判断,不需要一定是常量
val typeInt = 1
when (typeInt) {
parseType1(typeInt) -> {
Log.log("$TAG testControlStatement:$type")
}
in 2..10 -> Log.log("$TAG testControlStatement:$type")
else -> Log.log("$TAG testControlStatement:$type")
}

if else 也可作为表达式返回赋值给变量:

1
val maxExpression = if (isTrue()) 1 else 2

2、集合

对于集合的使用上,kotlin 提供了 stream 链式调用和一系列的高阶扩展函数极大的简化操作,我们以 List 举例:

1)遍历 List<TestData> 获取每个 TestData 属性 TestEntity 的属性 name

2)获取 List<TestData> 中每个 TestData 属性 id 并组成新的 list

3)获取 List<TestData> 中每个 TestData 属性 id 大于 2 的列表部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<TestData> list = Arrays.asList(new TestData(new TestEntity("1-1"), 1), new TestData(new TestEntity("2-2"), 2), new TestData(new TestEntity("3-3"), 3));
for (TestData data : list) {
if (data.getEntity() != null && data.getEntity().getName() != null) {
Log.log(data.getEntity().getName());
}
}

List<Integer> idList = new ArrayList<>();
for (TestData data : list) {
idList.add(data.getId());
}

List<TestEntity> nameUp2EntityList = new ArrayList<>();
for (TestData data : list) {
if (data.getId() >= 2 && data.getEntity() != null) {
nameUp2EntityList.add(data.getEntity());
}
}

可以看到 java 遍历中需要手动一个一个的遍历获取,在 java8 引入了 stream 后,对 forEachfiltermap 也变得更简洁。

在 kotlin 中,流式调用可以如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// stream api 调用
val list = listOf<TestData>(
TestData(TestEntity("1-1"), 1),
TestData(TestEntity("2-2"), 2),
TestData(TestEntity("3-3"), 3)
)
// 遍历
list.forEach { Log.log(it.entity?.name ?: "") }

// list 转换
val idList = list.map { it.id }

// list 按值转换取出
val nameUp2EntityList = list.filter { it.id >= 2 }.map { it.entity }

3、函数

kotlin 函数相比 java,需要使用 fun 声明,并增加了默认参数,? 的属性在调用时可缺省,当然非缺省的必须放在缺省的前边:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun testShowDialog(
title: String,
content: String? = null,
leftText: String? = null,
rightText: String? = null
) {
Log.log("title:$title, content:$content, leftText:$leftText, rightText:$rightText")
}

override fun testFunction() {
// 函数重载
// 1、默认参数
// 2、具名参数
testShowDialog("titles")
testShowDialog("titles", "contents")
testShowDialog("titles", leftText = "leftTexts")
testShowDialog("titles", rightText = "leftTexts")
}

java 声明时需要使用多个重载调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void testShowDialog(String title) {
testShowDialog(title, null);
}

private void testShowDialog(String title, String content) {
testShowDialog(title, content, null);
}

private void testShowDialog(String title, String content, String leftText) {
testShowDialog(title, content, leftText, null);
}

private void testShowDialog(String title, String content, String leftText, String rightText) {
Log.log("title:" + title + ", content:" + content + ", leftText:" + leftText + ", rightText:" + rightText);
}

可见 kotlin 带缺省的函数,可有效减少函数重载多的问题。

4、类

1)类声明

java 类的静态变量一般直接声明和调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestJava implements ITest {

public static final String TAG = "TestJava";

public TestJava() {

}

public static TestJava getInstance() {
return Helper.instance;
}

private static class Helper {
static TestJava instance = new TestJava();
}
}

kotlin 使用 companion 伴生对象来表示用类名作为限定符调用 TestKotlin.TAG, 如果想成为真正的静态方法和字段,需添加 @JvmStatic 等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestKotlin : ITest {

companion object {
val TAG = "TestKotlin"

@JvmStatic
fun getInstance(): TestKotlin = Helper.instance
}

private object Helper {
val instance = TestKotlin()
}
}

可见性修饰,相比 java 多了 internal,表示模块内可见,非导出的 library 则不可访问,非常方便 sdk 用于访问符限制。

2)数据类

在存储操作时,经常会使用数据类,但为了减少外部修改用提供的 getset, 会让类代码变得繁琐。同时对于类的一些方法 equalhashCodetoString 都需要开发者重写,否则表现会不太符合预期:

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
class TestData {
private int id;
private TestEntity entity;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public TestEntity getEntity() {
return entity;
}

public void setEntity(TestEntity entity) {
this.entity = entity;
}

@Override public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TestData testData = (TestData) o;
return id == testData.id &&
Objects.equals(entity, testData.entity);
}

@Override public int hashCode() {
return Objects.hash(id, entity);
}

@Override public String toString() {
return "TestData{" +
"id=" + id +
", entity=" + entity +
'}';
}
}

对于 kotln 有专门的数据类表示,用 data 修饰,所有属性都默认提供了 settergetter

1
2
3
4
5
6
7
8
9
10
11
12
13
data class TestData(
val entity: TestEntity?,
val id: Int = 0
) {

var name = "Mike"
get() {
return field + "_nb"
}
set(value) {
field = value + "_text"
}
}

同时对数据类的主构造函数提供了 toStringequlas/hasCode 的实现:

  • toString() 格式是 TestData(entity=TestEntity(name=ivan), id=1)
  • equals()/hashCode() 实现了 entitiyid 属性的比较和映射

  • 提供了 componentN() 的解构声明,可以直接从类中获取解构的属性,如 val(entity, id) = testData

3)扩展类

扩展使得 kotlin 在类使用时变得非常灵活,在 Java 中想扩展一个类的新功能时,一般使用继承或者装饰等方式。但如果想为一个第三方类或系统类增加新的函数,就像调用原始类一样调用该方法,就可以使用 kotlin 的扩展属性:

1
2
3
4
5
6
7
8
data class TestData(
val entity: TestEntity?,
val id: Int = 0
) {}

public fun TestData.getMapId(): String {
return "${1000 + id}"
}

经过外部声明 TestData 的扩展函数 getMapId 后,像普通函数调用方式使用:

1
2
3
4
// 类扩展函数
val data = TestData(TestEntity(""), 1)
val mapId = data.getMapId()
Log.log("testClass id:${data.id} mapId:$mapId")

扩展的方法并不会破坏现有的类方法,它是静态分发的,通过该类型的变量用点的表达式去调用这个新函数。

三、kotlin 的高阶用法

1、高阶函数

kotlin 的高阶函数是将函数用作参数或返回值的函数,可以用高数里的高阶导数类比:

高阶导数:y=f(x) ,求导后的 y=f’(x),仍然是x的函数,则二阶函数。把≥2阶的导数认为是高阶导数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ViewKotlin {
fun setOnClickListener(onClick: (v: ViewKotlin) -> Unit) {
onClick(this)
}
}

// 高阶函数:函数的参数或返回值为函数
// A -> B ,在 B 执行某个方法后,给 A 一个回调
val viewKotlin = ViewKotlin()
viewKotlin.setOnClickListener({ v: ViewKotlin ->

})
// 匿名函数是一个函数式对象,传参或赋值
viewKotlin.setOnClickListener(click)

比如示例,把 onClick 函数作为 setOnClickListener 的返回值,则认为它是高阶函数。但其实函数并不能传递,传递的是函数式的对象(匿名函数/lambda)。

2、lambda 用法

lambda 表达式总是括在花括号中, 完整语法是参数声明放在花括号内,并有可选的类型标注, 函数体跟在 -> 符号之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// lambda 表达式
val sum = { x: Int, y: Int ->
x + y
}
Log.log("testLambda: ${sum(1, 2)}")

val view = View()
view.setOnClickListener(null, { views ->
})
// 拖尾 lambda 表达式: 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外
view.setOnClickListener(null) { views ->
}
// 也可直接忽略,用 it 代值
view.setOnClickListener(null) {
}
// 当只有一个参数,且该参数是函数时,可以直接忽略括号
view.setOnClickListener {
}

lambda 表示式的变形:

  • 拖尾 lambda 表达式: 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外

3、协程

协程:一种编程思想,解决的是并发问题,让协作式任务实现起来更方便,Kotlin-JVM 的协程:是基于 Java Thread API 实现的一种更上层的工具。

我们以一个异步请求任务举例,java 需要通过异步回调来进行,如果有多个依赖关系的回调,最终代码的嵌套会非常深:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 做一个网络请求,一般需要 callback 方式或阻塞式IO的同步调用
// 主线程做一个网络请求
sendRequest(new Callback<String>() {
@Override
public void onSuccess(String data) {
// 还需要切回主线程
refreshUI(data);
}

@Override
public void onFail(int errCode, String errMsg) {

}
});

在 kotlin 中,则使用同步代码的方式来执行异步请求,对于依赖关系的几个异步回调也变得非常简单:

1
2
3
4
5
6
7
8
9
10
val coroutineScope = CoroutineScope(Dispatchers.Main + Job())
coroutineScope.launch(Dispatchers.Main) { // launch: 创建一个新的协程,并在指定的线程上运行它

launch (Dispatchers.IO){
val result = sendRequestFor("1-coroutineScope")
launch (Dispatchers.IO){
val result2 = sendRequestForTwo("1-coroutineScope")
refreshUI(result + result2)
}
}

此时可以用 withContext 简化切换线程的操作,它切换到指定的线程,在代码块执行完后把结果带回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 子线程网络请求
// 协程在执行到有 suspend 标记的函数的时候,会被挂起,就是切个线程,稍后会被自动切回来的线程调度操作
private suspend fun sendRequestFor(work: String): String = withContext(Dispatchers.IO)
{
delay(1000L)
Log.log("testCoroutines $work sendRequestFor: ${Thread.currentThread().id}")
"success work"
}

val resultWith = withContext(Dispatchers.IO) {
sendRequestFor("2-coroutineScope")
}
val resultWith2 = withContext(Dispatchers.IO) {
sendRequestForTwo("2-coroutineScope")
}
refreshUI(resultWith + resultWith2)

对于一些没有依赖关系的任务1和任务2,允许他们并行执行时,可以使用关键字 async 标志,在需要串行等待时使用 await 进行等待:

1
2
3
4
5
6
7
8
9
10
11
val coroutineScope = CoroutineScope(Dispatchers.Main + Job())
coroutineScope.launch(Dispatchers.Main) { // launch: 创建一个新的协程,并在指定的线程上运行它
val result = sendRequestFor("3-coroutineScope") // 网络请求:IO 线程
val result2 = sendRequestForTwo("3-coroutineScope") // 网络请求:IO 线程
refreshUI(result + result2) // 更新 UI:主线程

val one = async {sendRequestFor("4-coroutineScope")} // 网络请求:IO 线程
val two = async { sendRequestForTwo("4-coroutineScope")} // 网络请求:IO 线程
val result4 = one.await() + two.await()
refreshUI(result4) // 更新 UI:主线程
}

可以看到协程让并发协作式的任务变得非常简洁,达到了顺序式编程,消灭了回调。

四、对比展望

1、kotlin & java 对比

kotiln 在 Android 中使用,只需要引入 kotlin-android 扩展库,几个对比如下:

  • 体积增长:kotin 标准库 600K,但是大部分代码是对 java 的扩展,以及 inline function,而且可以混淆。在 proguard strip 之后,几乎没有增长。

  • 性能影响:Kotlin是静态强类型语言,性能不弱于 Java,考虑到 inline function/lambda 的影响(将 lambda 本该生成匿名类的地方用 inline 方式将代码块放在调用的地方),Kotlin的性能稍微比 java 更好。

  • 编译速度:Google一直在优化编译器,使生成的字节码质量更高。

2、kotlin 的未来

kotlin 将是 Android 开发的必备技能之一,同时作为 JVM 平台上的跨平台语言,它还存在很多可能性:

kotlin_future

kotlin 不仅用于 jvm 平台的 Android 和服务器上,也可以用于iOS、macOS、Windows、Linux、web 等平台上。

参考链接:

1、kotlin 官网:https://www.kotlincn.net/docs/reference/

2、扔物线博客:https://rengwuxian.com/kotlin-coroutines-1/

知道是不会有人点的,但万一被感动了呢?