Kotlin under the hood - look decompiled bytecode
Viewing Kotlin decompiled in Java bytecode is almost the best way to understand how it works and how some language constructs affect performance. Many of them have done it a long time ago, so this article will be especially relevant for beginners and those who have mastered Java a long time ago and decided to use Kotlin recently.
I specifically miss the rather battered and well-known moments, since it probably makes no sense to write about the generation of getters / setters for var and similar things for the hundredth time. So, let's begin.
How to view decompiled bytecode in Intellij Idea?
Quite simply - just open the file you need and select Tools -> Kotlin -> Show Kotlin Bytecode from the menu.
Next, in the window that appears, just click Decompile.
To view, Kotlin 1.3-RC version will be used.
Now, finally, let's get to the main part.
object
Kotlin
object Test
Decompiled java
publicfinalclassTest{
publicstaticfinal Test INSTANCE;
static {
Test var0 = new Test();
INSTANCE = var0;
}
}
I suppose everyone who deals with Kotlin knows that an object creates a singleton. However, it is far from obvious to everyone what kind of singleton is being created and whether it is thread-safe.
From the decompiled code, you can see that the resulting singleton is similar to the eager implementation of the singleton, it is created at the moment when the classloader loads the class. On one side, the static block is executed when loaded with a classloader, which is thread-safe in itself. On the other hand, if there are more than one classroom, then you can not get rid of one instance.
extensions
Kotlin
fun String.getEmpty(): String {
return""
}
Decompiled java
publicfinalclassTestKt{
@NotNullpublicstaticfinal String getEmpty(@NotNull String $receiver){
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
return"";
}
}
Here, in general, everything is clear - extensions are just syntactic sugar and compiled into the usual static method.
If someone got confused by the line with Intrinsics.checkParameterIsNotNull, then everything is transparent there - in all functions with not nullable arguments, Kotlin adds a null test and throws an exception if you slipped a null
publicstaticvoidcheckParameterIsNotNull(Object value, String paramName){
if (value == null) {
throwParameterIsNullException(paramName);
}
}
What is characteristic, if you write not a function, but an extension property
val String.empty: Stringget() {
return ""
}
Then as a result we will get exactly the same thing that we got for the method String.getEmpty ()
inline
Kotlin
inlinefunsomething() {
println("hello")
}
classTest{
funtest() {
something()
}
}
Decompiled java
publicfinalclassTest{
publicfinalvoidtest(){
String var1 = "hello";
System.out.println(var1);
}
}
publicfinalclassTestKt{
publicstaticfinalvoidsomething(){
String var1 = "hello";
System.out.println(var1);
}
}
With inline, everything is quite simple - a function marked as inline is simply completely inserted in the place from which it was called. What is interesting is that it itself also compiles static, probably for interoperability with Java.
All the power of inline is revealed at the moment when the arguments include lambda :
Kotlin
inlinefunsomething(action: () -> Unit) {
action()
println("world")
}
classTest{
funtest() {
something {
println("hello")
}
}
}
Decompiled java
publicfinalclassTest{
publicfinalvoidtest(){
String var1 = "hello";
System.out.println(var1);
var1 = "world";
System.out.println(var1);
}
}
publicfinalclassTestKt{
publicstaticfinalvoidsomething(@NotNull Function0 action){
Intrinsics.checkParameterIsNotNull(action, "action");
action.invoke();
String var2 = "world";
System.out.println(var2);
}
}
At the bottom, statics is again visible, and at the top, one can see that the lambda in the function argument is also inline, and does not create an additional anonymous class, as is the case with the usual lambda in Kotlin.
Around this knowledge, inline in Kotlin, many end, but there are 2 more interesting points, namely, noinline and crossinline. These are keywords that can be added to a lambda argument in an inline function.
Kotlin
inlinefunsomething(noinline action: () -> Unit) {
action()
println("world")
}
classTest{
funtest() {
something {
println("hello")
}
}
}
Decompiled java
publicfinalclassTest{
publicfinalvoidtest(){
Function0 action$iv = (Function0)null.INSTANCE;
action$iv.invoke();
String var2 = "world";
System.out.println(var2);
}
}
publicfinalclassTestKt{
publicstaticfinalvoidsomething(@NotNull Function0 action){
Intrinsics.checkParameterIsNotNull(action, "action");
action.invoke();
String var2 = "world";
System.out.println(var2);
}
}
With such a record, the IDE begins to indicate that such an inline is useless a little less than completely. And compiles exactly the same as Java - creates Function0. Why decompiled with strange (Function0) null.INSTANCE; - I have no idea, most likely this is a decompiler bug.
Crossinline, in turn, does exactly the same as the usual inline (that is, if you do not write anything at all in front of the lambda), with a few exceptions, you cannot write return in the lambda, which is necessary to block the ability to abruptly terminate the function that calls the inline. In a sense, you can write, but firstly the IDE will swear, and secondly, when compiled we get
'return' is not allowed hereHowever, the crossinline bytecode does not differ from the default inline - the keyword is used only by the compiler.
infix
Kotlin
infixfunInt.plus(value: Int): Int {
returnthis+value
}
classTest{
funtest() {
val result = 5 plus 3
}
}
Decompiled java
publicfinalclassTest{
publicfinalvoidtest(){
int result = TestKt.plus(5, 3);
}
}
publicfinalclassTestKt{
publicstaticfinalintplus(int $receiver, int value){
return $receiver + value;
}
}
Infix functions are compiled as extensions to regular statics.
tailrec
Kotlin
tailrecfunfactorial(step:Int, value: Int = 1):Int {
val newValue = step*value
returnif (step == 1) newValue else factorial(step - 1,newValue)
}
Decompiled java
publicfinalclassTestKt{
publicstaticfinalintfactorial(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 methodpublicstaticint factorial$default(int var0, int var1, int var2, Object var3) {
if ((var2 & 2) != 0) {
var1 = 1;
}
return factorial(var0, var1);
}
}
tailrec is quite an amusing thing. As can be seen from the code, the recursion just gets distilled into a much less readable cycle, but the developer can sleep well, since nothing will fly out from Stackoverflow at the most unpleasant moment. Another thing in real life is to find the use of tailrec rarely.
reified
Kotlin
inlinefun<reified T>something(value: Class<T>) {
println(value.simpleName)
}
Decompiled java
publicfinalclassTestKt{
privatestaticfinalvoidsomething(Class value){
String var2 = value.getSimpleName();
System.out.println(var2);
}
}
In general, about the very concept of reified and why it should be possible to write a whole article. If at a glance, then access to the type itself in Java in compile time is impossible, since before compiling java to know does not know what will be there at all. Kotlin is another matter. The keyword reified can only be used in inline functions, which, as already noted, are simply copied and pasted in the right places, so that during the “call” of the function, the compiler is already aware of what type there is and can modify bytecode.
It should be noted that a static function with a private access level is compiled in bytecode , which means that it will not work like this from Java. By the way, because of the reified advertising Kotlin "100% interoperable with Java and Android"It turns out at least inaccuracy.
Maybe still 99%?
init
Kotlin
classTest{
constructor()
constructor(value: String)
init {
println("hello")
}
}
Decompiled java
publicfinalclassTest{
publicTest(){
String var1 = "hello";
System.out.println(var1);
}
publicTest(@NotNull String value){
Intrinsics.checkParameterIsNotNull(value, "value");
super();
String var2 = "hello";
System.out.println(var2);
}
}
In general, with init, everything is simple - this is the usual inline function that executes before calling the code of the constructor itself.
data class
Kotlin
dataclassTest(val argumentValue: String, val argumentValue2: String) {
var innerValue: Int = 0
}
Decompiled java
publicfinalclassTest{
privateint innerValue;
@NotNullprivatefinal String argumentValue;
@NotNullprivatefinal String argumentValue2;
publicfinalintgetInnerValue(){
returnthis.innerValue;
}
publicfinalvoidsetInnerValue(int var1){
this.innerValue = var1;
}
@NotNullpublicfinal String getArgumentValue(){
returnthis.argumentValue;
}
@NotNullpublicfinal String getArgumentValue2(){
returnthis.argumentValue2;
}
publicTest(@NotNull String argumentValue, @NotNull String argumentValue2){
Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue");
Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2");
super();
this.argumentValue = argumentValue;
this.argumentValue2 = argumentValue2;
}
@NotNullpublicfinal String component1(){
returnthis.argumentValue;
}
@NotNullpublicfinal String component2(){
returnthis.argumentValue2;
}
@NotNullpublicfinal Test copy(@NotNull String argumentValue, @NotNull String argumentValue2){
Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue");
Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2");
returnnew Test(argumentValue, argumentValue2);
}
// $FF: synthetic method@NotNullpublicstatic 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);
}
@NotNullpublic String toString(){
return"Test(argumentValue=" + this.argumentValue + ", argumentValue2=" + this.argumentValue2 + ")";
}
publicinthashCode(){
return (this.argumentValue != null ? this.argumentValue.hashCode() : 0) * 31 + (this.argumentValue2 != null ? this.argumentValue2.hashCode() : 0);
}
publicbooleanequals(@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)) {
returntrue;
}
}
returnfalse;
} else {
returntrue;
}
}
}
Honestly, I didn’t want to mention the date classes, which have already been said so much, but nevertheless there are a couple of moments worthy of attention. First, it is worth noting that only those variables that have been passed to the constructor fall into equals / hashCode / copy / toString. When asked why this is so, Andrei Breslav replied that taking the fields that were not transmitted in the constructor was also difficult and urgent. By the way, the date of the class cannot be inherited, though only because the inherited code would not be correct when inheriting . Secondly, it is worth noting the component1 () method to get the field value. As many componentN () methods are generated as there are arguments in the constructor. It looks useless, but actually it is necessary for the destructuring declaration .
destructuring declaration
For example, use the date class from the previous example and add the following code:
Kotlin
classDestructuringDeclaration{
funtest() {
val (one, two) = Test("hello", "world")
}
}
Decompiled java
publicfinalclassDestructuringDeclaration{
publicfinalvoidtest(){
Test var3 = new Test("hello", "world");
String var1 = var3.component1();
String two = var3.component2();
}
}
Usually this feature is gathering dust on the shelf, but sometimes it can be useful, for example, when working with the contents of the map.
operator
Kotlin
classSomething(var likes: Int = 0) {
operatorfuninc() = Something(likes+1)
}
classTest() {
funtest() {
var something = Something()
something++
}
}
Decompiled java
publicfinalclassSomething{
privateint likes;
@NotNullpublicfinal Something inc(){
returnnew Something(this.likes + 1);
}
publicfinalintgetLikes(){
returnthis.likes;
}
publicfinalvoidsetLikes(int var1){
this.likes = var1;
}
publicSomething(int likes){
this.likes = likes;
}
// $FF: synthetic methodpublicSomething(int var1, int var2, DefaultConstructorMarker var3){
if ((var2 & 1) != 0) {
var1 = 0;
}
this(var1);
}
publicSomething(){
this(0, 1, (DefaultConstructorMarker)null);
}
}
publicfinalclassTest{
publicfinalvoidtest(){
Something something = new Something(0, 1, (DefaultConstructorMarker)null);
something = something.inc();
}
}
The keyword operator is needed in order to override any language statement for a particular class. Honestly, I have never seen anyone use this, but nevertheless there is such an opportunity, but there is no magic inside. In fact, the compiler simply replaces the operator with the desired function, just like typealias is replaced with a specific type.
And yes, if you are thinking about what will happen if you redefine the identity operator (=== which), then I hurry to upset, this is an operator that cannot be redefined.
inline class
Kotlin
inlineclassUser(internalval name: String) {
funupperCase(): String {
return name.toUpperCase()
}
}
classTest{
funtest() {
val user = User("Some1")
println(user.upperCase())
}
}
Decompiled java
publicfinalclassTest{
publicfinalvoidtest(){
String user = User.constructor-impl("Some1");
String var2 = User.upperCase-impl(user);
System.out.println(var2);
}
}
publicfinalclassUser{
@NotNullprivatefinal String name;
// $FF: synthetic methodprivateUser(@NotNull String name){
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
}
@NotNullpublicstaticfinal String upperCase_impl/* $FF was: upperCase-impl*/(String $this) {
if ($this == null) {
thrownew 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;
}
}
@NotNullpublicstatic String constructor_impl/* $FF was: constructor-impl*/(@NotNull String name) {
Intrinsics.checkParameterIsNotNull(name, "name");
return name;
}
// $FF: synthetic method@NotNullpublicstaticfinal User box_impl/* $FF was: box-impl*/(@NotNull String v) {
Intrinsics.checkParameterIsNotNull(v, "v");
returnnew User(v);
}
@NotNullpublicstatic String toString_impl/* $FF was: toString-impl*/(String var0) {
return"User(name=" + var0 + ")";
}
publicstaticint hashCode_impl/* $FF was: hashCode-impl*/(String var0) {
return var0 != null ? var0.hashCode() : 0;
}
publicstaticboolean 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)) {
returntrue;
}
}
returnfalse;
}
publicstaticfinalboolean equals_impl0/* $FF was: equals-impl0*/(@NotNull String p1, @NotNull String p2) {
Intrinsics.checkParameterIsNotNull(p1, "p1");
Intrinsics.checkParameterIsNotNull(p2, "p2");
thrownull;
}
// $FF: synthetic method@NotNullpublicfinal String unbox_impl/* $FF was: unbox-impl*/() {
returnthis.name;
}
public String toString(){
return toString-impl(this.name);
}
publicinthashCode(){
return hashCode-impl(this.name);
}
publicbooleanequals(Object var1){
return equals-impl(this.name, var1);
}
}
Of the limitations - you can use only one argument in the constructor, however, it is understandable, given that the inline class is generally a wrapper over any one variable. An inline class may contain methods, but they represent the usual static. It is also obvious that all necessary methods have been added to support interop with Java.
Total
Do not forget that, firstly, the code is not always decompiled correctly, and secondly, not any code can be decompiled. However, the ability to watch the decompiled Kotlin code is in itself very interesting and can clarify a lot.