之前初次接触 Kotlin 时,总有个疑问,同样是运行在 JVM 内,而且 Kotlin 和 Java 可以无缝混合开发,那究竟 Kotlin 是如何实现 Java 不存在的特性的呢? 今天就来粗略了解这些语法糖的奥妙。
Kotlin 支持的 Java 中没有的特性:类型推断、可变性、可空性、自动拆装箱、泛型数组、高阶函数、DSL、顶层函数、扩展函数、内联函数、伴生对象、数据类、密封类、单例类、类代理、internal、泛型具体化……
先说结论,Kotlin 编译器通过以下几种方式支持 Java 没有的特性:
编译器推断、中间代码添加、元注解、Metadata
.kt 是如何编译成 .class 的?
对语言编译过程有基本认识的话,就知道大概分为以下几步:
- 词法分析:把源码的字符流,转化成标记(Token)序列,标记是语言的最小语义单位,包括关键字、标识符、运算符、常数等;
- 语法分析:把标记序列,组合成各类语法短句,判断标记序列在语法结构上,是否正确,输出树形结构的抽象语法树;
- 语义分析:结合上下文,检查每一个语法短句,语义是否正确,是否符合语言规范。
所以 kotlin 支持 Java 不存在的特性,主要还是通过 Kotlin 编译器做了加工处理。
编译器幕后干的好事
以下直接说结论,懒得贴反编译的代码了,可以自己写 kotlin 代码,然后通过 AS 的 Tools/Kotlin/Show kotlin Bytecode,再点击 decompile 查看 Kotlin 转成 Java 的代码
1)类型推断
- 类型推断并不是不确定数据类型,相反是从上下文推断出一个明确的数据类型;
- 类型推断的意义在于,去掉代码中的冗余信息,提升研发效率;
- 类型推断主要发生在语法分析和语义分析阶段;
1 | val name = “Vincent” //String |
2)Kotlin 的自动拆装箱
- 自动拆装箱是有性能损耗的
- 支持泛型类型,所以默认不具备协变性,字节码实现对应anewarray,相当于Java的Integer[]。
- 字节码实现对应newarray,相当于Java的int[],性能较好。
1 | //Java 1.5 后支持自动拆装箱 |
总的来说,
- 可空的基本数据类型,会被编译成装箱类;
- 泛型中基本数据类型,在使用时,会自动拆装箱;
- 泛型数组,使用的是装箱类型。
- 出于性能考虑,为避免自动拆装箱所带来的开销,在Kotlin中,应当尽量避免使用可空的基本数据类型,以及泛型数组;
3)Kotlin的高阶函数(返回函数或参数是函数)
- 从性能上讲,高阶函数要创建实例,所以开销会增大。
- Kotlin的匿名内部类,在和外部类有互动的时候,也会持有外部类的引用,存在一定的、潜在的内存泄漏的风险。
- 高阶函数是通过中间代码添加生成的
1 | class类名$方法名$1 : FunctionReference(receiver:方法所在类),Funtion1 { |
4)顶层函数
- Java中的函数,必须在类的内部定义;
- 而 Kotlin 中允许在类的外部,定义文件级别的顶层函数以及变量。
- 从字节码层面来说,所有的函数和变量都必须在类的内部;
- Kotlin编译器,在生成字节码时,会给顶层的函数及变量,创建一个所属的类,类名默认规则是文件名+Kt ;
- Java代码,可以通过这些Kt结尾的类,调用到这些在Kotlin中定义的顶层函数和变量;
- 也是通过中间代码添加生成的
1 | //TimeUtil.kt |
5)扩展函数
- 通过中间代码添加生成的
- 变成类的静态方法,或者是类似顶层函数的逻辑
6)inline 函数
- 通过中间代码添加生成的
- inline函数除了能解决泛型具体化问题,还比一般的函数更有性能优势,因为代码的添加发生在编译时,运行时会减少一次虚拟机栈中栈帧的入栈出栈操作;
- inline函数的副作用是,会导致代码体积增长;
7)其他
- 数据类 data class -> final class, 生成 getter()、setter()、equals()、hashCode()、toString()、componentN()、copy()
- 密封类 sealed class -> abstract class + static final inner class
- 内联类 inline class-> final class, 构造参数变成类的属性
- 接口里面的默认实现-> 会生成默认实现的类,里面对应实现的方法
1 | interface IFace { |
**类型的可空性检查 ->**编译的时候会补充上 NotNull 或 NullAble
泛型的可空检查-> 通过 Kotlin 的 Metadata
val names = listOf<String?>(“Vincent”, null)字符串模板 -> 编译器最终会将它们转换成 Java 拼接的形式。
when 表达式 -> 编译器最终会将它们转换成类似 switch case 的语句。
类默认 public -> Kotlin 当中被我们省略掉 public,最终会被编译器补充。
嵌套类默认 static -> 我们在 Kotlin 当中的嵌套类,默认会被添加 static 关键字,将其变成静态内部类,防止不必要的内存泄漏。
lateinit -> lateinit 用于变量 var,只是让编译期间忽略对属性未初始化的检查,后续在哪里什么时候初始化由开发者决定
suspend 的原理
- 挂起函数的本质,就是 Callback
- 这个“从挂起函数转换成 CallBack 函数”的过程,被叫做是 CPS 转换(Continuation-Passing-Style Transformation)
- 反编译转成 Java 后,可以看到suspend 函数的参数多了一个 Continuation 的类(里面有协程的上下文,和 resumeWith方法),内部是通过 label 来实现程序的 goto 功能,从而实现“状态机”的效果 (详细代码分析略)
1 | suspend fun getUserInfo(): String { |
—End—