zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Kotlin 朱涛-3 原理 编译器 反编译 字节码

2023-09-14 09:00:05 时间

本文地址


目录

03 | Kotlin原理:编译器在幕后干了什么

Kotlin 的编译流程

Kotlin 代码在运行之前,要先经过 Kotlin 编译器 的编译(Compile),编译后会变成 Java 字节码。因此 Kotlin 和 Java 能够兼容的原因就在于,它们用的都是 Java 字节码。JVM 拿到 Java 字节码后,会根据特定的语法来解析其中的内容,并在操作系统之上建立一层抽象运行环境,并让字节码运行起来。

如何研究 Kotlin 代码

两种方法

  • 第一种方法:直接研究 Kotlin 编译后的字节码

这种方法实践起来比较困难,因为大多数开发者并不十分清楚 Java 字节码的语法规则,对于一些稍微复杂一点的代码,分析起来更是非常吃力。

  • 第二种方法:将 Kotlin 转换成字节码后,再将字节码 反编译成等价的 Java 代码

这种方式不及字节码那样深入底层,但好处是简单、直观,对于非常复杂的代码也能轻松应对。

反编译流程

  • 红色虚线框外面的图,是 Kotlin 的编译流程,将 Kotlin 代码变成了 Java 字节码
  • 红色虚线框内部的图,是通过反编译工具,将 Java 字节码翻译成了 Java 代码

经过这样一个流程后,我们就能得到和 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
    • 这时候右边窗口中就可以直接展示当前 Kotlin 源文件对应的字节码
  • 点击右边窗口中的 Decompile 按钮
    • 这时就会打开一个新的窗口,用来展示反编译出来的 Java 源文件 xxx_decompiled.java

Kotlin 中的基础类型

Kotlin 在 语法层面 摒弃了原始类型,不知大家会不会有一个疑惑,难道 Kotlin 就不担心由此带来的性能问题吗?根据我十余年 Java 开发经验,如果 Kotlin 胆敢在某一语法设计上牺牲性能,最终肯定是会被 Java 开发者抛弃的。

下面我们就研究下 Kotlin 的 Long 与 Java 的 long/Long 是否存在某种联系。

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:它们的类型虽然是可能为空的,但是编译器对上下文分析后发现,这两个变量不存在变成空的情况,所以 Kotlin 编译器也会将它们优化成原始类型
  • e、f、g 被转换成了 Java 里的包装类型 Long
    • 因为它们被赋值过 null,所以 Kotlin 无法将它们优化成原始类型,因为 Java 中只有对象才能被赋值为 null

总结:

  • 只要基础类型的变量可能为空,那么这个变量就会被转换成 Java 的包装类型
  • 只要基础类型的变量不可能为空,那么这个变量就会被转换成 Java 的原始类型

Kotlin 接口的新特性

Kotlin 接口有两个新特性:支持成员属性、支持方法默认实现。虽然在 Java 1.8 版本中,接口也引入了类似的特性,但由于 Kotlin 是完全兼容 Java 1.6 版本的,因此 Kotlin 接口中的这些特性,并不是基于 Java 1.8 实现的。那它到底是如何实现的呢?

下面就来分析下 Kotlin 接口语法的实现原理,从而找出它的局限性。

Kotlin 接口源码分析

interface Behavior {
    val canWalk: Boolean  // 接口内可以有成员属性
    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 接口的成员属性 canWalk,本质上并不是一个真正的属性,当它转换成 Java 以后,就变成了一个普通的接口方法 getCanWalk()
  • Kotlin 接口方法的默认实现,它本质上也没有直接提供实现的代码。对应的,它只是在接口当中定义了一个静态内部类 DefaultImpls,然后将默认实现的代码放到了静态内部类当中去了

Kotlin 接口实现类源码分析

class Man: Behavior {                    // 实现 Behavior 接口
    override val canWalk: Boolean = true // 重写 canWalk 属性,没有实现 walk() 方法
}
public final class Man implements Behavior {
   private final boolean canWalk = true; // 因为重写了 canWalk 属性,所以属性值为 true

   public boolean getCanWalk() { // 新增了一个访问属性的方法
      return this.canWalk;       // 返回的是它内部私有的 canWalk 属性
   }

   public void walk() {          // 最终还是实现了接口当中的方法
      Behavior.DefaultImpls.walk(this); // 默认实现转到了接口 Behavior 的静态内部类中
   }
}

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 接口默认实现的逻辑,被放在了静态内部类 DefaultImpls 中
  • 箭头④: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 常量,需要特殊处理,例如下面的代码:
interface Behavior {
    companion object {  // 接口中定义常量
        @JvmField val canWalk = true // 'const' might be used instead of '@JvmField'
    }
}

Behavior.canWalk // 访问接口中定义的常量

小结

Kotlin 的每一个语法,最终都会被翻译成对应的 Java 字节码。所有 Kotlin 的新特性,最终都被转换成了一种 Java 能认识的语法。正是因为 Kotlin 编译器在背后做的这些翻译工作,才可以让我们写出的 Kotlin 代码更加简洁、更加安全。

  • 类型推导:我们写 Kotlin 代码时省略的变量类型,最终会被编译器补充回来
  • 原始类型:编译器会根据每一个变量的可空性将它们转换成原始类型或者包装类型
  • 字符串模板:编译器最终会将它们转换成 Java 拼接的形式
  • when 表达式:编译器最终会将它们转换成类似 switch case 的语句
  • 类默认 public:我们写 Kotlin 代码时省略的 public,最终会被编译器补充回来
  • 嵌套类默认 static:我们在 Kotlin 当中写的嵌套类,默认会被添加 static 关键字
  • 数据类:数据类的 equals、copy、toString 等方法,都是编译器帮我们自动生成的

2018-06-09