Data Classes

Where to start?

You probably won’t be able to dedicate time solely to porting your Java code to idiomatic Kotlin. You’ll have to work incrementally, porting parts of your codebase to Kotlin as you continue to add features to the system, maintaining a mixed Java/Kotlin codebase as you do so. Luckily Kotlin’s tooling supports this exceptionally well, and our experience is that you soon see enough productivity benefits from Kotlin that the effort pays for itself. That leaves the question of where to start the conversion effort.

The first time we were in this situation was with a small team that included six developers, building a relatively greenfield project. We had already deployed some small web applications with Kotlin but the enterprise architects insisted that the new system be written in Java 8. This was shortly after Kotlin 1.0 had been released but before Google announced that Kotlin was an official language for Android, so it was understandable that the architects would be wary about committing to a language with an uncertain future for a strategic system that they expected to be around for decades.

Even in Java, we leant towards a functional approach, designing the core application domain model as immutable data types transformed by streams. But eventually we found the limitations of Java too irksome: the verbosity required to implement immutable value types, the distinction between primitive and reference types, null references, Streams lacking common FP operations such as zip, …. By this time Google had made their announcement and Kotlin was being adopted at an ever increasing rate across the industry and within the company, We decided to start converting the Java codebase to Kotlin.

We decided that starting in the core domain model would give us the biggest bang for our buck. Kotlin’s data classes shrunk the code significantly, replacing in some cases hundreds of lines of code with a single declaration. We started carefully, using IntelliJ to convert a small value class that had no dependencies on other classes beyond those in the standard library, and examined how that affected the rest of our Java codebase. It had no effect at all! Emboldened by this success we picked up the pace. Whenever a new feature needed changes to a Java domain model class, we would first convert it to a Kotlin data class, commit the conversion, and then implement the feature.

As more of the domain model logic became implemented purely in Kotlin, we were able to make better use of Kotlin features. For example, we replaced calls to the Stream API with Kotlin’s standard functions on collections and sequences. The biggest improvement, was replacing our use of Java’s Optional type with nullable references. This simplified our code and gave us greater confidence in its null safety.

TBD: another, significantly different, example (Oscar? Did Oscar start by turning UtterlyIdle into HTTP4K to get rid of the dependency injection jibber-jabber?)

As you can see from these different experiences, the choice of starting point depends on a number of factors: why is the team adopting Kotlin, how large is the codebase, how frequent are changes being made to it, how large is the team, … and you probably have reasons aren’t in this list.

If you are choosing Kotlin for its language features it makes sense to convert the classes you are working in most frequently, as we did in the first example above. If you are choosing Kotlin to use a specific library then it makes sense to start writing Kotlin against the API, annotate it to make your Kotlin code convenient to the Java code in the rest of the app, and continue from there.

In a small team it’s easy to establish the Kotlin coding style for your system (beyond the standard style guide) — eg error handling conventions, how code is to be organised into files, what should be a top-level declaration and what should be in an object, etc.

Above a certain size, you run the risk of Kotlin code becoming inconsistent as people establish their own conventions in different parts of the system. So it may be worth starting with a small sub-team working in one area of the system, who establish conventions and build up a body of example code. Once there are some established conventions, you can expand the effort to the rest of the team and other parts of the system.

In the rest of this book we will examine in detail how to progress, how to keep your Java code maintainable while you are introducing Kotlin that it depends upon, how to take advantage of Kotlin’s features to further simplify the code after IntelliJ has performed its conversion magic, and But first, we’ll start, as the team above started, by converting a small, immutable value class from an application’s domain model into Kotlin and turning it into a data class.

Kotlin Data classes are probably the quickest win for most Java projects. Let’s warm up on a very simple example.

A> We’ll skip over configuring your build system to handle Kotlin for now.

package chapter1.java;

import java.util.Objects;

public class Presenter {

    private final String id;
    private final String name;

    public Presenter(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

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

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

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }

}

Looking at the code you can see:

  1. Presenter is immutable.

  2. We want to make sure that two presenters with the same fields compare equal, and have the same hashcode - Presenter is a Value Object. There is quite a lot of code to make that happen, but luckily our IDE has generated it for us.

  3. We come from the school of Java that assumes that everything we pass, store or return is not null unless explicitly indicated otherwise.

IntelliJ has a command to convert a Java source file to Kotlin. It is called (at the time of writing) ‘Convert Java File to Kotlin File’, and is bound to Ctrl+Shift+Alt+K on Windows and Linux, Cmd-Shift-Option-K on Mac. If we run that command on Presenter.java we get

class Presenter(val id: String, val name: String) {

    override fun toString(): String {
        return "Presenter{" +
                "id='" + id + '\''.toString() +
                ", name='" + name + '\''.toString() +
                '}'.toString()
    }

    override fun equals(o: Any?): Boolean {
        if (this === o) return true
        if (o == null || javaClass != o.javaClass) return false
        val presenter = o as Presenter?
        return id == presenter!!.id && name == presenter.name
    }

    override fun hashCode(): Int {
        return Objects.hash(id, name)
    }
}

Take some time to compare the two files.

To our eyes the most noticeable difference is that the in the Kotlin file fields have been moved into brackets after the class name. This is a primary constructor (we’ll come to secondary constructors later). Any parameters to the primary constructor may be labelled as val and will automatically become available as properties. In other words that first line stands in for all this Java

public class Presenter {

    private final String id;
    private final String name;

    public Presenter(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

val is short for ‘value’ and marks a property that you cannot change once set - the equivalent of Java’s final. We could (but almost always wouldn’t) have used var for variable, in which case the property could be modified and a Java setter would be generated.

So far converting our Presenter to Kotlin has saved us 13 lines of code, but we aren’t done yet. Value types like this are so useful but so tedious to get right and keep right that Kotlin supports them at a language level. Mark the class with the data modifier and the compiler will generate the equals, hashCode and toString methods automatically, leaving us with just

data class Presenter(val id: String, val name: String)

Ah that’s better, and when we add properties to a Presenter we won’t have to remember to update the generated methods or find ourselves with hard-to-diagnose bugs.

Now there is so little code, it’s easy to pick out a few more differences between Java and Kotlin.

Firstly, the default visibility in Kotlin is public, so public class Presenter can just be class Presenter.

A> If you are the same sort of Java programmer as we were you may question this language design decision. Our experience working with the language is that it fits well with a more data-oriented design philosophy, where less state has to be hidden. See Chapter TODO.

Secondly, type specifiers in Kotlin come after the identifier, not before it. So instead of String id we have id: String. This turns out to play nicer with complex nested types and we found that we very quickly didn’t notice it at all.

Finally, we have been able to remove the body of the class altogether, so there isn’t an empty {} pair.

You haven’t seen them yet, but we had some tests for our Presenter.

package chapter1.java;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class PresenterTests {

    private Presenter nat = new Presenter("1", "Nat");
    private Presenter duncan = new Presenter("2", "Duncan");

    @Test
    void properties() {
        assertEquals("1", nat.getId());
        assertEquals("Duncan", duncan.getName());
    }

    @Test
    void equality() {
        assertEquals(nat, new Presenter("1", "Nat"));
        assertNotEquals(nat, duncan);
    }

    @Test
    void testHashCode() {
        assertEquals(nat.hashCode(), new Presenter("1", "Nat").hashCode());
        assertNotEquals(nat.hashCode(), duncan.hashCode());
    }
}

Due to the excellent interoperation between Kotlin and Java, these continue to pass with the converted class. Take a moment to think what that implies about the generated class.

Our Java Presenter had explicit getId and getName methods. These are not in the converted Kotlin, but still our Java can call them. This means that the Kotlin compiler is not only generating the equals, hashCode and toString methods we knew about, but also getId and getName. id and name are not fields in Kotlin, they are properties.

A> We’ll have a lot to say about properties later, but for now if you think of them as a private field with a public getter you won’t often be wrong.

Let’s convert the tests themselves to Kotlin

class PresenterTests {

    private val nat = Presenter("1", "Nat")
    private val duncan = Presenter("2", "Duncan")

    @Test
    fun properties() {
        assertEquals("1", nat.id)
        assertEquals("Duncan", duncan.name)
    }

    @Test
    fun equality() {
        assertEquals(nat, Presenter("1", "Nat"))
        assertNotEquals(nat, duncan)
    }

    @Test
    fun testHashCode() {
        assertEquals(nat.hashCode(), Presenter("1", "Nat").hashCode())
        assertNotEquals(nat.hashCode(), duncan.hashCode())
    }
}

As usual, take a few moments to compare before and after.

The first thing that we spot is that the Java fields, nat and duncan are declared as val properties but we haven’t had to repeat their type - Presenter nat = new Presenter(...);. Kotlin would allow us to say val nat: Presenter = Presenter(...) but does not require it here.

A> Properties can be declared with val if they don’t change, and var if they do. Even though the fields were not declared as final in the Java; in this case the conversion has been clever enough to see that they are not changed and to convert them to val.

Looking at the properties, we see that there is no new keyword to construct instances of classes - ‘invoking’ the class name as a function calls the relevant constructor.

At the end of the line, if you’re already used to reading languages other than Java you may not notice the lack of semicolons to terminate statements. They are optional in Kotlin - you can use them to separate statements on a single line, but if the compiler can make sense of code by pretending that there is one on the end of a line it will.

Moving on, we might infer that the Java void keyword is replaced in Kotlin by fun. Actually though, methods in Kotlin are marked by fun and, where they return nothing, do not have to declare a return type.

Looking in the body of the methods, where Java called nat.getId(), Kotlin accesses the property nat.id. In actual fact Kotlin will call the getId that it has generated (or one supplied by a Java class) rather than accessing a field directly - so nothing has really changed except that we can drop the get and ().

A> There is quite a bit of subtlety to properties. We will keep on returning to this subject.

Stepping away from the Java, and comparing with our previous Kotlin class, Presenter, we see that in the tests the class properties are not declared in the primary constructor. JUnit requires a default (no argument) constructor, and where properties are not initialised through a constructor (like nat and duncan here) they are not declared in the primary constructor but rather inside the class body.

Conclusions

  • Convert to Kotlin is handy
  • But does not do all the work for you
  • Minimum ceremony
  • Convenience
  • Favour immutability
  • Data classes FTW

Looking in the codebase in which we found Presenter, we find another class ripe for conversion to a data class.

package chapter1.java;

import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import static java.util.Arrays.asList;


public class Session {
    public final String title;
    @Nullable
    public final String subtitleOrNull;
    public final Slots slots;
    public final List<Presenter> presenters;

    public Session(String title, @Nullable String subtitle, Slots slots, List<Presenter> presenters) {
        this.title = title;
        this.subtitleOrNull = subtitle;
        this.slots = slots;
        this.presenters = Collections.unmodifiableList(new ArrayList<>(presenters));
    }

    public Session(String title, @Nullable String subtitle, Slots slots, Presenter... presenters) {
        this(title, subtitle, slots, asList(presenters));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Session session = (Session) o;
        return Objects.equals(title, session.title) &&
                Objects.equals(subtitleOrNull, session.subtitleOrNull) &&
                Objects.equals(slots, session.slots) &&
                Objects.equals(presenters, session.presenters);
    }

    @Override
    public int hashCode() {
        return Objects.hash(title, subtitleOrNull, slots, presenters);
    }

    @Override
    public String toString() {
        return "Session{" +
                "title='" + title + '\'' +
                ", subtitle='" + subtitleOrNull + '\'' +
                ", slots=" + slots +
                ", presenters=" + presenters +
                '}';
    }

    public Session withPresenters(List<Presenter> newLineUp) {
        return new Session(title, subtitleOrNull, slots, newLineUp);
    }

    public Session withTitle(String newTitle) {
        return new Session(newTitle, subtitleOrNull, slots, presenters);
    }

    public Session withSubtitle(@Nullable String newSubtitle) {
        return new Session(title, newSubtitle, slots, presenters);
    }
}
package chapter1.java;


import org.junit.jupiter.api.Test;

import static java.util.Arrays.asList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

public class SessionTests {

    static Presenter alice = new Presenter("id1", "Alice");
    static Presenter bob = new Presenter("id2", "Bob");
    static Presenter carol = new Presenter("id3", "Carol");

    Session aSession = new Session("The Title", null, new Slots(1, 2), alice);

    @Test
    public void can_change_presenters() {
        assertThat(
            aSession.withPresenters(asList(bob, carol)),
            equalTo(new Session("The Title", null, new Slots(1, 2), bob, carol)));
    }

    @Test
    public void can_change_title() {
        assertThat(
            aSession.withTitle("Another Title"),
            equalTo(new Session("Another Title", null, new Slots(1, 2), alice)));
    }

    @Test
    public void can_change_subtitle() {
        assertThat(
            aSession.withSubtitle("The Subtitle"),
            equalTo(new Session("The Title", "The Subtitle", new Slots(1, 2), alice)));
    }

    @Test
    public void can_remove_subtitle() {
        assertThat(
            aSession.withSubtitle("The Subtitle").withSubtitle(null),
            equalTo(new Session("The Title", null, new Slots(1, 2), alice)));
    }
}

Session is another Value Object, with tedious equals, hashCode and toString methods, but this time it has copy methods, the withPresenters etc.

subtitleOrNull is both named and annotated to let us know that it may not be present, so we had better check for null before using the subtitle. Some shops would have had a getter returning an Optional, but in this case it seems that we took advantage of the class' immutability to leave out the getters in favour of direct public field access.

Finally note that we’ve chosen to defensively copy the list of presenters passed into the constructor into an unmodifiableList in order to prevent clients mutating the list and breaking our value semantics.

For such a simple class there are a lot of hoops to jump through. Let’s convert to Kotlin to see what we get.

class Session(val title: String, val subtitleOrNull: String?, val slots: Slots, presenters: List<Presenter>) {
    val presenters: List<Presenter>

    init {
        this.presenters = Collections.unmodifiableList(ArrayList(presenters))
    }

    constructor(title: String, subtitle: String?, slots: Slots, vararg presenters: Presenter) : this(
        title,
        subtitle,
        slots,
        asList<Presenter>(*presenters)
    ) {
    }

    override fun equals(o: Any?): Boolean {
        if (this === o) return true
        if (o == null || javaClass != o.javaClass) return false
        val session = o as Session?
        return title == session!!.title &&
            subtitleOrNull == session.subtitleOrNull &&
            slots == session.slots &&
            presenters == session.presenters
    }

    override fun hashCode(): Int {
        return Objects.hash(title, subtitleOrNull, slots, presenters)
    }

    override fun toString(): String {
        return "Session{" +
            "title='" + title + '\''.toString() +
            ", subtitle='" + subtitleOrNull + '\''.toString() +
            ", slots=" + slots +
            ", presenters=" + presenters +
            '}'.toString()
    }

    fun withPresenters(newLineUp: List<Presenter>): Session {
        return Session(title, subtitleOrNull, slots, newLineUp)
    }

    fun withTitle(newTitle: String): Session {
        return Session(newTitle, subtitleOrNull, slots, presenters)
    }

    fun withSubtitle(newSubtitle: String?): Session {
        return Session(title, newSubtitle, slots, presenters)
    }
}

At the time of writing IntelliJ’s convert to Kotlin doesn’t do a great job on this code, but we can learn a lot by tidying, so let’s work our way though it.

The first new thing is that subtitleOrNull has the type String?. This is Kotlin for a String that may be null. The conversion wasn’t clever enough to look at the name, but instead took heed of the @Nullable annotation. We’ll look at the ramifications of nullability in the typesystem later.

Next note that presenters is a constructor parameter that isn’t declared as val, but later there is a presenters property, and that it is initialised in an init block, which has access to the constructor parameters by name.

That’s all a bit odd - the conversion seems to have been confused by our defensive copying of presenters. Alt-Enter on the val gives the option to ‘Join declaration and assignment’

val presenters: List<Presenter> = Collections.unmodifiableList(ArrayList(presenters))

and then we can remove that vestigial ArrayList construction as well.

val presenters: List<Presenter> = Collections.unmodifiableList(presenters)

Moving on we see that Kotlin allows for other constructors to be declared with the constructor keyword. Here we have a single secondary constructor which takes its presenters as varargs for convenience, especially in test code, and converts them to a List to present to the primary constructor.

constructor(title: String, subtitle: String?, slots: Slots, vararg presenters: Presenter) :
    this(
        title,
        subtitle,
        slots,
        asList<Presenter>(*presenters)
    ) {
}

That asList<Presenter>(*presenters) is too ugly to live - it’s invoking Java’s Arrays.asList(T... a). A better Kotlin expression would be toList() so we will substitute that, and at the same time remove those useless secondary constructor braces.

class Session(val title: String, val subtitleOrNull: String?, val slots: Slots, presenters: List<Presenter>) {
    val presenters: List<Presenter> = Collections.unmodifiableList(presenters)

    constructor(title: String, subtitle: String?, slots: Slots, vararg presenters: Presenter) :
        this(
            title,
            subtitle,
            slots,
            presenters.toList()
        )
}

// equals etc

A subtlety of the conversion is that while the original Java had presenters as List, the Kotlin has them as List. OK, maybe that explanation is itself too subtle, because in the Kotlin case the List is not `java.util.List` but `kotlin.collections.List`. Whereas the Java interface has methods to mutate the list (add and remove elements, replace them, sort in place etc), the Kotlin version does not.

The Kotlin list is not truly immutable, but instead read only. If we ignore this subtlety, and you generally should in your Kotlin code, then we can take it on trust that the presenters list passed to the primary constructor will not be changed, so that we don’t have to make a defensive copy. In that case the property can be moved into the primary constructor.

class Session(
    val title: String,
    val subtitleOrNull: String?,
    val slots: Slots,
    val presenters: List<Presenter>
) {
    constructor(title: String, subtitle: String?, slots: Slots, vararg presenters: Presenter) :
        this(
            title,
            subtitle,
            slots,
            presenters.toList()
        )
}

// equals etc

Now that all primary constructor parameters are vals (or vars) our class is eligible for conversion to a data class, allowing us to remove the boilerplate methods and implement the Java copy methods with the Kotlin copy method.

data class Session(
    val title: String,
    val subtitleOrNull: String?,
    val slots: Slots,
    val presenters: List<Presenter>
) {
    constructor(title: String, subtitle: String?, slots: Slots, vararg presenters: Presenter) :
        this(
            title,
            subtitle,
            slots,
            presenters.toList()
        )

    fun withPresenters(newLineUp: List<Presenter>): Session {
        return this.copy(presenters = newLineUp)
    }

    fun withTitle(newTitle: String): Session {
        return this.copy(title = newTitle)
    }

    fun withSubtitle(newSubtitle: String?): Session {
        return this.copy(subtitleOrNull = newSubtitle)
    }
}
[ If you liked this, you could share it on Twitter. ]