Decompiling Kotlin into Java - Few years late

7 minute read

Recently I was asked by a co-worker “what are sealed classes?” and realised the classic answer “enums in steroid” falls really short and a clear sign that I didn’t fully understand what they are. Because of this, I decided to spend some time decompiling Kotlin code into Bytecode and then back into Java with the idea to better understand the language features.

Because this is such a big topic, in this article I’ll cover the very well known and basic features. While boring for the broad audience, I found that there were still a few things to be learned. I’ll probably do a second part of the article discovering more complex features. In this article, we’ll see:

  1. Objects
  2. Classes
  3. Data Classes
  4. Sealed Classes

Objects

Let’s start easy with Objects. Objects in Kotlin are widely known to be equivalent to Java Singletons.

object KotlinObject {
    var aString: String = "foo"
}

After doing the dance to decompile into bytecode and then into Java, we can see its equivalent.

public final class JavaKotlinObject {
    @NotNull
    private static String aString;
    public static final JavaKotlinObject INSTANCE;

    @NotNull
    public final String getAString() {
        return aString;
    }

    public final void setAString(@NotNull String var1) {
        Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
        aString = var1;
    }

    private JavaKotlinObject() {
    }

    static {
        JavaKotlinObject var0 = new JavaKotlinObject();
        INSTANCE = var0;
        aString = "foo";
    }
}

We can see we’ve got a Singleton after decompiling into Java. Something interesting to note is that it’s using an eager implementation and the instance will be created when the class loader loads.

Classes

class MyKotlinRegularClass(
    val aString: String,
    val aNumber: Int
)
public final class MyKotlinRegularClass {
    @NotNull
    private final String aString;
    private final int aNumber;

    @NotNull
    public final String getAString() {
        return this.aString;
    }

    public final int getANumber() {
        return this.aNumber;
    }

    public MyKotlinRegularClass(@NotNull String aString, int aNumber) {
        Intrinsics.checkParameterIsNotNull(aString, "aString");
        super();
        this.aString = aString;
        this.aNumber = aNumber;
    }
}

Even if this is a super simple example, there are some interesting things to talk about:

  • Our Java class is final since our Kotlin class isn’t open.
  • Annotations for null parameters - this however isn’t enough to enforce nullability, that’s why inside the constructor we find nullability checks ( checkParameterIsNotNull) that will throw at runtime. That is why when using Java we lose the nullability safety net we get at compile time when using Kotlin.
  • Getters but not setters since in our Kotlin class all our fields are val (aka immutable).

Let’s now make the class open and add some mutable fields, like this:

open class MutableOpenKotlinClass(
    val aString: String,
    var aMutableString: String,
)

And its Java counterpart:

public class MutableOpenKotlinClass {
    @NotNull
    private final String aString;
    @NotNull
    private String aMutableString;
      
    @NotNull
    public final String getAMutableString() {
        return this.aMutableNullableString;
    }

    public final void setAMutableString(@NotNull String var1) {
        Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
        this.aMutableString = var1;
    }
  
    ...
}

We can now notice that:

  • final is gone, meaning our class can be extended.
  • We’ve got getters and also setters for the properties declared as var.

Data Classes

Data classes are probably the most known Kotlin class types and its adventages over regular Java classes are well known: you get the implementation of equals, toString and hashCode for free. Despite knowing this, seeing its Java counterpart was very surprising for me.

data class MyKotlinDataClass(
    val aString1: String,
    val aString2: String
)
public final class MyJavaKotlinDataClass {
    @NotNull
    private final String aString1;
    @NotNull
    private final String aString2;

    @NotNull
    public final String getAString1() {
        return this.aString1;
    }

    @NotNull
    public final String getAString2() {
        return this.aString2;
    }

    public MyKotlinRegularClass(@NotNull String aString1, @NotNull String aString2) {
        Intrinsics.checkParameterIsNotNull(aString1, "aString1");
        Intrinsics.checkParameterIsNotNull(aString2, "aString2");
        super();
        this.aString1 = aString1;
        this.aString2 = aString2;
    }

    @NotNull
    public final String component1() {
        return this.aString1;
    }

    @NotNull
    public final String component2() {
        return this.aString2;
    }

    @NotNull
    public final MyKotlinRegularClass copy(@NotNull String aString1, @NotNull String aString2) {
        Intrinsics.checkParameterIsNotNull(aString1, "aString1");
        Intrinsics.checkParameterIsNotNull(aString2, "aString2");
        return new MyKotlinRegularClass(aString1, aString2);
    }

    // $FF: synthetic method
    public static MyKotlinRegularClass copy$default(MyKotlinRegularClass var0, String var1, String var2, int var3, Object var4) {
        if ((var3 & 1) != 0) {
            var1 = var0.aString1;
        }

        if ((var3 & 2) != 0) {
            var2 = var0.aString2;
        }

        return var0.copy(var1, var2);
    }

    @NotNull
    public String toString() {
        return "MyKotlinRegularClass(aString1=" + this.aString1 + ", aString2=" + this.aString2 + ")";
    }

    public int hashCode() {
        String var10000 = this.aString1;
        int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
        String var10001 = this.aString2;
        return var1 + (var10001 != null ? var10001.hashCode() : 0);
    }

    public boolean equals(@Nullable Object var1) {
        if (this != var1) {
            if (var1 instanceof MyKotlinRegularClass) {
                MyKotlinRegularClass var2 = (MyKotlinRegularClass)var1;
                if (Intrinsics.areEqual(this.aString1, var2.aString1) && Intrinsics.areEqual(this.aString2, var2.aString2)) {
                    return true;
                }
            }

            return false;
        } else {
            return true;
        }
    }
}

Things to note:

  1. As it’s been mentioned a few times 😅 the Java version is very verbose compared to Kotlin.

  2. We can indeed see the implementation of hashCode, equals and toString.

  3. 2 getters per field: getString1() and getComponent1. Why is this needed?!

    After some head scratching, I bumped into sudonull’s post which explains it pretty well. Turns out it’s needed to port Kotlin’s destructuring declarations:

    val (name, age) = person
    println(name)
    println(age)
    

    Which in Java equals to

    String name = person.component1()
    int age = person.component2()
    println(name)
    println(age)
    
  4. Another adventage of Kotlin’s data classes is the copy method, which allows to create a new object changing some of its properties but keeping the rest unchanged. Strangely, we can notice there are 2 copy methods:

    @NotNull 
    public final MyKotlinRegularClass copy(@NotNull String aString1, @NotNull String aString2) {       
    	Intrinsics.checkParameterIsNotNull(aString1, "aString1");       
    	Intrinsics.checkParameterIsNotNull(aString2, "aString2");       
    	return new MyKotlinRegularClass(aString1, aString2); 
    }   
       
    // $FF: synthetic method   
    public static MyKotlinRegularClass copy$default(MyKotlinRegularClass var0, String var1, String var2, int var3, Object var4) {   
      if ((var3 & 1) != 0) {        
        var1 = var0.aString1;      
      }        
      if ((var3 & 2) != 0) {       
        var2 = var0.aString2;       
      }       
      return var0.copy(var1, var2); 
    }
    

    To be entirely honest, I’m still not sure what the first one is doing, since it simply calls the constructor and the caller needs to provide all the parameters. It doesn’t look the Kotlin’s counterpart at all.

    The second copy it’s more interesting. If you’re like me, you might be wondering:

    what the heck is a synthetic method?

    Thanks to some clever references in the internet, I found out that a synthetic method is a construct introduced by the Java compiler to do extra things that otherwise couldn’t do. For example, when having Java nested class, the compiler will create synthetic getters of its private fields so the enclosing class can access to them.

    Our copy synthetic method is using var3 as a mask to tell which parameters were provided and then it’ll create a copy. As an oddity, Object var4 is no used and looks like it could be removed.

Sealed Classes

sealed classes are enum classes on storoids

I myself have given this answer when asked about sealed classes, but the reality is that it is quite a vague answer and in my personal situation, a flag that I didn’t fully understand what’s going on behind the scenes. Let’s live into details by taking Kotlin’s documentation sealed class example:

interface Expr 
sealed class MathExpr : Expr
data class Const(val number: Double) : MathExpr()
data class Sum(val e1: Expr, val e2: Expr) : MathExpr()
object NotANumber : MathExpr

And its Java counterpart (removing no relevant data class details to reduce noise)

interface Expr {}
abstract class MathExpr implements Expr {  
  private MathExpr() {}
  
  // $FF: synthetic method  
  public MathExpr(DefaultConstructorMarker $constructor_marker) { 
    this();    
  }
}

public final class Sum extends MathExpr { 
  @NotNull   
  private final Expr e1; 
  
  @NotNull  
  private final Expr e2;    
  
  public Sum(@NotNull Expr e1, @NotNull Expr e2) {       
    Intrinsics.checkParameterIsNotNull(e1, "e1");   
    Intrinsics.checkParameterIsNotNull(e2, "e2"); 
    super((DefaultConstructorMarker)null);    
    this.e1 = e1;      
    this.e2 = e2; 
  }   
  
  //...Plus all data classes details...
  
  public final class Const extends MathExpr {  
    private final double number;  
    public Const(double number) {     
      super((DefaultConstructorMarker)null);     
      this.number = number;  
    }
  }
  
  //...Plus all data classes details...

  public final class NotANumber extends MathExpr {  
  	public static final NotANumber INSTANCE;  
    private NotANumber() {}
    static {    
        NotANumber var0 = new NotANumber();   
        INSTANCE = var0;   
    }
  }

Interesting things to note:

  • One to one Expr interface as expected
  • Our sealed class MathExpr implements Expr, is abstract and has a private constructor. This serves us as a reminder that sealed classes cannot be instanciated.
  • Const and Sum (kotlin data classes) and NotANumber (Kotlin object/singleton) subclass (java extends) our MathExpr sealed class.

Some points of interesting when comparing enums and sealed classes:

  • sealed means no more subclasses can appear outside the module, meaning all subclasses of sealed classes are known at compile time. This differs to how traditional inheritance works, where a new subclass can be declared from anywhere.
  • both enum and sealed classes are similar in the sense that they both have a number of values restricted.
  • However, each enum constant exists as a single instance. Sealed classes on the other hand can be an object but also a class, meaning they can hold state. In other words, subclasses of a sealed class can have multiple instances.

I think saying sealed classes are a finite/concrete way to represent hierarchies is a better definition, since it has touches of enums but with multiple instances and touches of inheritance but way more controlled since all subclases are known at compile time.

Conclusions

Summing up, decompilying Kotlin code into Java is a great way to better understand how the language works behind the scenes. I’ll try to create a second part covering features such as Extension Functions, infix, reified, by, delegate and more!

Hope you enjoyed the article. Any questions, my twitter account is the best place to ask. Thanks for your time!