loading...

Data classes in Kotlin

grahamcox82 profile image Graham Cox ・3 min read

(Crazy busy day today, so simple article to meet my quota. Sorry)

I mentioned before about Kotlin being significantly streamlined as compared to Java. And one of the most obvious places it achieves this, that you are likely to see very early on, is in Data Classes.

Data classes are just classes. There's nothing special about them from that point. The actual "data" keyword was originally nothing more than an annotation that the compiler saw and reacted to. (I'm honestly not sure if this is still the case I'm afraid). As such, they can do (almost) anything that a normal class can do.

The point of Data Classes is that - believe it or not - they are meant to represent data, and not functionality. This is the definition of a Java Bean from Java-land. And that is exactly what they are. Only significantly more streamlines. When you add the "data" keyword (or annotation, or whatever it is) in front of the class definition, what you are telling the compiler to do is:

  • Generate a class
  • With this set of fields
  • With getters (and optionally setters) for every field
  • With an equals and hashCode method that is correct
  • With a toString method that is correct
  • With a copy method
  • With a set of componentN methods

The first three of those are just part of how Kotlin defines classes anyway. That's nothing special. It's the other 4 that are where data classes really come into it.

Writing equals, hashCode and toString methods isn't hard. Writing them correctly can be though. But even then, most of the time people either have their IDE generate them automatically or else they use something like Commons-Lang Equals/HashCode/ToStringBuilder classes. And that's fine, but not without cost. The generated ones from the IDE will get stale as soon as the class changes, unless you remember to keep them updated. The Builder classes will either suffer the same problems, or else will use reflection with the performance cost that incurs.

The final set of methods are simply some helper methods. The copy method lets you take one object, and create a new one with the same values. Or with some of the same values. You can selectively pick and choose which values you want to replace with new ones. For example:

val user = User(username = "graham", email = "my.email@example.com", banned = false)
val copiedUser = user.copy(email = "my.real.email@example.com)

In this case, copiedUser has:

And you only had to specify one of those values.

The componentN methods are used by Destructuring, which is a topic in itself. Simply put though, it lets you take a class and decompose it into individual fields. This is very useful in a few places - such as pattern matching - when you don't care about the class itself but just the individual fields.

In short though, data classes aren't anything special. They don't really do anything. They just allow you to not write a lot of boilerplate for what are essentially generated methods. You can write a Java Bean with a number of fields, and get all of the correct JavaBean methods for everything you need, and a few more on top, in a single line of code. To steal an example from Reddit, it's turning this:

public class User {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String component1() {
        return name;
    }

    public int component2() {
        return age;
    }

    public User copy() {
        return new User(name, age);
    }

    public User copy(String newName) {
        return new User(newName, age);
    }

    public User copy(String newName, int newAge) {
        return new User(newName, newAge);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

into this:

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

Discussion

markdown guide
 

Most of my articles will be way way simpler than this, for meeting my 100 days quota 😊

 

Yeah - turns out I don't know how to write short articles... ;) My emails at work are similar. I'm well known there for how long they can get!