1. Introduction
In this article, I would like to introduce you to the concept of data classes in Kotlin and how they can help you in your day-to-day coding. Together, we will learn their purpose, possibilities and a few rules we have to remember about when using them.
2. What Are Data Classes and Why Do We Need Them?
Oftentimes, when implementing any program we may encounter the need for a class, which main purpose will be holding and transferring the data. Database queries results might be a great example here.
For such a case, the creators of Kotlin came up with data classes:
data class Person(val name: String, val age: Int)
Although we can’t see it, adding a data word before a standard Kotlin class results in a few functions being generated automatically:
- equals() and hashCode()
- toString()
- copy()
- component1()…componentN() – for each property declared in the primary constructor
Given these points, data classes allow us to write less verbose, less error-prone and easier to maintain code.
2.1. equals() and hashCode()
With that being said, let’s have a look at the first two functions- equals() and hashCode():
val firstPerson = Person("Peter", 40) val secondPerson = Person("Peter", 40) println(firstPerson.hashCode()) //-1907803204 println(secondPerson.hashCode()) //-1907803204 println(firstPerson == secondPerson)// true
As we can clearly see, both functions work, as expected and the hashCode for two structurally equal instances is exactly the same.
If you would like to learn a bit more about Kotlin equality, then I highly recommend this article from their official documentation.
2.2. toString()
As the next step, let’s let’s have a look at the toString():
val person = Person("John", 20) println(person) // Person(name=John, age=20)
As can be seen, the default Kotlin toString() implementation uses the following format:
ClassName(propertyOne=valueOne, propertyTwo=valueTwo)
2.3. copy()
Nextly, let’s check out the copy() function, which allows us to create a new instance of a data class changing some of its properties:
val beforeCopying = Person("Peter", 30) val afterCopying = beforeCopying.copy( age = 35 ) println(beforeCopying === afterCopying) // false println(afterCopying) // Person(name=Peter, age=35)
A copy function created a totally new instance of Person class (referential equality === returning false is a proof for that) with age set to a new value.
From the technical side, the copy implementation for our class would be as follows:
fun copy(name: String = this.name, age: Int = this.age) = Person(name, age)
2.4. Destructuring Declarations with componentN() Functions
Finally, let’s take a while to understand componentN() functions.
As I’ve mentioned previously, such a function will be generated for each property declared in the primary constructor:
data class Person(val name: String, val age: Int) { lateinit var email: String } val person = Person("John", 20) val personName = person.component1() // John val personAge = person.component2() // 20 val personEmail = person.component3() // ERROR
As we can see, we can reference to the Person name and age using component1() and component2() functions (and yes, in case of more properties, functions component3()… etc. would be generated).
However, the most important thing here is that these functions allow us to destructure an object into variables:
val person = Person("John", 20) val (personName, personAge) = person println(personName) println(personAge)
This syntax in Kotlin is called a destructuring declaration and allows us to keep our code even more concise.
If you would like to learn a bit more about it, then I recommend this article from JetBrains.
3. Data Classes Limitations and Behaviour
So for now, we’ve learned what data classes are in Kotlin and what do they bring to the table. Nevertheless, we have to keep in mind that there are a few rules we have to remember about when working with them.
In this chapter, I will walk you through all of them.
3.1. Open, Sealed, Abstract, Inner
First of all, data classes cannot be open, sealed or abstract. If we declare it with either of them, the code won’t compile informing us with the following message:
Modifier ‘XYZ’ is incompatible with ‘data’
3.2. Primary Constructor
Nextly, we have to remember that primary constructor has to contain at least one parameter:
class One() // OK data class Two() // ERROR
As we can see, the second line will result in a pretty descriptive error thrown:
Data class must have at least one primary constructor parameter
Whatsoever, all primary constructors has to be marked as val or var, so the following code:
data class Person(var name: String, age: Int)
Will lead us to again to the compilation error:
Data class primary constructor must have only property (val / var) parameters.
3.3. copy() and componentN() Implementations
Following, we have to keep in mind that we can’t implement our own copy() and componentN() implementations.
data class Person(val name: String, val age: Int) { fun copy( name: String = this.name, age: Int = this.age ) = Person(name, age) fun component1() : String = "" }
As we can see, both declarations will prohibit our code from compiling because of the conflicting overloads.
On the other hand, we can provide our custom implementation for toString(), hashCode() and equals() functions.
3.4. Default Constructor
Another thing worth remembering when working with JVM is that we have to specify default values for all properties if we would like the default constructor to be generated:
// default constructor won't be generated data class One(val name: String = "", val age: Int) // default constructor generated underneath data class Two(val name: String = "", val age: Int = 0)
Although it might seem like a trifle, some libraries might require us to do so to work properly. A good example here is Jackson and “No Creators, like default construct, exist” error
3.5 Inheritance
Finally, let’s see a few rules related to the inheritance:
- first of all, data classes in Kotlin can extend other classes (although cannot be open)
- secondly, please keep in mind that if the superclass contains a final implementation for toString(), hashCode() or equals(), then none of them will be generated in our data class
- lastly, if the supertype declares open componentN() functions with incompatibile types, then the error will be thrown
4. Summary
And that would be all for this article covering data classes in Kotlin. I know, that all these rules and limitations above seem like plenty of things to deal with, but trust me, you won’t event notice them in real-life scenarios. Data classes are a great feature in Kotlin and sooner or later you will find them useful and this article is definitely worth bookmarking.
I would be happy if you would like to spend one minute and let me know if such materials are useful for you in the comments section or with the contact form.