Kotlin学习笔记4---如何探寻Kotlin原理

30 阅读4分钟

本课程为极客时间上朱涛老师的《Kotlin编程第一课》的学习笔记

1.Kotlin的编译流程

JavaCoder应该都明白,Java之所以能跨平台运行,是在于JVM接受Java编译后的字节文件;所以Kotlin同样是基于JVM的语言,它又是如何做到能兼容Java的同时,还有自己的一些特性;而它又是怎么在JVM上运行的?

这里打个比方

println("Hello World.")

这样的语言JVM是无法直接执行了;经过编译后,它对应的文件格式如下

LDC "Hello world."
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V

聪明的你会发现这不就是Java的字节码,对的,Kotlin最终的编译结果还是字节码;所以总的来说:Java和Kotlin本质是用同一种语言在沟通

下面是就是Java和Kotlin的编译流程

img

2.如何分析Kotlin代码

因为大部分研发对于Java的了解度很高,可以从Java的角度去分析Kotlin的代码;那么是否可以按照下面这个流程来分析:

  • 首先把kotlin的代码编译成字节码
  • 再将编译后的字节码转换为Java代码
  • 等价分析Kotlin和Java的代码

img

下面就是这样流程的变化过程

println("Hello world.") /*
          编译
           ↓            */    
LDC "Hello world."
INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V  /*
         反编译
           ↓            */
String var0 = "Hello world.";
System.out.println(var0);

可以理解Kotlin中的println等价于Java中的System.out.println;

那么有什么工具可以实现上面的需求

还好,这样的需求,Android Studio已经提供工具;具体的操作如下:

第一步,打开我们需要分析的Kotlin代码

img

第二步,依次点击菜单栏:Tools -> Kotlin -> Show Kotlin Bytecode;这里能看到编译后的字节码

img

第三步,点击画面右边的“Decompile”按钮;

img

就可以看到反编译后的Java代码了

img

3.Kotlin是否有基础类型

实例1:Kotlin中的Long是否和Java中的long/Long有关系?

这里就用上面的思路来分析,首先上Kotlin源码

// kotlin 代码

// 用 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;

最终分析结果如下:

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

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

这样的方式很好的利用了基础类型占用低的特性,提高了特性

实例2:分析Kotlin中Interface的成员属性

备注:Kotlin中的成员属性是如何实现的?Java对应的方法?

// Kotlin 代码

interface Behavior {
    // 接口内可以有成员属性
    val canWalk: Boolean

    // 接口方法的默认实现
    fun walk() {
        if (canWalk) {
            println(canWalk)
        }
    }
}

private fun testInterface() {
    val man = Man()
    man.walk()
}

转换后对应的Java代码

// 等价的 Java 代码

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()。

这里补充上Man的代码

class Man:Behavior {
    override val canWalk: Boolean
        get() = true
}

对应的Java代码

public final class Man implements Behavior {
   public boolean getCanWalk() {
      return true;
   }

   public void walk() {
      DefaultImpls.walk(this);
   }
}

总结:Kotlin 的新特性,最终被转换成了一种 Java 能认识的语法

4.小结

所有的Kotlin代码,最终都会被Kotlin编译器进行一次统一的翻译,把它们变成Java能理解的格式。

img

5.思考题

之前提到的问题,为Person类增加isAdult属性,我们要通过自定义getter来实现,如:

class Person(val name: String, var age: Int) {
    val isAdult
        get() = age >= 18
}

而下面这种写法则是错误的:

class Person(val name: String, var age: Int) {
    val isAdult = age >= 18
}

为什么?

前一个代码转换成Java代码对应是:

 public final boolean isAdult() {
      return this.age >= 18;
   }

错误代码转换成Java对应的代码是:

public final class Person {
   private final boolean isAdult;

   public final boolean isAdult() {
      return this.isAdult;
   }


   public Person(@NotNull String name, int age) {
     ...
      super();
      this.name = name;
      this.age = age;
      this.isAdult = this.age >= 18;
   }
}

它的问题就在于isAdult在初始化以后就不会变化了;后面age发生变化了,它也不会更新;