1. Overview

The Kotlin language has introduced the concept of Data Classes, whose main purpose is to simply hold data without the boilerplate code needed in Java to create a POJO. Simply put, Kotlin’s solution enables us to avoid writing getters, setters, equals, and hashCode methods, so it makes the model classes cleaner and more readable.

In this quick article, we’ll have a look at Data Classes in Kotlin and compare them with their Java counterparts.

2. Kotlin Setup

To get started setting up the Kotlin project, check our Introduction to the Kotlin Language tutorial.

3. Data Classes in Java

If we wanted to create a Task entry in Java, we’d need to write a lot of boilerplate code:

public class Task {
    private int id;
    private String description;
    private int priority;

    public Task(int id, String description, int priority) {
        this.id = id;
        this.description = description;
        this.priority = priority;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public float getPriority() {
        return priority;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = Integer.hashCode(this.id) * prime;
        result = prime * result + Integer.hashCode(this.priority);
        result = prime * result + ((this.description == null) ? 0 : this.description.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object var1) {
        if (this != var1) {
            if (var1 instanceof Task) {
                Task var2 = (Task) var1;
                if (this.id == var2.id
                  && Intrinsics.areEqual(this.description, var2.description)
                  && this.priority == var2.priority) {
                    return true;
                }
            }
            return false;
        } else {
            return true;
        }
    }

    @Override
    public String toString() {
        return "Task [id=" + id + ", description=" + description + ", priority=" + priority + "]";
    }
}

69 lines of code! That’s a lot to store only three fields in a simple class.

4. Kotlin Data Classes

Now, let’s redefine the same Task class, with the same functionality, as a Kotlin Data Class:

data class Task(
    var id: Int,
    var description: String,
    var priority: Int
)

As we can see, that’s massively easier and cleaner. Kotlin generates the basic functions that must be overloaded for a good model class, giving us good toString(), hashCode(), and equals() functions. Kotlin also provides some extra functionality in the form of a copy() function, and various componentN() functions, which are important for variable destructuring.

4.1. Usage

We instantiate a data class the same way as other classes:

val task= Task(1001, "Replace Fuel Tank Filler Caps", 5)

Now, the properties and functions are available:

println(task.id) // 1001
println(task.description) // Replace Fuel Tank Filler Caps
println(task.priority) // 5

task.priority = 4

println(task.toString())

4.2. Copy Function

The copy() function is created for us in case we need to copy an object, altering some of its properties but keeping the rest unchanged:

val copyTask = task.copy(priority = 4)
println(copyTask.toString())

Java doesn’t provide a clear, native way for copying/cloning objects. We could use the Clonable interface, SerializationUtils.clone(), or a cloning constructor.

4.3. Destructuring Declarations

Destructuring declarations allow us to treat objects’ properties as individual values. For each property in our data class, a componentN() function is generated:

task.component1()
task.component2()
task.component3()

We can also create multiple variables from the object or directly from a function – it’s important to remember about using brackets:

val(id, description, priority) = task

fun getTask() = movie
val(idf, descriptionf, priorityf) = getTask()

4.4. Data Class Restrictions

In order to create a data class, we have to consider the following conditions:

  • The class declaration must start with data
  • It must have at least one constructor parameter
  • All constructor parameters must be vals or vars
  • A data class can’t be open, sealed, abstract, or an inner classes
  • The parent of a data class isn’t available to the compiler to use in defining the generated copy() function

If the generated class needs to have a parameterless constructor, default values for all properties have to be specified:

data class Task(var id: Int = 1000, var description: String = "", var priority: Int= 0)

5. Java Records Compatibility

As of Kotlin 1.5, we can make Kotlin data classes compile as Java 14+ records. To make that happen, all we have to do is to annotate the data class with the @JvmRecord annotation:

@JvmRecord
data class Person(val firstName: String, val lastName: String)

In order to compile this, we can use kotlinc:

>> kotlinc -jvm-target 15 -Xjvm-enable-preview Person.kt

If we’re targeting Java 15 or older versions, we have to enable the preview JVM versions via the -Xjvm-enable-preview flag. As of Java 16, however, records are stable Java features. Therefore, if we’re targeting Java 16 or newer versions, we don’t need to enable the preview features:

>> kotlinc -jvm-target 16 Person.kt

Now, if we take a peek at the generated bytecode, we’ll see that the Person class extends the java.lang.Record class:

>> javap -c -p com.baeldung.dataclass.Person
Compiled from "Person.kt"
public final class com.baeldung.dataclass.Person extends java.lang.Record {
    // omitted
}

Since Java records are immutable, we can’t use var declarations for data classes annotated with @JvmRecord:

@JvmRecord // won't compile
data class Person(val firstName: String, var lastName: String)

Here, the compiler will fail with the following error message:

Constructor parameter of @JvmRecord class should be a val

Moreover, this type of data class can’t extend other classes, as it’s already extending the Record superclass.

6. Conclusion

We’ve seen Data Classes in Kotlin, their usage and requirements, the reduced amount of boilerplate code required, and comparisons with the same code in Java.

To learn more about Kotlin, check articles such as Kotlin Java Interoperability and the already mentioned Introduction to the Kotlin Language.

The full implementation of these examples can be found over on GitHub.

Comments are closed on this article!