Kotlin Uncovered: Part 2
Learning about Kotlin classes through decompilation
41.jpg by Startup Stock Photos is licensed under CC0
One of the tools we can use to learn about Kotlin is decompilation. Because Kotlin compiles down to bytecode, we can decompile it into Java to learn some interesting things. If you missed my first blog post introducing this topic, you can get caught up here.
The first thing I tried decompiling to Java was a simple class, and the results were exciting. Let’s look at a User
class with a firstName
and lastName
. Both attributes are String
s. The first name is non-null and immutable, while the last name is nullable and mutable.
class User( val firstName: String, var lastName: String? )
That’s the entire class. By using val
and var
we make the fields immutable and mutable, respectively. Then, the question mark after String
for lastName
makes it nullable. Let’s put this through the decompiler.
For the full class, check out this gist. We’ll break down the individual parts here.
The first thing to notice is the class declaration. Kotlin classes are final by default, so you can’t extend from them. Immutability is a great practice to follow. By using immutability, we facilitate simpler classes and thread-safe objects. It makes for a couple more steps when working with libraries that depend on classes being extendable, such as declaring it as open
, but the trade-off is worth it.
public final class User {
Then, we have the fields. Notice they are private by default, which is great. Also notice that the firstName
is final, which reflects the immutability we wanted. Lastly, we get the @NotNull
and @Nullable
annotations.
@NotNull private final String firstName; @Nullable private String lastName;
Here is our constructor. Notice the first line of the body: checkParameterIsNotNull()
. With nullability built into the type system, we are usually pretty safe with Kotlin. But what about if we are calling it from Java? Kotlin does an extra check for us just in case. Not only does it make this check if the parameter is null when it shouldn’t be, but it also gives us a stack trace that clearly shows what was null and where it was.
public User(@NotNull String firstName, @Nullable String lastName) { Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; }
Finally, we have our getters and setters. Notice that they reflect our desired immutability. There is a getter and setter for lastName
, but there is only a getter for firstName
. We also have the @NonNull
and @Nullable
annotations. You’ll continue to see those all over as we decompile more examples.
@NotNull public final String getFirstName() { return this.firstName; } @Nullable public final String getLastName() { return this.lastName; } public final void setLastName(@Nullable String var1) { this.lastName = var1; }
Very quickly we can start to see the work Kotlin does for us in reducing boilerplate code. We get so much from a few lines of code, which only contains the information we care about, thus reducing the cognitive overhead.
We got a lot from investigating this class, and we can get even more by making it a data class. Catch the next blog post in this series to see the features we get by adding the single keyword: data
.
Need to jump arond and check out other parts of the series? You can find them below:
Comments
This is soooo good, but how do we keep up with knowing when you post the next one??
Really enjoying your series on Kotlin! Very well explained, and makes me excited to learn more!
How does the constructor allow something else before calling super()?
@AG the constructor can do “something” because the call is static:
Intrinsics.checkParameterIsNotNull(firstName, “firstName”);
Nothing there depends on the current type being created yet. The “firstName” string is passed in the constructor, therefore available.