Kotlin-通过Java反编译撕开Kotlin的高端语法糖的真面目

前言

用了kotlin 很多年了,kotlin使用的方便性大大提高了我们的开发效率。android结合kotlin的特性,推出的compose、coroutine、Flow、Channel、Room等框架大大提升了我们的开发效率。人无法从有知状态变为无知状态,如果有很多跟我一样,是先学的java,再学的kotlin,是不是会感叹kotlin语法糖的强大。对于kotlin的用法已经有很多的教程,今天我通过将kotlin转换成Java代码的方式,从java的角度去看待kotlin到底做了什么,让我们开发如此的简便。

那么废话不多说,我们直接开始。

这里我们找了很多大家比较常用的一些语法糖。(本篇主要是把自己理解kotlin语法的方式分享给大家,不卖鱼、只分享捕鱼的方式)

顶层函数

Java一切皆对象,不过kotlin的函数可以不放在class内部,直接写成顶层函数调用,我们都知道,kotlin最终也会转换成字节码运行在JVM上的,那么顶层函数到底是什么形式?

例如:

大家可以看到,我们定义了一个顶层函数,他不属于任何一个class里面。那么他的本质是什么呢? 我们通过android studio转换出字节码来看一下:

swift 复制代码
// class version 52.0 (52)
// access flags 0x31
public final class com/yuanyi/myapplication/kt/AlwaysLearnKt { // 可以看到,我们在外部定义的AlwaysLearn.kt文件被创建成为了一个final类


  // access flags 0x19
  // 我们的topFunction被定义成了一个final static类型的静态函数,
  // ()V指代该方法没有任何参数,V代表返回值为Void
  public final static topFunction()V 
   L0
    LINENUMBER 4 L0
    RETURN
   L1
    MAXSTACK = 0
    MAXLOCALS = 0
  // 这里指代当前是一个kotlin的文件,对应的kotlin的版本号是1.8.0
  @Lkotlin/Metadata;(mv={1, 8, 0}, k=2, d1={"\u0000\u0008\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001\u00a8\u0006\u0002"}, d2={"topFunction", "", "app_debug"})
  // compiled from: AlwaysLearn.kt
}

然后我们把字节码反编译成java代码看一下:

这个就很简单了是不是,我们发现,所谓的顶层函数,本质上就是一种静态方法。

扩展函数

顶层类扩展函数

我们查看一下字节码

swift 复制代码
// class version 52.0 (52)
// access flags 0x31
public final class com/yuanyi/myapplication/kt/ExtendFunctionKt {


  // access flags 0x19
  // 可以看到,我们定义的顶层扩展函数被转换成了final static的方法
  // (Ljava/lang/String;)I 代表参数是string
  // 返回值一个不为空的int因为我在定义返回值的时候,用的是Int不是Int?
  public final static getLength(Ljava/lang/String;)I 
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "$this$getLength"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 4 L1
    ALOAD 0
    INVOKEVIRTUAL java/lang/String.length ()I
    IRETURN
   L2
    LOCALVARIABLE $this$getLength Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  @Lkotlin/Metadata;(mv={1, 8, 0}, k=2, d1={"\u0000\u000c\n\u0000\n\u0002\u0010\u0008\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002\u00a8\u0006\u0003"}, d2={"getLength", "", "", "app_debug"})
  // compiled from: ExtendFunction.kt
}

然后我们将字节码反编译为java看一下

我们发现,这个方法的确是一个public static final int getLength(String)的形式。

为了效率,我们后面不再看字节码了,最后我会把查看字节码和转Java代码的方式写出来。

类内部扩展函数

kotlin 复制代码
package com.yuanyi.myapplication.kt

// 这是一个顶层的String类型的扩展函数,获取string的长度
fun String.getLength() : Int = this.length

class ExtendFunction{
    // 这是一个类内部的扩展函数
    fun String.println() = println(this)
}

对应的Java代码

kotlin 复制代码
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {1, 8, 0},
   k = 1,
   d1 = {"\u0000\u0016\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\u0010\u000e\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\n\u0010\u0003\u001a\u00020\u0004*\u00020\u0005¨\u0006\u0006"},
   d2 = {"Lcom/yuanyi/myapplication/kt/ExtendFunction;", "", "()V", "println", "", "", "app_debug"}
)
// 这是我们在kt里面定义的class
public final class ExtendFunction {
   // 我们发现,我们定义的类的内部扩展函数,是类的一个final类型的成员函数
   // 它接收一个String类型的参数,然后打印它
   public final void println(@NotNull String $this$println) {
      Intrinsics.checkNotNullParameter($this$println, "$this$println");
      System.out.println($this$println);
   }
}
// ExtendFunctionKt.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {1, 8, 0},
   k = 2,
   d1 = {"\u0000\f\n\u0000\n\u0002\u0010\b\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"},
   d2 = {"getLength", "", "", "app_debug"}
)
// 这是我们的文件名,在编译的时候,kotlin都会为我们生成一个"${文件名}kt"的类
// 我们定义的顶层扩展函数,是这个类的静态函数
public final class ExtendFunctionKt {
   public static final int getLength(@NotNull String $this$getLength) {
      Intrinsics.checkNotNullParameter($this$getLength, "$this$getLength");
      return $this$getLength.length();
   }
}

相信看到这里,大家大致对kotlin写类外的代码有了一定的了解了,我们定义的顶层函数,本质上是被写入到文件名生成的类内部的,这也符合了java一切切对象的设计。

block到底是什么?

rust 复制代码
block1 : (String?,String?) -> String?
block2 : (String?) -> Int
block3 : (String?) -> Unit

相信大家都用过上面的代码吧,其实我告诉大家,它本质就是一个接口,是一个只有一个方法的接口。我们来看看它对应的代码是什么。

首先,定义一个用block的方法

kotlin 复制代码
class BlockTest {
    // 定义一个获取string长度的方法
    fun getStringLength(
        string : String,
        block : ((String) -> Int)
    ) = block(string)
}

对应的Java的代码为:

less 复制代码
@Metadata(
   mv = {1, 8, 0},
   k = 1,
   d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J"\u0010\u0003\u001a\u00020\u00042\u0006\u0010\u0005\u001a\u00020\u00062\u0012\u0010\u0007\u001a\u000e\u0012\u0004\u0012\u00020\u0006\u0012\u0004\u0012\u00020\u00040\b¨\u0006\t"},
   d2 = {"Lcom/yuanyi/myapplication/kt/BlockTest;", "", "()V", "getStringLength", "", "string", "", "block", "Lkotlin/Function1;", "app_debug"}
)
public final class BlockTest {
   // 我们发现,
   // 我们定义的方法当中的block变成了一个Function1的类型的对象,
   // 他通过调用invoke方法来调用我们定义的方法,
   // 然后讲结果强转为Number,
   // 然后通过intValue()返回
   public final int getStringLength(@NotNull String string, @NotNull Function1 block) {
      Intrinsics.checkNotNullParameter(string, "string");
      Intrinsics.checkNotNullParameter(block, "block");
      return ((Number)block.invoke(string)).intValue();
   }
}

查看这个Function1是什么?

我们发现,这个Function1是属于kotlin.jvm.functions里面定义的一种interface,他接收一个P1类型的数据,然后返回一个R类型的数据,发现了没有,这个形式跟我们定义的block是不是很相似。

kotlin 复制代码
// block的形式
(String) -> Int
// 可以看成
fun method(string : String) : Int

// Function1的形式
fun invoke(p1:P1) : R

所以,本质上,我们的block就是一个被定义过的interface,只不过kotlin的编译器帮我们做了转换。

其中block最多接受参数个数为22个,超过22个就会报错。但是我们通常不会有超过22个的参数,超过了,那么就封装一下。

let、with、apply、run、alse、takeIf...到底是什么?

为此,我写了如下的例子:

kotlin 复制代码
class ExtendFunction{
    fun test_Let_Run_Also_Apply_With_TakeIf (){
        val data = "hello"
        // let
        val letResult = data.let {
            Log.i(data, "let block")
            it.length
        }

        // apply
        val applyResult = data.apply {
            Log.i(data, "apply block")
            length
        }
        Log.i(data, applyResult)
        // also
        val alsoResult = data.also {
            Log.i(data, "also block")
            it.length
        }
        Log.i(data, alsoResult)
        // run
        val runResult = data.run {
            Log.i(data, "run block")
            length
        }
        Log.i(data, runResult.toString())
        // with
        val withResult = with(data){
            Log.i(data, "with block")
            length
        }
        Log.i(data, withResult.toString())
        // takeIf
        val takeIfResult = data.takeIf {
            Log.i(data, "takeIf block")
            it == "hello"
        }
        Log.i(data, takeIfResult.toString())

    }
}

将上述代码转换为java代码之后,我们查看一下

我们发现,block在这里的方式不直观(这是因为let这些扩展函数被inline标记了,)直接被隐藏了,我们先说一下部分结果

  • let是执行完block里面的代码,直接将block最后一行的结果返回给我们
  • apply是执行完block里面的代码,直接将string返回给我们
  • 其余的略 我们发现,好像也没什么大区别嘛。

我们查看一下apply、let他们的定义

apply

kotlin 复制代码
@kotlin.internal.InlineOnly
// 方法定义,它被定义是一个范型T类型的一个扩展函数,
// 它接受一个block为参数
// 然后返回这个T对象(从这里,大家就知道为什么apply会返回对象本身了)
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

其实转换成java代码就是(由于java代码没有内联,因此这里模拟一下)

r 复制代码
public <T> T apply(T t, Function1<T> function1){
    ...
    function1.invoke(t);
    return t;
}

let

kotlin 复制代码
@kotlin.internal.InlineOnly
// let也是一个范型T的一个扩展函数,
// 它接收一个block:(T) -> R类型的对象,
// 然后将R类型返回
// 所以各位知道了吧,为什么block()的最后一行就是let的返回值
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

转换成java代码就是

其实转换成java代码就是(由于java代码没有内联,因此这里模拟一下)

r 复制代码
public <T,R> T let(T t, Function1<T> function1){
    ...
    R r = function1.invoke(t);
    return r;
}

后续的就不再举例子了。 因此,如果有面试官问你,apply、with、let有什么区别,什么场景下使用它。我认为,其实他们这些很大程度上都是可以通用的,就看你喜欢用什么。

object class到底是什么?

我们定义一个object看下先

kotlin 复制代码
object INSTANCE{
    fun sayHello() = println("Hello , instance!")
}

然后转换成java代码查看

java 复制代码
public final class INSTANCE {
   @NotNull
   public static final INSTANCE INSTANCE;// INSTANCE实例对象

   public final void sayHello() {
      String var1 = "Hello , instance!";
      System.out.println(var1);
   }
   // 将构造函数设置为私有
   private INSTANCE() {
   }
   // 通过静态代码块在类加载的时候就直接对类进行初始化
   static {
      INSTANCE var0 = new INSTANCE();
      INSTANCE = var0;
   }
}

我们发现,其实它本质上,就是一个饿汉的单例模式。

什么是伴生对象

为什么它也能当作是单例模式? 我们先写一个伴生对象的类

kotlin 复制代码
class INSTANCE{
    companion object{
        val data : String = "hello"
    }
}

然后转化成java代码查看

java 复制代码
public final class INSTANCE {
   @NotNull
   private static final String data = "hello";
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
   // 我们发现,其实compaion也是一个静态内部类,通过Compaion去访问
   public static final class Companion {
      @NotNull
      public final String getData() {
         return INSTANCE.data;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

发现Companion是一个静态内部类,外部的INSTCANCE持有了这个内部类。

委托模式(by)

我们看到委托模式,是不是有点像代理模式?待会我们再说一下代理模式和委托模式的区别,先来看一个例子。

kotlin 复制代码
// 先定义一个接口
interface A{
    fun say(words : String)
}

// 定义一个实现类
// 构造函数接收一个A,
// 然后将Speaker对应A的功能全权交给a
class Speaker(a : A) : A by a

那么关键by到底做了什么呢?答案我们就可以通过java代码获取

less 复制代码
// Speaker的实现类
public final class Speaker implements A {
   // $FF: synthetic field
   private final A $$delegate_0;// 内部有一个delegate的A对象
   // Speaker的构造函数,接收一个A对象a
   public Speaker(@NotNull A a) {
      Intrinsics.checkNotNullParameter(a, "a");
      super();
      this.$$delegate_0 = a;
   }
   // Speaker的say方法,可以看到,直接调用了delegate的方法
   public void say(@NotNull String words) {
      Intrinsics.checkNotNullParameter(words, "words");
      this.$$delegate_0.say(words);
   }
}
// A.java
package com.yuanyi.myapplication.kt;

import kotlin.Metadata;
import org.jetbrains.annotations.NotNull;
// 定义了一个接口
public interface A {
   void say(@NotNull String var1);
}

所以,通过java代码看,其实很简单,很像我们的代理模式,只不过我们使用代理模式的目的是为了增强被代理对象的能力。而kotlin的by关键字就是全权交给被委托的对象来操作。

以上举的个别例子只是kotlin众多语法糖的冰上一角。当我们去使用kotlin的语法的时候,要做到知其然,知其所以然。那么通过kotlin->Java代码的方式,对于有java基础的同学去理解kotlin本身是非常好的一种方式。后面我们就来介绍一下kotlin->Java的方法,很简单的。

kotlin->Java

首先,在android studio上,安装一个名为"Kotlin to Java Decompiler"插件,通过Settings->Plugin->Market找到这款插件,如下图:

安装之后重启一下AS。

随便写一个kotlin的代码,然后通过tools->kotlin->Show Kotlin Bytecode

就可以得到kotlin对应的class字节码

点击Decompile就可以得到java代码

本次分享比较简单,重点是分享一下自己是如何理解kotlin语法糖背后的原理的一些方式的。希望能帮到大家。在理解过程中有疑问的,可以留言,大家一起讨论一下。

相关推荐
深海呐3 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang3 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼3 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss4 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
一丝晨光5 小时前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
消失的旧时光-19436 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男7 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽9 小时前
Android 源码集成可卸载 APP
android
码农明明9 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风10 小时前
mariadb主从配置步骤
android·adb·mariadb