You never touched Groovy, nor did you jump on the Scala train. Clojure never attracted you; and you heard about Ceylon long after the language had already died. You are one of those old-fashioned Java folks! But now, after all those years, you want to join the cool Kotlin kids. So, where to start? Let’s discover the language together by decompiling it to Java code. Today: the Data class!

Today: the Data class

We have seen the standard Kotlin class. Now let’s delve into a specialized class, the data class. The data class can be compared to a Java record; which is a class that acts as transparent carrier for immutable data. Creating a class is as simple as creating a standard class, with the exception the data keyword is put in front of it:

data class User(val name: String, val age: Int)

The documentation states the Kotlin compiler automatically adds following functions to the class:

  • equals() / hashCode() pair

  • toString() of the form "User(name=John, age=42)"

  • componentN() functions used for destructuring

  • copy()

Now let’s put those claims to the test. Once we decompile the bytecode back to Java code, we can see the following class:

public final class User {
   @NotNull
   private final String name;
   private final int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getAge() {
      return this.age;
   }

   public User(@NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

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

   public final int component2() {
      return this.age;
   }

   @NotNull
   public final User copy(@NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      return new User(name, age);
   }

   // $FF: synthetic method
   public static User copy$default(User user, String name, int age, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         name = user.name;
      }

      if ((var3 & 2) != 0) {
         age = user.age;
      }

      return user.copy(name, age);
   }

   @NotNull
   public String toString() {
      return "User(name=" + this.name + ", age=" + this.age + ")";
   }

   public int hashCode() {
      return (this.name != null ? this.name.hashCode() : 0) * 31 + Integer.hashCode(this.age);
   }

   public boolean equals(@Nullable Object other) {
      if (this != other) {
         if (other instanceof User user) {
            if (Intrinsics.areEqual(this.name, user.name) && this.age == user.age) {
               return true;
            }
         }

         return false;
      }
      return true;
   }
}

Ah, it looks like the decompiled code does indeed match the documentation! The class is well readable and has the same behaviour as the record does in Java[1].

But then again, if a data class acts like a record, shouldn’t it just be a record? Well, Kotlin’s data classes are way older than Java records, so by default those classes are just compiled as 'normal' classes. But no worries, you can change this behaviour by adding a @JvmRecord annotation to your data class:

@JvmRecord
data class User(val name: String, val age: Int)

And lo, the class is compiled as record:

public record User() {
   @NotNull
   private final String name;
   private final int age;

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

   public final int age() {
      return this.age;
   }

   // rest of the file is exactly the same
}

Well, that’s enough for one day. Stay tuned for more!


1. That is finals, only getters, equals, hashCode and toString; see also https://www.baeldung.com/java-record-keyword.
shadow-left