📜 ⬆️ ⬇️

Consider Kotlin more closely

image


https://trends.google.com/trends/explore?q=%2Fm%2F0_lcrx4


The above is a screenshot of Google Trends when I searched for the word "kotlin". A sudden surge is when Google announced that Kotlin is becoming the main language in Android . It happened at the Google I / O conference a few weeks ago. To date, you have either used this language before, or are interested in it, because everyone around you suddenly started talking about it.


One of the main features of Kotlin is its mutual compatibility with Java: you can call Kotlin code from Java, and Java code from Kotlin. This is perhaps the most important feature by which the language is widely distributed. You do not need to migrate everything at once: just take a piece of the existing code base and start adding the Kotlin code, and this will work. If you experiment with Kotlin and you do not like it, then you can always refuse it (although I recommend trying it).


When I first used Kotlin after five years in Java, some things seemed like real magic to me.


“Wait, what? Can I just write a data class to avoid templating code? ”
“Stop, so if I write apply , then I no longer need to define an object every time I want to call a method with reference to it?”


After the first sigh of relief from the fact that at last there was a language that does not look outdated and cumbersome, I began to feel some discomfort. If mutual compatibility with Java is required, how exactly all these great features are implemented in Kotlin? What's the catch?


This article is dedicated to. I was very interested in finding out how the Kotlin compiler transforms concrete constructs so that they become interoperable with Java. For my research, I chose the four most requested methods from the Kotlin standard library :


  1. apply
  2. with
  3. let
  4. run

When you read this article, you will no longer need to fear. Now I feel much more confident, because I understood how everything works, and I know that I can trust the language and the compiler.


Apply


 /** *    [block]   `this`        `this`. */ @kotlin.internal.InlineOnly public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this } 

apply is simple: it is an extension function that executes the block parameter in relation to an extended type instance (it is called the "receiver") and returns the receiver itself.


There are many ways to use this feature. You can bind the creation of an object to its initial configuration:


val layout = LayoutStyle().apply { orientation = VERTICAL }


As you can see, we provide the configuration for the new LayoutStyle right at creation, which contributes to the purity of the code and the implementation, which is much less error prone . Did it happen to call the method in relation to the wrong instance, because it had the same name? Or even worse, when refactoring was completely wrong? With the above approach it will be much more difficult to face such troubles. Also note that it is not necessary to define the this parameter: we are in the same scope as the class itself . It’s as if we were extending the class itself, so this is implicitly specified.


But how does this work? Let's take a quick example.


 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } fun main(vararg args: Array<String>) { val layout = LayoutStyle().apply { orientation = VERTICAL } } 

Thanks to the IntelliJ IDEA Show Kotlin bytecode tool ( Tools > Kotlin > Show Kotlin Bytecode ), we can see how the compiler converts our code into JVM bytecode:


 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 2 ALOAD 2 ASTORE 3 ALOAD 3 GETSTATIC kotlindeepdive/Orientation.VERTICAL : Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V ALOAD 2 ASTORE 1 

If you are not too good at bytecode, then I suggest reading these wonderful articles , after them you will understand much better (remember that when calling each method, the stack is accessed, so the compiler needs to load the object each time).


We will sort by points:


  1. A new instance of LayoutStyle and duplicated LayoutStyle stack.
  2. Called constructor with zero parameters.
  3. The operations are performed store / load (about this - below).
  4. The value of Orientation.VERTICAL passed to the stack.
  5. Called setOrientation , which raises an object and a value from the stack.

Here we note a couple of things. First, no magic is involved, everything happens as expected: the LayoutStyle method is called for the setOrientation instance we created. In addition, the apply function is nowhere to be seen, because the compiler inline it .


Moreover, the bytecode is almost identical to the one generated by using only Java! Judge for yourself:


 // Java enum Orientation { VERTICAL, HORIZONTAL; } public class LayoutStyle { private Orientation orientation = HORIZONTAL; public Orientation getOrientation() { return orientation; } public void setOrientation(Orientation orientation) { this.orientation = orientation; } public static void main(String[] args) { LayoutStyle layout = new LayoutStyle(); layout.setOrientation(VERTICAL); } } // Bytecode NEW kotlindeepdive/LayoutStyle DUP ASTORE 1 ALOAD 1 GETSTATIC kotlindeepdive/Orientation.VERTICAL : kotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (kotlindeepdive/Orientation;)V 

Tip: You may have noticed a large number of ASTORE/ALOAD . They are inserted by the Kotlin compiler, so the debugger works for lambdas too! We will talk about this in the last section of the article.


With


 /** *    [block]   [receiver]       . */ @kotlin.internal.InlineOnly public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() 

with looks similar to apply , but there are some important differences. First, with not a type extension function: the receiver must be explicitly passed as a parameter. Moreover, with returns the result of the block function, and apply - the recipient itself.


Since we can return anything, this example looks very believable:


 val layout = with(contextWrapper) { // `this` is the contextWrapper LayoutStyle(context, attrs).apply { orientation = VERTICAL } } 

Here you can omit the contextWrapper prefix. for context and attrs , because contextWrapper is the receiver of the with function. But even in this case, the application methods are not so obvious as compared to apply , this function may be useful under certain conditions.


Given this, let us return to our example and see what happens if we use with :


 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation = VERTICAL } fun main() { val layout = with(SharedState) { LayoutStyle().apply { orientation = previousOrientation } } } 

The recipient with is a SharedState singleton, it contains an orientation parameter (orientation parameter) that we want to set for our layout. Inside the block function, we create an instance of LayoutStyle , and thanks to apply we can simply set the orientation, reading it from SharedState .


Look again at the generated bytecode:


 GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState; ASTORE 1 ALOAD 1 ASTORE 2 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 3 ALOAD 3 ASTORE 4 ALOAD 4 ALOAD 2 INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V ALOAD 3 ASTORE 0 RETURN 

Nothing special. Singleton extracted, implemented as a static field in the SharedState class; an instance of LayoutStyle is created in the same way as before, a constructor is called, another call to get the previousOrientation value inside a SharedState and the last call to assign a value to an instance of LayoutStyle .


Tip: when using Show Kotlin Bytecode, you can click Decompile and see the Java representation of the bytecode created for the Kotlin compiler. Spoiler: it will be exactly as you expect!


Let


 /** *    [block]   `this`      . */ @kotlin.internal.InlineOnly public inline fun <T, R> T.let(block: (T) -> R): R = block(this) 

let very useful when working with objects that can be null. Instead of creating endless chains of if-else expressions, you can simply combine the operator ? (called the “safe call operator”) with let : as a result, you get a lambda, in which the argument it is a non-nullable version of the original object.


 val layout = LayoutStyle() SharedState.previousOrientation?.let { layout.orientation = it } 

Consider the whole example:


 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation: Orientation? = VERTICAL } fun main() { val layout = LayoutStyle() // layout.orientation = SharedState.previousOrientation -- this would NOT work! SharedState.previousOrientation?.let { layout.orientation = it } } 

Now previousOrientation can be null. If we try to assign it directly to our layout, the compiler will be indignant, because the nullable type cannot be assigned to the non-nullable type. Of course, you can write an if expression, but this will lead to a double reference to the SharedState.previousOrientation expression. And if we use let , we get a non-nullable link to the same parameter that can be safely assigned to our layout.
From the point of view of bytecode, everything is very simple:


 NEW kotlindeepdive/let/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/let/LayoutStyle.<init> ()V GETSTATIC kotlindeepdive/let/SharedState.INSTANCE : Lkotlindeepdive/let/SharedState; INVOKEVIRTUAL kotlindeepdive/let/SharedState.getPreviousOrientation ()Lkotlindeepdive/let/Orientation; DUP IFNULL L2 ASTORE 1 ALOAD 1 ASTORE 2 ALOAD 0 ALOAD 2 INVOKEVIRTUAL kotlindeepdive/let/LayoutStyle.setOrientation (Lkotlindeepdive/let/Orientation;)V GOTO L9 L2 POP L9 RETURN 

It uses a simple IFNULL conditional transition, which, in fact, you would have to do manually, except for this time, when the compiler effectively executes it for you, and the language offers a pleasant way of writing such code. I think this is great!


Run


There are two versions of the run: the first is a simple function, the second is an extension function of a generic type (generic type). Since the first only calls the function block, which is passed as a parameter, we will analyze the second.


 /** *    [block]   `this`      . */ @kotlin.internal.InlineOnly public inline fun <T, R> T.run(block: T.() -> R): R = block() 

Perhaps, run is the simplest of the functions considered. It is defined as an extension function of the type, whose instance is then passed as the receiver and returns the result of executing the block function. It may seem that run is a hybrid of let and apply , and this is true. The only difference is in the return value: in the case of apply we return the receiver itself, and in the case of run , the result of the block function (as in let ).


This example emphasizes the fact that run returns the result of the block function, in this case, the assignment ( Unit ):


 enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = HORIZONTAL } object SharedState { val previousOrientation = VERTICAL } fun main() { val layout = LayoutStyle() layout.run { orientation = SharedState.previousOrientation } // returns Unit } 

Equivalent bytecode:


 NEW kotlindeepdive/LayoutStyle DUP INVOKESPECIAL kotlindeepdive/LayoutStyle.<init> ()V ASTORE 0 ALOAD 0 ASTORE 1 ALOAD 1 ASTORE 2 ALOAD 2 GETSTATIC kotlindeepdive/SharedState.INSTANCE : Lkotlindeepdive/SharedState; INVOKEVIRTUAL kotlindeepdive/SharedState.getPreviousOrientation ()Lkotlindeepdive/Orientation; INVOKEVIRTUAL kotlindeepdive/LayoutStyle.setOrientation (Lkotlindeepdive/Orientation;)V RETURN 

run was inline, like other functions, and it all comes down to simple method calls. Here, too, there is nothing strange!




We noted that there are many similarities between the functions of the standard library: this is done intentionally to cover as many applications as possible. On the other hand, it is not so easy to understand which of the functions is best suited for a particular task, given the small differences between them.


To help you deal with the standard library, I drew a table that summarizes all the differences between the main features considered (except also ):


image


Appendix: additional store/load operations


I still could not fully understand when comparing "Java-bytecode" and "Kotlin-bytecode". As I said before, in Kotlin, unlike Java, there were additional operations astore/aload . I knew that it was somehow connected with lambdas, but I could figure out why they were needed.


It seems that these additional operations are necessary for the debugger to process lambdas as stack frames , which allows us to intervene (step into) in their work. We can see what the local variables are, who causes the lambda, who will be called from the lambda, etc.


But when we send the APK to production, we don’t care about the debugger’s capabilities, right? So, these functions can be considered redundant and subject to removal, despite their small size and insignificance.


For this, ProGuard , a well-known and well-loved tool, can be suitable. It works at the bytecode level and, in addition to obfuscation and cutting, also performs optimization passes to make the bytecode more compact. I wrote the same piece of Java and Kotlin code, applied it to both versions of ProGuard with one set of rules, and compared the results. That's what came to light.


ProGuard configuration


 -dontobfuscate -dontshrink -verbose -keep,allowoptimization class kotlindeepdive.apply.LayoutStyle -optimizationpasses 2 -keep,allowoptimization class kotlindeepdive.LayoutStyleJ 

Source


Java:


 package kotlindeepdive enum OrientationJ { VERTICAL, HORIZONTAL; } class LayoutStyleJ { private OrientationJ orientation = HORIZONTAL; public OrientationJ getOrientation() { return orientation; } public LayoutStyleJ() { if (System.currentTimeMillis() < 1) { main(); } } public void setOrientation(OrientationJ orientation) { this.orientation = orientation; } public OrientationJ main() { LayoutStyleJ layout = new LayoutStyleJ(); layout.setOrientation(VERTICAL); return layout.orientation; } } 

Kotlin:


 package kotlindeepdive.apply enum class Orientation { VERTICAL, HORIZONTAL } class LayoutStyle { var orientation = Orientation.HORIZONTAL init { if (System.currentTimeMillis() < 1) { main() } } fun main() { val layout = LayoutStyle().apply { orientation = Orientation.VERTICAL } layout.orientation } } 

Baytkod


Java:


  sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c LayoutStyleJ.class Compiled from "SimpleJ.java" final class kotlindeepdive.LayoutStyleJ { public kotlindeepdive.LayoutStyleJ(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: aload_0 5: getstatic #6 // Field kotlindeepdive/OrientationJ.HORIZONTAL$5c1d747f:I 8: putfield #5 // Field orientation$5c1d747f:I 11: invokestatic #9 // Method java/lang/System.currentTimeMillis:()J 14: lconst_1 15: lcmp 16: ifge 34 19: new #3 // class kotlindeepdive/LayoutStyleJ 22: dup 23: invokespecial #10 // Method "<init>":()V 26: getstatic #7 // Field kotlindeepdive/OrientationJ.VERTICAL$5c1d747f:I 29: pop 30: iconst_1 31: putfield #5 // Field orientation$5c1d747f:I 34: return } 

Kotlin:


  sgotti@Sebastianos-MBP ~/Desktop/proguard5.3.3/lib/PD/kotlindeepdive > javap -c apply/LayoutStyle.class Compiled from "Apply.kt" public final class kotlindeepdive.apply.LayoutStyle { public kotlindeepdive.apply.LayoutStyle(); Code: 0: aload_0 1: invokespecial #13 // Method java/lang/Object."<init>":()V 4: aload_0 5: getstatic #11 // Field kotlindeepdive/apply/Orientation.HORIZONTAL:Lkotlindeepdive/apply/Orientation; 8: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation; 11: invokestatic #14 // Method java/lang/System.currentTimeMillis:()J 14: lconst_1 15: lcmp 16: ifge 32 19: new #8 // class kotlindeepdive/apply/LayoutStyle 22: dup 23: invokespecial #16 // Method "<init>":()V 26: getstatic #12 // Field kotlindeepdive/apply/Orientation.VERTICAL:Lkotlindeepdive/apply/Orientation; 29: putfield #10 // Field orientation:Lkotlindeepdive/apply/Orientation; 32: return } 

Conclusions after comparing the two bytecode listings:


  1. Additional astore/aload in “Kotlin-bytecode” disappeared, because ProGuard considered them redundant and immediately deleted (curiously, this required two optimization passes, after one they were not deleted).
  2. “Java bytecode” and “Kotlin bytecode” are almost identical. In the first there are interesting / strange moments when working with enum-value, and in Kotlin there is nothing like that.

Conclusion


It's great to get a new language offering so many possibilities to developers. But it is also important to know that we can rely on the tools used, and feel confident when working with them. I'm glad I can say: “I trust Kotlin”, in the sense that I know: the compiler does nothing extra or risky. It only does what we need to do manually in Java, saving us time and resources (and returns the long lost joy of coding for the JVM). To some extent, this also benefits the end users, because, due to more stringent type safety, we will leave fewer bugs in the applications.


In addition, the Kotlin compiler is constantly improving, so that the generated code becomes more efficient. So no need to try to optimize Kotlin-code using the compiler, it is better to focus on writing more efficient and idiomatic code, leaving everything else to the compiler.


')

Source: https://habr.com/ru/post/331442/


All Articles