Instruction
Throughout this lesson, you’ll:
- Learn uses for data classes in your projects.
- Understand the requirements of using data classes.
- Gain hands-on experience creating data classes in code.
What Are Kotlin Data Classes?
The answer to this question is simple: Kotlin data classes provide a streamlined, simple way to store and model data. For example, you can use these classes for these everyday data objects:
- Users
- Blog posts
- Product Descriptions
- Transactions
- Customer Information
Using data classes to store this information makes your code significantly cleaner and less verbose. Data classes automatically implement five key functionalities: initialization, string representation, equality, componentN functions, and the object’s hashcode.
In other words, when you store your data in one of these classes, you don’t have to write the constructor or equality operator, and you get an out-of-the-box string representation that makes it easy to log your data class. Additionally, you get a free way to deconstruct your object into individual variables like this:
data class Point(val x: Int, val y: Int)
val myPoint = Point(10, 20)
val (xCoord, yCoord) = myPoint
Another benefit of having a data class is getting a copy function on that data class. You can also modify individual properties within the class like this:
val newPoint = myPoint.copy(x = 30)
In the example above, the returned Point contains all the properties of the initial Point, just with a modified x attribute.
Creating a Kotlin Data Class
With all the benefits mentioned above, you may worry that creating data classes would be challenging. Fortunately, it’s actually a straightforward process.
The first step in creating a Kotlin data class is to use the data keyword. Consider the following code snippet:
data class User(val name: String, val age: Int)
In the example above, you created a user with a name and an age.
To initialize a new user, you provide the parameters to your class like any other class. For example, to create a user named Alice who is 32 years old, you’d write this:
val user1 = User("Alice", 32)
Data classes must have a primary constructor with at least one parameter. This restriction makes sense, considering that the class aims to hold data. If the class had no parameters, it couldn’t hold any data! Parameters can be immutable, using the val keyword, or mutable, using the var keyword.
As mentioned above, many functions and concepts are available when you use these data classes. You can check for equality, print a string representation of the class, and even copy it with just one value changed.
Here’s a little code snippet demonstrating the power of Kotlin data classes. For this example, assume you’ve implemented the User class above:
val user1 = User("Alice", 32)
val user2 = User("Alice", 32)
println(user1.toString()) // User(name=Alice, age=32)
println(user2.toString()) // User(name=Alice, age=32)
val user3 = user2.copy(age = 35)
println(user3.toString()) // User(name=Alice, age=35)
Here, you create two identical users, print the details, change one of the parameters, and then print the different user. You could imagine a world where you have a client that hooks up to a server, collects a few signed-in User objects, and then leverages these powerful concepts to determine which users are equal.
##Limitations of Kotlin Data Classes
Kotlin data classes are a powerful concept, but like any programming paradigm, they have some limitations.
First, these classes can’t be abstract, open, sealed, or inner. You can, however, inherit from them. For example, you could have a base class of a Person and make both an Employee and a Customer subclasses of a person.
However, please keep in mind that since one of the benefits of data classes is their inherent simplicity and readability, you likely don’t want to have super complex class hierarchies. If you have these types of complex structures, data classes likely won’t be flexible enough for your needs.
Second, data classes are not very effective for handling complex logic. They’re meant strictly for storing data.
In many cases, people will use a Kotlin data class to store the contents of a JSON response from a server. Libraries like Gson can help translate from JSON to a data class. Your program’s business and data manipulation logic should live outside the data class itself. Again, these objects should not be complex but readable, simple, and fast!
Diving Deeper Into the Functions
Data classes provide a few essential functions and capabilities for free. This free functionality is one of the core reasons data classes are so powerful. Here’s the complete list of functionalities and what each piece of the data class does.
Equals and hashCode
These two functions come free with a data class. When comparing two objects, developers often need to determine if they’re equal. Knowing equality is important for concepts like sorting or determining whether the blog post the user submitted has any changes.
A Kotlin data class provides an equality and hashCode function. The equals() function compares the data class based on its content, not its reference. So, if you have two classes containing the same properties, the equals() function will return true, even if they don’t reference the same object. The hashCode() method indirectly helps with equality and makes it easy to put a data class into a HashMap or HashSet.
ToString
Data classes contain a default implementation of the toString() method. This default implementation is merely a readout of the class name, followed by each parameter and value. The nice thing about the toString() method’s default implementation is that you don’t need to write it yourself, and it’s already highly readable.
However, you can still override the toString() function when you don’t want to log personal information. Here’s an example of overriding the toString() method in a data class. In it, you exclude the password field because you don’t want your logs to contain real user password information. That would be a massive liability to any company!
data class User(val name: String, val password: String) {
override fun toString(): String {
return "User(name='$name')" // No password
}
}
Copy
Kotlin data classes provide a quick and easy way to make copies of themselves while retaining the ability to modify various properties. Every single class gets an automatically generated copy() method. If you want to make a straight copy, this method takes zero parameters. Or it can take one or more parameters of properties you wish to change in the copied data.
The copy method can be beneficial. Consider a scenario where you’re building a game. Your main character takes a hit, and its health depletes. However, suppose your game has a “rewind” functionality. You can use a data class to represent your character in this case. You can then make a copy of the character and put the old “healthier” character on the history stack. If the user wants to “undo” their progress, you can change the copied class to the main character and return to that state.
To see that in action, consider the following example code:
data class Character(val name: String, val health: Int, val inventory: List<String>)
...
val newCharacter = oldCharacter.copy(health = character.health - 25)
// You could then put oldCharacter at the front of a state array or stack to pop off if needed.
Here’s a code breakdown:
The example above shows a clean, concise way of managing the state. While most people aren’t writing games in Kotlin, you could use a similar paradigm to create an undo functionality for a text box component or create a data object representing an animation state and undo that animation if the user asks merely by returning to the initial object.
ComponentN
Finally, Kotlin data classes implement the componentN() functions. Often, you’ll want to pull specific parameters from a data class and use those in your app. For example, you might write an app that displays the user’s name and birthdate on a dashboard. You’ll get the whole user object, but all you need to display a particular component are the name and age. Rather than passing through the entire object, you can pull out the two properties you need and only pass those.
The componentN() functions enable this essential functionality. Here’s a code sample to further explain how it works:
data class User(val name: String, val age: Int)
val user = User("Alice", 30)
val (userName, userAge) = user
The code in the example above is clean and easy to read. You only access the name and age properties from the underlying data. However, consider the same example with manual implementations of the componentN() functions.
class User(val name: String, val age: Int) {
operator fun component1(): String = name
operator fun component2(): Int = age
}
val user = User("Alice", 30)
val (userName, userAge) = user
For this simple example, these two classes are more or less equivalent functionally. However, the second example is much longer and prone to potential complications. For complex classes with, say, 20 potential components, making a mistake on one of the componentN() implementations is trivial. With Kotlin data classes, you don’t need to worry about potential errors!
##Performance Gains or Losses with Data Classes
From a developer’s perspective, using these classes is tremendously beneficial. However, you may wonder about their performance relative to a straightforward class. Do these data classes introduce potential performance issues, or are they faster than other implementations?
Generally speaking, these data classes offer at least as good performance as other alternatives. Since these classes are immutable by default, their performance is often better in multi-threaded environments since you don’t need to worry about synchronization or defensive copying as much.
Additionally, since Kotlin compilers recognize and know about the data keyword, they can optimize these objects tremendously. These compilers can often remove unused components, including automatically generated methods.
Technically, there is some slight overhead with automatically generating methods like equals(), but that overhead is usually negligible. The toString() implementation iterates over every property by default. If your data class has 100 parameters, this could lead to a significant string representation, slowing down your app—especially if you’re logging those giant string representations.
Most people find these data classes to be equally performant with alternatives. There may be specific cases where performance is an issue, but those are typically few and far between. You can use profilers to help identify where the performance problems are in your app, and if you find that they’re in your data classes, you can always override the problematic function.