zsmb.coEst. 2017



Data classes aren't (that) magical

2019.01.16. 16h • Márton Braun

Any time someone is advertising Kotlin as the savior for Java developers, data classes are in the top 3 reasons they list, as being a concise way to replace traditional POJOs.

Don’t get me wrong, data classes are great, but due to the nature of how everyone talks about them (and uses them, I bet), many people don’t know what the data keyword actually does to a class. So let’s clear that up!

Regular classes

Here’s a very simple model class without the data keyword:

class Person(val name: String, var age: Int)

This class, in fact, is already equivalent to what many might call a POJO. In Java terms, it has two fields, getters and setters as appropriate, and a constructor that takes two parameters and sets the fields’ values.

public final class Person {
   private final String name;
   private int age;

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

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

   public final void setAge(int age) {
      this.age = age;
   }

   public Person(String name, int age) {
      this.name = name;
      this.age = age;
   }
}

The data modifier

Let’s make our class a data class now.

data class Person(val name: String, var age: Int)

All this does is give us some additional generated methods, which in terms of Java code, would look something like this (simplified):

public final class Person {
   /* ... All previous fields and methods ... */
    
   public String toString() {
      return "Person(name=" + this.name + ", age=" + this.age + ")";
   }

   public int hashCode() {
      return this.name.hashCode() * 31 + this.age;
   }

   public boolean equals(Object obj) {
      if (this == obj) return true;
      if (!(obj instanceof Person)) return false;
      Person p = (Person) obj;
      return this.name.equals(p.name) && this.age == p.age;
   }
   
   public final String component1() {
      return this.name;
   }

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

   /* Vastly simplified! */
   public final Person copy(String name, int age) {
      return new Person(name, age);
   }
}

The first three methods are fairly self-explanatory, they are just sensible implementations of the Any (or Object, if you will) methods.

Note that the generated equals and hashCode methods of a data class will always consider all properties that are in the primary constructor, and only those. If you need different behaviour, you’ll need to write or generate these methods yourself. This might be a case where you’re better off without a data class.

The advantage, however, is that these methods are generated at compile time, which means they’ll always be up-to-date and make use of every property in the primary constructor. If you actually had these methods in your codebase, you’d have to maintain them yourself when you add, remove, or change a property!

The rest of the methods you get with a data class are Kotlin specific, and should essentially never be used from Java code.

The componentN style methods are a convention to support destructuring declarations. In the case of this class, it allows us to decompose our class into variables in the following way:

val natalie = Person("Natalie", 43)
val (name, age) = natalie

The copy method lets us create a new instance of our data class, which by default contains the exact same values for each property:

val nat = natalie.copy() // Person(name=Natalie, age=43)

It also has an optional parameter (one with a default value) for each property of the data class. You can change any of these one by one as you wish, this is usually done using a named parameter:

val will = natalie.copy(name = "Will") // Person(name=Will, age=43)

As you can see, any parameters not provided will be retained from the instance being copied.

Pros and cons

So, what are the pros of choosing one or the other? (The list of cons are essentially the same, just the other way around.)

Pros for regular classes:

  • Arguably enough for many use cases.
  • Fewer generated methods: you might still care about this on Android, for example.
  • Doesn’t put any restrictions on inheritance: data classes are a bit of a pain in hierarchies.
  • May have non-property constructor parameters: data classes require all primary constructor parameters to be properties.

Pros for data classes:

  • Sensible default implementations of Any methods: makes debugging, comparisons, and usage as keys in Maps easier.
  • Destructuring support.
  • copy method: especially useful for immutable classes.
  • Really, really trendy.

Conclusion

Data classes are awesome and they bring a lot to the table in terms of features. But regular classes in Kotlin don’t tend to get the spotlight they deserve - they are more capable on their own than most people think!

So the next time you create a data class, think about whether you actually need its capabilities, or if you just wanted to create a concise class that holds a couple properties.



You might also like...

Mastering API Visibility in Kotlin

When designing a library, minimizing your API surface - the types, methods, properties, and functions you expose to the outside world - is a great idea. This doesn't apply to just libraries: it's a consideration you should make for every module in a multi-module project.

Effective Class Delegation

One of the most significant items of the Effective Java book is Item 18: Favor composition over inheritance. Let's see how Kotlin promotes this with class delegation.

Building a macOS screen saver in Kotlin

This article documents how I created my own custom screen saver for macOS, built almost entirely in Kotlin, using the powers of Kotlin Multiplatform compiled to macOS. Join me for a tale of Kotlin success and macOS failures.

Coroutine Cancellation 101

A brief introduction to the basics of coroutine cancellation.