Kotlin — A deeper look

It feels like magic… But is it really?

What you’re looking at is Google Trends, where I’ve looked up “kotlin”. Can you see that sudden spike? That’s when Google announced that the Kotlin programming language is now a first-class citizen in Android, at its Google I/O conference that took place a couple of weeks ago.

By now, either you were already using it in the past, or you have been diving your face into the language because everyone is suddenly talking about it.

One of the most prominent features of the language is the interoperability with Java: this means that you can call Kotlin code from Java in the same way that you can call Java code from Kotlin. This is (and has been) probably the most important peculiarity that pushed the adoption forward. You do not need to migrate everything at once: you can simply take a piece of your existing code base and start adding Kotlin code, and just like that, it’ll work. You can experiment with Kotlin, and if you don’t like what you see, you can always go back (even though I dare you to do so).

When I first started using Kotlin at Clue, coming from 5 years of Java, some things felt just like magic.

“Wait, what? I can simply write data class to avoid boilerplate?” “Wait, so if I write apply then I no longer need to specify the object every time I want to invoke a method on it?”

After the initial sigh of relief for finally having a language that doesn’t feel old and cumbersome, I started feeling a little uncomfortable. If interoperability with Java is a requirement, how exactly does Kotlin implement these nice language features? What’s the catch?

This is what this article is about. I was super curious about knowing how the Kotlin compiler translates certain constructs so that they can interoperate with Java, and I chose to take a look at the four most used methods of the Kotlin Standard Library:

apply with let run

After reading this, you should not feel scared anymore. I feel way more confident now that I know how things work, and I know I can trust the language and the compiler.

Apply

apply is pretty simple: it is an extension function that executes the block parameter on the instance of the extended type (called “receiver”) and returns the receiver itself.

There are many use cases where this function comes in hand. You may bind the creation of an object to its initial configuration, like so:

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

As you can see, we are providing a configuration for our new LayoutStyle right at the creation site, which contributes to a cleaner code and to a much less error prone implementation. Has it ever happened to you to call methods on the wrong instance, just because of a similar name? Or worse, a refactoring gone horribly wrong? With this approach it is definitely harder to fall into these pits.

Also, note how we do not have to specify the this parameter: because we are in the same scope as the class itself, it’s as if we were extending that very class, thus this is implicit.

But how does that work? Let’s take a look at a brief example. Consider this simple snippet:

Thanks to IntelliJ IDEA’s “Show Kotlin bytecode” tool ( Tools > Kotlin > Show Kotlin Bytecode ), we can inspect how the compiler translates our code into JVM bytecode:

If you’re not too familiar with bytecode, I suggest you to read these great articles that will give you a much clearer idea (in this case, one important thing to keep in mind is that every method invocation pops the stack, so the compiler needs to load the object every time).

Let’s break this down:

Create a new instance of LayoutStyle and duplicate it on the stack Call the constructor with zero parameters Do a bunch of store/load (more on that later) Push the Orientation.VERTICAL value to the stack Invoke setOrientation , which pops the object and the value from the stack

We notice a couple of things here. First of all, there is no magic behind the scene, it all happens as you would expect: the setOrientation method is called on the LayoutStyle instance that we have created. In addition, the apply function is nowhere to be seen, because the compiler was instructed to inline it.

And most of all, the bytecode is almost identical to the one that is generated if you do the same thing in Java! Just see for yourself:

Pro Tip: you may have noticed a lot of ASTORE/ALOAD operations. Those are inserted by the Kotlin compiler so that the debugger works also for lambdas! We’re going to elaborate this in the last section of the article.

With

with may look similar to apply , but denotes some prominent differences. First of all, with is not an extension function over a type: the receiver has to be explicitly passed as a parameter. Moreover, with returns the result of the block function, whereas apply returns the receiver itself.

Since we have the freedom to return whatever we please, something like this is totally plausible:

val layout = with(contextWrapper) {

// `this` is the contextWrapper

LayoutStyle(context, attrs).apply { orientation = VERTICAL }

}

In this example we can omit the contextWrapper. prefix for context and attrs because contextWrapper is the receiver of the with function. Even though the use cases are far less obvious than what you can think for apply , this function can become really useful in particular circumstances.

With that in mind, let’s go back to our example and see what happens if we use with :

The receiver for with is a singleton called SharedState , which contains an orientation parameter that we would like our layout to have. Inside the block function we create the LayoutStyle instance and, thanks to apply , we are simply able to set the orientation with the one we read from the SharedState .

Now let’s look again at the generated bytecode:

Again, there is really nothing special here. The singleton, which is implemented as a static field on the SharedState class, is retrieved; the LayoutStyle instance is created just like before, there’s a call to the constructor, another invocation to get the value for previousOrientation inside SharedState and one last invocation to assign it to our LayoutStyle instance.

Pro Tip: when using “Show Kotlin Bytecode”, you can also press “Decompile” to see a Java representation of the bytecode produced by the Kotlin compiler. Spoiler alert: it’s exactly what you would expect!

Let

let is very useful when you’re dealing with nullable objects. Instead of chaining endless if-else statements, you can simply combine the ? operator (called “safe call operator”) with let : what you end up with is a lambda where the argument it is a not-nullable version of the original object.

val layout = LayoutStyle ()

SharedState.previousOrientation?.let { layout.orientation = it }

Let’s see the entire example:

Now that previousOrientation is nullable, if we tried to assign that directly to our layout, the compiler would complain because a nullable type cannot be assigned to a non-nullable type. Of course we could write an if statement, but that would mean referencing the SharedState.previousOrientation value twice: by using let instead, we get a not-nullable reference to the same parameter, which can safely be assigned to our layout.

From a bytecode perspective, it’s very straightforward:

It all resorts to a simple conditional jump IFNULL , which is essentially what you would have done by hand, except this time the compiler does that efficiently for you and the language offers you a nice way of writing that code. I think this is awesome!

Run

There are two versions of run, one is a simple function and the other is an extension function over a generic type. Since the former does nothing more than calling the block function that is passed as a parameter, we’re going to focus the analysis on the latter.

run is probably the simplest function among the ones that we have met so far. It is defined as an extension function over a type, whose instance is then passed as the receiver, and returns the result of executing the block function. You might think that run is somehow a hybrid between let and apply , and you would be right, the only difference being the return value: in the case of apply we return the receiver itself, in the case of run we return the result of the block function (just like we do on let ).

So the following example highlights the fact that run returns the result of the block function, so in this case an assignment ( Unit ):

The bytecode equivalent is then: