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 | String str = "nullableStr"; |
kotlin 定义变量用 val 和 var 声明,具体变量类型可放在变量名后用 : 类型
定义,甚至可不指定由赋值推导类型。val
只允许赋值一次,用 var
可重新赋值,其次代码结尾去除 ;
。
1 | // var 可变,val 不可变 |
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 |
|
由于注解传值是不影响调用方编译阶段的,而运行阶段出现的空指针不一定在必现的场景,因此把空指针控制在静态编译期间将非常有效减少空指针的产生。
kotlin 对于可空的值必须用 ?
来表示,否则传的一定是非空值,不然静态编译会提示错误。
1 | fun getTestNameNullable(testData: TestData?): String? { |
?.操作符:代替.操作符,对 nullable 类型做安全的解引用,如果 receiver 为 null 则直接返回 null,什么也不做。
?:操作符:相当于常用的三元操作符,nullable ?: other 等价于 nullable != null ? nullable : other。
4)懒加载
对于一些变量的声明时,如果它是可空的,后续想使用时,可以使用 ?.
进行调用,如果确定它是非空的,可以用 !!
忽略编译检查,但调用处处弥漫着 !!
让代码非常丑陋。
1 | private var testVar: String? = null |
如果我们非常明确它是非空的,但需要在之后的初始化方法里才能赋值,此时又不想声明可空的 ?
和 !!
调用,那我们可以在声明时加上懒加载标志,让它在编译期间被忽略,当成非空处理。
1 | private lateinit var testLazyVar: String // 忽略编译检查,由开发者决定 |
懒加载对声明两种变量的使用:
var:=> lateinit var
val:=> val by lazy {},表达式内的只会在第一次调用时执行,之后只是调用变量的
get
获取
5)控制流
java switch 支持基本数据类型、枚举、字符串,但只允许常量,不允许变量判断:
1 | String type = "1"; |
kotlin 没有 switch
,但新增了 when else
, 它可以覆盖任意类型的分支判断:
1 | // when else 任意类型判断 |
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 | 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)); |
可以看到 java 遍历中需要手动一个一个的遍历获取,在 java8 引入了 stream 后,对 forEach
、filter
、map
也变得更简洁。
在 kotlin 中,流式调用可以如下所示:
1 | // stream api 调用 |
3、函数
kotlin 函数相比 java,需要使用 fun
声明,并增加了默认参数,?
的属性在调用时可缺省,当然非缺省的必须放在缺省的前边:
1 | private fun testShowDialog( |
java 声明时需要使用多个重载调用:
1 | private void testShowDialog(String title) { |
可见 kotlin 带缺省的函数,可有效减少函数重载多的问题。
4、类
1)类声明
java 类的静态变量一般直接声明和调用:
1 | public class TestJava implements ITest { |
kotlin 使用 companion
伴生对象来表示用类名作为限定符调用 TestKotlin.TAG
, 如果想成为真正的静态方法和字段,需添加 @JvmStatic
等价:
1 | class TestKotlin : ITest { |
可见性修饰,相比 java 多了 internal,表示模块内可见,非导出的 library 则不可访问,非常方便 sdk 用于访问符限制。
2)数据类
在存储操作时,经常会使用数据类,但为了减少外部修改用提供的 get
和 set
, 会让类代码变得繁琐。同时对于类的一些方法 equal
、hashCode
, toString
都需要开发者重写,否则表现会不太符合预期:
1 | class TestData { |
对于 kotln 有专门的数据类表示,用 data
修饰,所有属性都默认提供了 setter
、getter
:
1 | data class TestData( |
同时对数据类的主构造函数提供了 toString
、equlas/hasCode
的实现:
toString()
格式是 TestData(entity=TestEntity(name=ivan), id=1)equals()/hashCode()
实现了entitiy
和id
属性的比较和映射提供了
componentN()
的解构声明,可以直接从类中获取解构的属性,如val(entity, id) = testData
3)扩展类
扩展使得 kotlin 在类使用时变得非常灵活,在 Java 中想扩展一个类的新功能时,一般使用继承或者装饰等方式。但如果想为一个第三方类或系统类增加新的函数,就像调用原始类一样调用该方法,就可以使用 kotlin 的扩展属性:
1 | data class TestData( |
经过外部声明 TestData
的扩展函数 getMapId
后,像普通函数调用方式使用:
1 | // 类扩展函数 |
扩展的方法并不会破坏现有的类方法,它是静态分发的,通过该类型的变量用点的表达式去调用这个新函数。
三、kotlin 的高阶用法
1、高阶函数
kotlin 的高阶函数是将函数用作参数或返回值的函数,可以用高数里的高阶导数类比:
高阶导数:y=f(x) ,求导后的 y=f’(x),仍然是x的函数,则二阶函数。把≥2阶的导数认为是高阶导数。
1 | class ViewKotlin { |
比如示例,把 onClick
函数作为 setOnClickListener
的返回值,则认为它是高阶函数。但其实函数并不能传递,传递的是函数式的对象(匿名函数/lambda)。
2、lambda 用法
lambda 表达式总是括在花括号中, 完整语法是参数声明放在花括号内,并有可选的类型标注, 函数体跟在 ->
符号之后:
1 | // lambda 表达式 |
lambda 表示式的变形:
- 拖尾 lambda 表达式: 如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外
3、协程
协程:一种编程思想,解决的是并发问题,让协作式任务实现起来更方便,Kotlin-JVM
的协程:是基于 Java Thread API 实现的一种更上层的工具。
我们以一个异步请求任务举例,java 需要通过异步回调来进行,如果有多个依赖关系的回调,最终代码的嵌套会非常深:
1 | // 做一个网络请求,一般需要 callback 方式或阻塞式IO的同步调用 |
在 kotlin 中,则使用同步代码的方式来执行异步请求,对于依赖关系的几个异步回调也变得非常简单:
1 | val coroutineScope = CoroutineScope(Dispatchers.Main + Job()) |
此时可以用 withContext 简化切换线程的操作,它切换到指定的线程,在代码块执行完后把结果带回:
1 | // 子线程网络请求 |
对于一些没有依赖关系的任务1和任务2,允许他们并行执行时,可以使用关键字 async
标志,在需要串行等待时使用 await
进行等待:
1 | val coroutineScope = CoroutineScope(Dispatchers.Main + Job()) |
可以看到协程让并发协作式的任务变得非常简洁,达到了顺序式编程,消灭了回调。
四、对比展望
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 不仅用于 jvm 平台的 Android 和服务器上,也可以用于iOS、macOS、Windows、Linux、web 等平台上。
参考链接:
1、kotlin 官网:https://www.kotlincn.net/docs/reference/