Joao Alves

Engineering Manager at Skyscanner

Kotlin data classes — enough boilerplate

Posted at — Jan 15, 2018

image

Hi everyone, we already looked into some differences between Kotlin and Java syntaxes in the previous 2 articles of the series. Today we’ll look into Kotlin data classes and how concise they are compared to POJOs (Plain Old Java Objects) and how much boilerplate we can get rid of by moving to Kotlin data classes.

First, let’s take a look at a simple POJO representing a Person with 4 properties (name, age, email and phone):

public final class PersonJava {

    private final String name;
    private final int age;
    private final String email;
    private final long phone;

    public PersonJava(String name, int age, String email, long phone) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.phone = phone;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getEmail() {
        return email;
    }

    public long getPhone() {
        return phone;
    }
}

As usual, we have the class declaration on top, followed by all the fields, then the constructor and to finish all the getters. 30 lines of code, that’s a lot, right? And this object only has 4 fields, imagine one of those bigger ones.

But this is not all, you might need to implement toString() and/or equals() and hashCode() in some cases and if you do here’s more code to add:

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

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

    PersonJava that = (PersonJava) o;

    if (age != that.age) {
        return false;
    }
    if (phone != that.phone) {
        return false;
    }
    if (name != null ? !name.equals(that.name) : that.name != null) {
        return false;
    }
    return email != null ? email.equals(that.email) : that.email == null;
}

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

An extra 41 lines making it a total of 75 lines of code if we count some empty lines for separation. If 30 was already a lot for such a simple example, what to say about 75?

Now, the title of the article mentions enough boilerplate code, right? How many lines of code do you think you need in Kotlin to achieve the same? The answer is 1(ONE). Yeah, you read it well, 1 line of code, check out below:

data class Person(val name: String, val age: Int, val email: String, val phone: Long)

In Kotlin we don’t need to declare our fields separately or to implement any getters and setters or even to implement toString(), equals() or hashCode() as everything is handled automatically by the language. So yeah that 1 line of code does exactly the same as the same 75 lines of Java code on the POJO above.

Default and named arguments

What we’ve seen above it’s amazing, right? But that’s not all let’s look at another great Kotlin feature that is particularly interesting when dealing with data classes.

data class Person(val name: String = "default name", val age: Int = 30, 
                  val email: String = "dummy email", val phone: Long = 1234567890)

The code above is the same Person data class as before but now we’re setting default values for each of the properties of a Person. How can we now take advantage of this? Let’s see some examples:

val person1 :Person = Person("name", 25, "email@gmail.com", 555544448)

val person2 :Person = Person()

val person3 :Person = Person("name", 25)

val person4 :Person = Person(name = "name", phone = 9876543210)

person1: in this case, we’re simply creating an instance of our Person object and passing values for all the fields, so we’re not taking advantage of the default values.

person2: but if the default values are exactly what we want we don’t need to pass all the arguments we can just instantiate Person without passing anything.

person3: what if we only care about name and age and we want to keep default email and phone? Then we just pass the first 2 and leave the others out. Because the properties we’re not interested are the last 2 we can instantiate Person like this, but what if we want to set just name and phone and leave the rest as the default values?

person4: this is where named arguments come in. We just need to name the arguments we’re passing and that’s it. You can send all the arguments you want and leave out all the ones that have default values. And the order is not important you could easily now add email as the first argument and it would work perfectly as Kotlin takes the name of the arguments and assigns the values accordingly.

Note: default and named arguments are also valid in regular functions not just in constructors. If you want to call functions using default and named arguments it works in the exact same way.

Immutability

As seen above, Kotlin data classes are final by default and can’t be made open. That’s not true for their properties though, we can make them mutable if we want by using var rather than val when declaring them (see the difference in article number 2) but that’s not something we should do. And why?

If your data class properties are not immutable you’re in danger of messing up your objects without even noticing it, be it from some asynchronous or concurrent code accessing your object and changing those values or because you do it yourself unconsciously. If you use val you’re protected against those dangers. Immutable states make your application more stable predictable rather than the opposite.

So what to do if you want a version of your other object with just some changes? Here comes copy another great API in Kotlin data classes.

Copy

Copying data classes in Kotlin is also super easy. Let’s say we quickly want to get an exact copy of person1 and a person1 with 30 years old and also a person4 with his email rather than default email:

val person1Copy = person1.copy()

val person1With30 = person1.copy(age = 30)

val person4WithEmail = person4.copy(email = "person4@gmail.com")

Super simple right? Again with the help of named arguments, we can easily make copies of any object setting only a couple of relevant changes, the code is so simple that is self-explanatory, right?

Inheritance

Data classes in Kotlin are final by default and can’t be made open so we can’t use inheritance like we do in Java or with normal Kotlin classes. So how do share behaviour and properties between 2 data classes that belong to same super type? Well the obvious answer is to use interfaces but that would be tricky in Java for properties, let’s see how we can do it in Kotlin:

interface Person {
    val name: String
    val age: Int
    val email: String

    fun hasResponsibilities() : Boolean
}

data class Adult(override val name: String, override val age: Int, override val email: String) : Person {
    val isMarried: Boolean = false
    val hasKids: Boolean = false
    override fun hasResponsibilities(): Boolean = true
}

data class Child(override val name: String, override val age: Int, override val email: String = "") : Person {
    override fun hasResponsibilities(): Boolean = false
}

As you can see in the example above we have a Person interface with the same properties we had before minus the phone, and also one function hasResponsibilities() that tells if a person has or not some responsibilities.

Now the difference here is that while in Java Interfaces you can not have fields (they’re basically constants), in Kotlin you can have them and you get a compilation error if you don’t override them in your implementation class constructor or implement getter methods for them. So in Kotlin, we can easily have our Adult and Child data classes implementing Person and overriding the properties in the constructor directly.

For example, Adult can have its own properties, therefore “extending” Person, giving us this inheritance like ability with interfaces. And as seen before we can have default values for arguments so we can define that children don’t have an email by setting it to an empty string by default for example. And then obviously hasResponsibilities() returns true for adults and false for children :)

What if I use AutoValue in Java?

Now, Kotlin data classes are great if you’re doing a new project in Kotlin but if you’re starting to use Kotlin in a project with Java it might not be only rainbows and golden pots, there are some considerations to make. If you use AutoValue and a builder pattern for your models the transition to Kotlin data classes is not straight forward.

@AutoValue
public abstract class PersonAutoValue {

    public abstract String getName();

    public abstract int getAge();

    public abstract String getEmail();

    public abstract int getPhone();

    public static Builder builder() {
        return new AutoValue_PersonAutoValue.Builder()
                .setAge(30);
    }

    @AutoValue.Builder
    public abstract static class Builder {

        public abstract Builder setName(String newName);

        public abstract Builder setAge(int newAge);

        public abstract Builder setEmail(String newEmail);

        public abstract Builder setPhone(int newPhone);

        public abstract PersonAutoValue build();
    }
}

Imagine that you created the same Person example with AutoValue as above, but setting a default age of 30 when we don’t pass one. If you decided to migrate it to a Kotlin data class (our 1 liner) you would have to update all the places where you use it in Java and replace that builder pattern with a regular constructor. And because Java doesn’t know anything about Kotlin default and named arguments you need to pass every single argument every time, let’s see:

PersonAutoValue personAutoValue = PersonAutoValue.builder()
        .setName("name")
        .setEmail("email@gmail.com")
        // .setAge(32) - not mandatory 
        .setPhone(1234567890)
        .build();

Person person = new Person("name", 32, "email@gmail.com", 1234567890);

When instantiating the AutoValue Person we don’t need to set the age as we have a default value but when instantiating our Person data class even if we set up default values we still need to pass them as Java can’t take advantage of Kotlin default or named arguments.

data class OverloadPerson @JvmOverloads constructor(val name: String = "some name",
                                                    val age: Int = 25,
                                                    val email: String = "some email",
                                                    val phone: Long = 1234)

The above is true by default, but if we annotate our data class with @JvmOverloads and set default values to our properties, we will have overloaded constructors that we can use when instantiating our data class from Java Code, let’s see:

OverloadPerson overloadPerson1 = new OverloadPerson("name", 32, "email@gmail.com", 1234567890);
OverloadPerson overloadPerson2 = new OverloadPerson("name", 32, "email@gmail.com");
OverloadPerson overloadPerson3 = new OverloadPerson("name", 32);
OverloadPerson overloadPerson4 = new OverloadPerson("name");

It’s good, right? But as you can see it’s limited, these 4 overloaded constructors are all we get. We have 2 Strings and 2 ints in our model and we obviously can’t create individual overloaded methods for all of them so while this can help for quite simple and small examples like this, it’s definitely not an option for real-life examples. We could still use a companion object and define creator functions that would allow us to build our model as we wish but again for real-life examples this would easily get out of control with even more boilerplate code than what we need when using AutoValue.

So, what we decided to do at Babylon was to leave our AutoValue classes alone if the code where we’re using them is still in Java, and we’ll tackle them later when we migrate the calling code to Kotlin. For new data classes that are used exclusively by Kotlin, we obviously use data classes. This is what I would recommend you to do as well.

It’s not an ideal solution but we decided it was the best way to go for now, there’s no point in wasting a long time refactoring your Java code and making it less readable and harder to use just for the sake of migrating some data models to Kotlin. Their time will come eventually. If you’re doing a 100% Kotlin project though, then definitely go for Kotlin data classes straight away.

Enough boilerplate code right? And enough article for today :) If you enjoyed it don’t forget to 👏 and please share your ideas and comments. As usual, all the code is available in the Series Github project (link below). Check under the dataclasses package and see you at number 5 👋

Kotlin Syntax Part II — when did this switch happen? ⇦ PREVIOUS

NEXT ⇨ Parcelable in Kotlin? Here comes Parcelize


comments powered by Disqus