Kotlin 朱涛-3 原理 编译器 反编译 字节码
目录
03 | Kotlin 原理:编译器在幕后干了什么
Kotlin 的编译流程
Kotlin 代码在运行之前,要先经过 Kotlin 编译器 的编译,编译后会变成 Java 字节码。
Kotlin 和 Java 能够兼容的原因就在于,它们用的都是 Java 字节码。
如何研究 Kotlin 代码
两种方法
- 直接研究 Kotlin 编译后的字节码
- 将 Kotlin 转换成字节码后,再将字节码 反编译成等价的 Java 代码
println("Hello world.")
LDC "Hello world."
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
String var0 = "Hello world.";
System.out.println(var0);
IDEA 反编译步骤
- 打开 Kotlin 源文件,例如
xxx.kt
- 点击菜单栏 Tools ->
Kotlin
-> Show Kotlin Bytecode,这时候右边窗口中就可以直接展示对应的字节码 - 点击右边窗口中的
Decompile
,这时会打开一个新窗口,用来展示反编译出来的 Java 代码xxx_decompiled.java
分析 Kotlin 中的基础类型
问题:Kotlin 在 语法层面 摒弃了原始类型
,难道就不担心由此带来的性能问题
吗?
Kotlin 源码
下面这段代码的思路,是将 Kotlin 的 Long
类型所有可能的使用情况都列举出来,然后去研究反编译后对应的 Java 代码。
// 用 val 定义可为空、不可为空的 Long,并且赋值
val a: Long = 1L
val b: Long? = 2L
// 用 var 定义可为空、不可为空的 Long,并且赋值
var c: Long = 3L
var d: Long? = 4L
// 用 var 定义可为空的 Long,先赋值,然后改为 null
var e: Long? = 5L
e = null
// 用 val 定义可为空的 Long,直接赋值 null
val f: Long? = null
// 用 var 定义可为空的 Long,先赋值 null,然后赋值数字
var g: Long? = null
g = 6L
Java 源码
反编译后的 Java 代码
long a = 1L;
long b = 2L;
long c = 3L;
long d = 4L;
Long e = 5L;
e = (Long)null;
Long f = (Long)null;
Long g = (Long)null;
g = 6L;
源码分析
a、b、c、d 被转换成了 Java 的原始类型 long
- a、c:因为它们的类型是不可为空的,所以 Kotlin 编译器会直接将它们优化成原始类型
- b、d:它们的类型虽然可能为空,但是编译器对上下文分析后发现,他们
不存在变成 null 的情况
,所以也会将它们优化成原始类型
e、f、g 被转换成了 Java 里的包装类型 Long
- 因为它们
都被赋值过 null
,所以 Kotlin 无法将它们优化成原始类型,因为 Java 中只有对象才能被赋值为 null
总结:
- 只要基础类型的变量可能为空,那么这个变量就会被转换成 Java 的包装类型
- 只要基础类型的变量不可能为空,那么这个变量就会被转换成 Java 的原始类型
分析 Kotlin 接口的新特性
Kotlin 接口有两个新特性:支持成员属性、支持方法默认实现。
下面就来分析下 Kotlin 接口语法的实现原理,从而找出它的局限性。
Kotlin 接口源码分析
interface Behavior {
val canWalk: Boolean // 接口内可以有成员属性,但不能赋初值:Property initializers are not allowed in interfaces
fun walk() { // 接口方法可以有默认实现
if (canWalk) {
println(canWalk)
}
}
}
public interface Behavior {
boolean getCanWalk(); // 接口的成员属性变成了普通的接口方法
void walk(); // 接口方法的默认实现消失了
// 多了一个静态内部类,接口方法的默认实现被放到了静态内部类当中去了
public static final class DefaultImpls {
public static void walk(Behavior $this) {
if ($this.getCanWalk()) {
boolean var1 = $this.getCanWalk();
System.out.println(var1);
}
}
}
}
- Kotlin 接口的
成员属性
本质上并不是一个真正的属性,当转换成 Java 以后,就变成了一个普通的接口方法
- Kotlin 接口中
方法的默认实现
,本质上也没有直接提供实现的代码,而只是在接口当中定义了一个静态内部类
,然后将默认实现的代码放到了静态内部类当中去了
Kotlin 接口实现类源码分析
class Man: Behavior { // 实现 Behavior 接口
override val canWalk: Boolean = true // 重写 canWalk 属性
}
public final class Man implements Behavior {
private final boolean canWalk = true;
public boolean getCanWalk() { // 同样,接口的成员属性变成了普通的接口方法
return this.canWalk; // 返回的是它内部 canWalk 属性的值
}
public void walk() { // 最终还是实现了接口当中的方法
Behavior.DefaultImpls.walk(this); // 默认实现转到了接口的静态内部类中
}
}
Kotlin 接口实现类调用逻辑
private fun testInterface() {
val man = Man()
man.walk()
man.canWalk
}
private static final void testInterface() {
Man man = new Man();
man.walk(); // 调用方法保持不变
man.getCanWalk(); // 调用属性也变成了调用方法
}
总结
- 箭头①:Kotlin
接口属性
,实际上会被当中接口方法
来看待 - 箭头②:Kotlin 接口默认实现,实际上还是一个普通的方法
- 箭头③:Kotlin 接口默认实现的逻辑,被放在了
静态内部类
中 - 箭头④:Kotlin 接口的实现类中,接口属性被转化成了一个接口方法
- 箭头⑤:Kotlin 接口的实现类中,仍然会实现接口默认实现的方法,并且执行流程转给了接口的
静态内部类
因此,Kotlin 接口中的属性,它既不能真正存储任何状态,也不能被赋予初始值,因为它本质上还是一个接口方法。
接口属性如果被赋予了初始值,会提示:Property initializers are not allowed in interfaces
接口属性如果没被实现类重写,会提示:Class xxx is not abstract and does not implement abstract member xxx
在 Kotlin 接口中定义类似 Java 中的 public static final
常量,需要使用伴生对象,否则会提示:
Const 'val' are only allowed on top level, in named objects, or in companion objects
interface Behavior {
companion object { // 在接口中定义常量
const val CAN_WALK = true
}
}
小结
Kotlin 的每一个语法,最终都会被翻译成对应的 Java 字节码。所有 Kotlin 的新特性,最终都被转换成了一种 Java 能认识的语法。
正是因为 Kotlin 编译器在背后做的这些翻译工作,才可以让我们写出的 Kotlin 代码更加简洁、更加安全。
- 类型推导:编译器会将省略的变量类型,补充回来
- 原始类型:编译器会根据每一个变量的可空性,将它们转换成原始类型或者包装类型
- 字符串模板:编译器会将字符串模板转换成 Java 拼接的形式
- when 表达式:编译器会其转换成 switch case 语句 --- 或 if else 语句
- 类默认 public:编译器会将省略的 public 补充回来
- 嵌套类默认 static:编译器会给 Kotlin 中的嵌套类,默认添加 static 关键字
- 数据类:编译器会自动给数据类添加 equals、copy、toString 等方法
2018-06-09
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/9160763.html