Kotlin深入了解-参见反编译的字节码



查看用Java反编译的Kotlin字节码也许是了解它仍然如何工作以及某些语言结构如何影响性能的最佳方法。 许多人很久以前就已经自己完成了此操作,因此,本文对初学者和那些已经精通Java并决定最近使用Kotlin的人尤其重要。

我将有意错过一些颇为陈词滥调的时刻,因为大概是第100次写关于var和类似事物的getter / setter方法的生成是毫无意义的。 因此,让我们开始吧。

如何在Intellij Idea中查看反编译的字节码?


非常简单-只需打开所需文件,然后在菜单中选择工具-> Kotlin->显示Kotlin字节码

图片

接下来,在出现的窗口中,只需单击反编译



为了观看,将使用Kotlin 1.3-RC的版本。
现在,最后,让我们继续进行主要部分。

对象


科特林

object Test 

反编译的Java

 public final class Test { public static final Test INSTANCE; static { Test var0 = new Test(); INSTANCE = var0; } } 

我想与Kotlin交往的每个人都知道该对象会创建一个单例。 但是,对于每个人来说,确切地创建哪个单例以及它是否是线程安全的还远远不够。

反编译的代码表明,接收到的单例类似于急切的单例实现,它是在类加载器加载类时创建的。 一方面,当静态块由类驱动程序加载时执行,该块本身是线程安全的。 另一方面,如果教室驾驶员多于一个,那么您将无法获得一份副本。

扩展名


科特林

 fun String.getEmpty(): String { return "" } 

反编译的Java

 public final class TestKt { @NotNull public static final String getEmpty(@NotNull String $receiver) { Intrinsics.checkParameterIsNotNull($receiver, "receiver$0"); return ""; } } 

在这里,一般来说,一切都很清楚-扩展只是语法糖,并被编译为常规的静态方法。

如果有人对Intrinsics.checkParameterIsNotNull感到困惑,那么那里的一切都是透明的-在所有带有不可为null的参数的函数中,Kotlin会添加一个null检查并在您忽略null 猪时抛出异常,尽管您承诺不会在参数中这样做。 看起来像这样:

 public static void checkParameterIsNotNull(Object value, String paramName) { if (value == null) { throwParameterIsNullException(paramName); } } 

如果您编写的不是函数,而是扩展属性,那是典型的情况

 val String.empty: String get() { return "" } 

然后,结果,我们得到的与String.getEmpty()方法完全相同的东西

内联


科特林

 inline fun something() { println("hello") } class Test { fun test() { something() } } 

反编译的Java

 public final class Test { public final void test() { String var1 = "hello"; System.out.println(var1); } } public final class TestKt { public static final void something() { String var1 = "hello"; System.out.println(var1); } } 

使用inline,一切都非常简单-标记为inline的函数将被完全完全插入到调用它的位置。 有趣的是,它还可以将自身编译为静态变量,可能是为了与Java互操作。

lambda出现在参数中的那一刻,内联的所有功能就会显现出来:

科特林

 inline fun something(action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } } 

反编译的Java

 public final class Test { public final void test() { String var1 = "hello"; System.out.println(var1); var1 = "world"; System.out.println(var1); } } public final class TestKt { public static final void something(@NotNull Function0 action) { Intrinsics.checkParameterIsNotNull(action, "action"); action.invoke(); String var2 = "world"; System.out.println(var2); } } 

在下部,静态变量再次可见,在上部,很明显,函数参数中的lambda也已内联,并且不会像Kotlin中通常的lambda那样创建其他匿名类。

围绕此,许多Kotlin的内联知识结束了,但是还有两个有趣的观点,即noinline和crossinline。 这些是可以分配给作为内联函数参数的lambda的关键字。

科特林

 inline fun something(noinline action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } } 

反编译的Java
 public final class Test { public final void test() { Function0 action$iv = (Function0)null.INSTANCE; action$iv.invoke(); String var2 = "world"; System.out.println(var2); } } public final class TestKt { public static final void something(@NotNull Function0 action) { Intrinsics.checkParameterIsNotNull(action, "action"); action.invoke(); String var2 = "world"; System.out.println(var2); } } 

有了这样的记录,IDE开始表明这样的内联是完全没有用的。 它的编译方式与Java完全相同-创建Function0。 为什么用奇怪的(Function0)null.INSTANCE进行反编译; -我不知道,这很可能是反编译器错误。

反过来,crossinline的功能与常规内联完全相同(也就是说,如果参数中的lambda之前没有写任何东西),除了少数例外,return不能写在lambda中,这对于阻止突然终止调用内联函数的功能是必需的。 从某种意义上说,您可以编写一些东西,但是首先,IDE会发誓,其次,在编译时,我们得到了
这里不允许“返回”
但是,交叉内联字节码与默认内联没有区别-关键字仅由编译器使用。

中缀


科特林

 infix fun Int.plus(value: Int): Int { return this+value } class Test { fun test() { val result = 5 plus 3 } } 

反编译的Java

 public final class Test { public final void test() { int result = TestKt.plus(5, 3); } } public final class TestKt { public static final int plus(int $receiver, int value) { return $receiver + value; } } 

中缀函数像常规静态函数的扩展一样进行编译

尾巴


科特林

 tailrec fun factorial(step:Int, value: Int = 1):Int { val newValue = step*value return if (step == 1) newValue else factorial(step - 1,newValue) } 

反编译的Java

 public final class TestKt { public static final int factorial(int step, int value) { while(true) { int newValue = step * value; if (step == 1) { return newValue; } int var10000 = step - 1; value = newValue; step = var10000; } } // $FF: synthetic method public static int factorial$default(int var0, int var1, int var2, Object var3) { if ((var2 & 2) != 0) { var1 = 1; } return factorial(var0, var1); } } 

tailrec是一件相当有趣的事情。 从代码中可以看到,递归只是进入了一个不太容易理解的周期,但是开发人员可以安然入睡,因为在最不愉快的时刻,没有东西会飞出Stackoverflow。 现实生活中的另一件事是很少找到tailrec。

整齐的


科特林

 inline fun <reified T>something(value: Class<T>) { println(value.simpleName) } 

反编译的Java

 public final class TestKt { private static final void something(Class value) { String var2 = value.getSimpleName(); System.out.println(var2); } } 

通常,关于修饰本身的概念及其必要性,您可以撰写整篇文章。 简而言之,在编译时无法在Java中访问类型本身,因为 在编译Java之前,没有人知道那里到底有什么。 Kotlin是另一回事。 reified关键字只能在内联函数中使用,正如已经指出的那样,这些内联函数可以简单地复制并粘贴到正确的位置,因此,在“调用”函数期间,编译器已经知道它是什么类型,并且可以修改字节码。

您应该注意以下事实:具有私有访问级别的静态函数是用字节码编译的,这意味着这将无法在Java中运行。 顺便说一下,由于在Kotlin广告中做了“与Java和Android 100%可互操作”的修正,因此至少获得了不准确性。

图片

也许毕竟是99%?

初始化


科特林

 class Test { constructor() constructor(value: String) init { println("hello") } } 

反编译的Java

 public final class Test { public Test() { String var1 = "hello"; System.out.println(var1); } public Test(@NotNull String value) { Intrinsics.checkParameterIsNotNull(value, "value"); super(); String var2 = "hello"; System.out.println(var2); } } 

通常,使用init,一切都很简单-这是一个普通的内联函数,可以调用构造函数本身的代码之前进行工作。

资料类别


科特林

 data class Test(val argumentValue: String, val argumentValue2: String) { var innerValue: Int = 0 } 

反编译的Java

 public final class Test { private int innerValue; @NotNull private final String argumentValue; @NotNull private final String argumentValue2; public final int getInnerValue() { return this.innerValue; } public final void setInnerValue(int var1) { this.innerValue = var1; } @NotNull public final String getArgumentValue() { return this.argumentValue; } @NotNull public final String getArgumentValue2() { return this.argumentValue2; } public Test(@NotNull String argumentValue, @NotNull String argumentValue2) { Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue"); Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2"); super(); this.argumentValue = argumentValue; this.argumentValue2 = argumentValue2; } @NotNull public final String component1() { return this.argumentValue; } @NotNull public final String component2() { return this.argumentValue2; } @NotNull public final Test copy(@NotNull String argumentValue, @NotNull String argumentValue2) { Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue"); Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2"); return new Test(argumentValue, argumentValue2); } // $FF: synthetic method @NotNull public static Test copy$default(Test var0, String var1, String var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.argumentValue; } if ((var3 & 2) != 0) { var2 = var0.argumentValue2; } return var0.copy(var1, var2); } @NotNull public String toString() { return "Test(argumentValue=" + this.argumentValue + ", argumentValue2=" + this.argumentValue2 + ")"; } public int hashCode() { return (this.argumentValue != null ? this.argumentValue.hashCode() : 0) * 31 + (this.argumentValue2 != null ? this.argumentValue2.hashCode() : 0); } public boolean equals(@Nullable Object var1) { if (this != var1) { if (var1 instanceof Test) { Test var2 = (Test)var1; if (Intrinsics.areEqual(this.argumentValue, var2.argumentValue) && Intrinsics.areEqual(this.argumentValue2, var2.argumentValue2)) { return true; } } return false; } else { return true; } } } 

老实说,我不想提及日期类,关于日期类已经讲了很多,但是仍然有几点值得注意。 首先,值得注意的是,只有传递给构造函数的变量才能进入equals / hashCode / copy / toString。 对于为什么会这样的问题,安德烈·布雷斯拉夫(Andrei Breslav)回答说,获取未在构造函数中传输的字段也很困难和困难。 顺便说一句,从类日期继承是不可能的,事实只是因为在继承期间生成的代码将是不正确的 。 其次,值得注意的是component1()方法获取字段值。 构造函数中会生成尽可能多的componentN()方法。 它看起来没用,但是您确实需要它来进行销毁声明

销毁声明


例如,我们将使用上一个示例中的date类,并添加以下代码:

科特林

 class DestructuringDeclaration { fun test() { val (one, two) = Test("hello", "world") } } 

反编译的Java

 public final class DestructuringDeclaration { public final void test() { Test var3 = new Test("hello", "world"); String var1 = var3.component1(); String two = var3.component2(); } } 

通常,此功能是在架子上收集灰尘,但有时在例如处理地图内容时很有用。

算子


科特林

 class Something(var likes: Int = 0) { operator fun inc() = Something(likes+1) } class Test() { fun test() { var something = Something() something++ } } 

反编译的Java

 public final class Something { private int likes; @NotNull public final Something inc() { return new Something(this.likes + 1); } public final int getLikes() { return this.likes; } public final void setLikes(int var1) { this.likes = var1; } public Something(int likes) { this.likes = likes; } // $FF: synthetic method public Something(int var1, int var2, DefaultConstructorMarker var3) { if ((var2 & 1) != 0) { var1 = 0; } this(var1); } public Something() { this(0, 1, (DefaultConstructorMarker)null); } } public final class Test { public final void test() { Something something = new Something(0, 1, (DefaultConstructorMarker)null); something = something.inc(); } } 

需要使用operator关键字来覆盖特定类的某些语言运算符。 老实说,我从未见过有人使用过此功能,但是尽管如此,但里面没有任何魔术。 实际上,编译器只是将运算符替换为所需的函数,就像将typealias替换为特定类型一样。
是的,如果现在您考虑过重新定义身份运算符(=== which)会发生什么,那么我赶紧让您烦恼,这是一个无法重新定义的运算符。

内联类


科特林

 inline class User(internal val name: String) { fun upperCase(): String { return name.toUpperCase() } } class Test { fun test() { val user = User("Some1") println(user.upperCase()) } } 

反编译的Java

 public final class Test { public final void test() { String user = User.constructor-impl("Some1"); String var2 = User.upperCase-impl(user); System.out.println(var2); } } public final class User { @NotNull private final String name; // $FF: synthetic method private User(@NotNull String name) { Intrinsics.checkParameterIsNotNull(name, "name"); super(); this.name = name; } @NotNull public static final String upperCase_impl/* $FF was: upperCase-impl*/(String $this) { if ($this == null) { throw new TypeCastException("null cannot be cast to non-null type java.lang.String"); } else { String var10000 = $this.toUpperCase(); Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()"); return var10000; } } @NotNull public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String name) { Intrinsics.checkParameterIsNotNull(name, "name"); return name; } // $FF: synthetic method @NotNull public static final User box_impl/* $FF was: box-impl*/(@NotNull String v) { Intrinsics.checkParameterIsNotNull(v, "v"); return new User(v); } @NotNull public static String toString_impl/* $FF was: toString-impl*/(String var0) { return "User(name=" + var0 + ")"; } public static int hashCode_impl/* $FF was: hashCode-impl*/(String var0) { return var0 != null ? var0.hashCode() : 0; } public static boolean equals_impl/* $FF was: equals-impl*/(String var0, @Nullable Object var1) { if (var1 instanceof User) { String var2 = ((User)var1).unbox-impl(); if (Intrinsics.areEqual(var0, var2)) { return true; } } return false; } public static final boolean equals_impl0/* $FF was: equals-impl0*/(@NotNull String p1, @NotNull String p2) { Intrinsics.checkParameterIsNotNull(p1, "p1"); Intrinsics.checkParameterIsNotNull(p2, "p2"); throw null; } // $FF: synthetic method @NotNull public final String unbox_impl/* $FF was: unbox-impl*/() { return this.name; } public String toString() { return toString-impl(this.name); } public int hashCode() { return hashCode-impl(this.name); } public boolean equals(Object var1) { return equals-impl(this.name, var1); } } 

从局限性出发,您只能在构造函数中使用一个参数,但是,由于内联类通常是对任何一个变量的包装,因此这是可以理解的。 内联类可能包含方法,但它们只是静态的。 同样很明显,已经添加了所有必需的方法来支持Java互操作。

总结


不要忘记,首先,代码不会总是正确地反编译,其次,并不是每个代码都可以反编译。 但是,观看Kotlin反编译代码本身的能力非常有趣并且可以澄清很多。

Source: https://habr.com/ru/post/zh-CN425077/


All Articles