1. Introduction
While working with Kotlin, we have access to a variety of features designed to boost our productivity. One such feature is Data Classes, which automatically generate essential utility functions like equals(), hashCode(), toString(), and copy(). However, when we use objects in Kotlin while working with sealed hierarchies, we encounter an inconsistency. We usually have to write additional boilerplate code to deal with that inconsistency.
To improve this situation, in version 1.9, Kotlin introduced a new feature called Data Objects. Let’s dive in and explore what data objects are and how to use them.
2. What Are Data Objects?
Data objects serve as a solution to the inconsistencies between data classes and regular objects in Kotlin. Unlike regular objects, *data objects inherit the convenience features of data classes, such as default implementations of equals(), hashCode(), and toString() methods*. These utility methods significantly help reduce a lot of boilerplate code. Let’s look at an example:
class DataObjectsExample {
data class MyDataClass(val name: String)
object MyRegularObject
data object MyDataObject
fun printObjects() {
println(MyDataClass("Jon")) // prints: MyDataClass(name=Jon)
println(MyRegularObject) // prints: DataObjects$MyRegularObject@1b6d3586
println(MyDataObject) // prints: MyDataObject
}
}
As we can see, MyDataClass automatically generates the toString() function implementation and returns a string representation of the object. This eliminates the need to write these functions manually.
Next, let’s observe the result of printing a regular object in Kotlin. We can see that the string representation consists of the class name and hash of the object. This is inconsistent with the cleaner string representation of data class objects.
In the code snippet above, we declared MyDataObject as a data object instead of a regular object. This means that MyDataObject will get a readable toString() function implementation without the need to override it manually. This helps maintain symmetry with the data class implementations.
3. Difference Between Data Objects and Data Classes
As we’ve learned, when we mark a class as a data class or an object as a data object, we get access to some auto-generated utility functions. However, there are some subtle differences.
3.1. No copy() Function in Data Objects
The primary difference between a data class and a data object is that some specific functions are not auto-generated for data objects. One such function is the copy() function. The reason for the absence of a copy() function in the data object comes down to the language design decision to make data objects as singletons.
The singleton design pattern restricts a class to only have a single instance. In line with this design philosophy, it would be a contradiction to allow for the creation of a copy of this single instance, as it would essentially mean creating another instance.
3.2. No componentN() Functions in Data Objects
When using data classes, we also get access to auto-generated componentN() functions. We can use these functions to map to the properties of the data class, helping us destructure these properties.
However, when we deal with data objects, Kotlin doesn’t generate these componentN() functions. The main reason for this difference is that data objects typically don’t possess properties like data classes do, making componentN() functions unnecessary.
3.3. No Custom Implementations for equals() and hashCode()
When we define a data object, we’re essentially operating with a singleton. This means that there’s only one instance of the object.
Therefore, Kotlin disallows overriding equals() and hashCode() utility functions for data objects, which is typically done to provide a custom implementation for distinguishing between multiple instances.
4. Conclusion
The introduction of data objects in Kotlin is a significant step towards reducing inconsistencies when working with regular objects. *With this feature, we get access to essential utility functions like toString(), equals(), and hashCode() when we create a data object.* This is similar to how we get access to these nifty auto-generated utility functions when we create a data class. Moreover, this is especially useful when we’re working with sealed hierarchies like sealed class and sealed interface implementations.
As always, the code samples can be found over on GitHub.