At the end of the last chapter, we saw how all objects in Kotlin inherit three functions from an open class called Any
. Those functions are equals()
, hashCode()
, and toString()
. In this chapter, we’re going to learn about data classes, which are super-powered classes that are especially helpful when you’ve got an immutable class that mainly just holds properties.
In order to best understand data classes, let’s first visit each of the three functions above, and see what’s involved when we override them.
Overriding equals()
Reference Equality
Two fathers were crossing paths at the local park, with their daughters in tow, when each accidentally let go of his daughter’s hand.

Both of these girls were named Fiona. However, despite having the same name, they were two different girls, so when the fathers turned around, it was important that each collected his own daughter, not just the first girl he saw who was named Fiona!

Similarly, in Kotlin, there are times when we want to check to see if an object is the instance that we want. To do this, we can use the equality operator, which is two adjacent equal signs ==
. Let’s demonstrate this by creating a class to represent the two girls, and instantiate an object for each.

When using the equality operator on these two objects, we see that they are not equal. In other words, they’re two different Child
objects. That’s exactly what we want! After all, these two girls are different children, even though they both have the same name.
Of course, if we assign the one Child
object to two different variables, then the equality operator will indicate that those two variables are equal, because they both refer to the same exact object instance.

In the code above, fiona1
and fiona2
are both assigned the same object, so they’re equal.
This kind of equality is sometimes called reference equality,1 because as long as the two variables refer to the same object instance, then they will be considered equal. By default, Kotlin uses reference equality when we use the equality operator to compare two objects.
Value Equality
The next day, the two fathers were again crossing paths in the park. This time, they each accidentally let go of a five-dollar bill at the same time.

When they turned around, they looked at the dollar bills on the ground, but they weren’t sure which bill belonged to which person.

However, each bill is worth the same amount - five dollars. Unlike the previous day when it was important for each father to pick up his own exact daughter, in this case, it doesn’t matter whether each person picks up the exact bill that he dropped. As long as each of them picks up one of the five-dollar bills, it’s fine, because the two bills are equal to one another. In other words, some things are interchangeable as long as they have certain characteristics that are the same.

Similarly, in Kotlin, when objects are considered equal based on their property values rather than their identity, it’s often called value equality.2
Since we would consider two five-dollar bills to be equal to each other, we would probably want a DollarBill
class to have value equality rather than reference equality. The following code is roughly the same as Listing 15.1 above, but with a DollarBill
class instead of a Child
class.
As you can see, when we run this, we get false
. How can we get this code to print true
?
Under the hood, when we use the equality operator, it calls into the equals()
function to determine whether the two objects are considered equal. The implementation of equals()
that is handed down from the Any
class will check to see if the two variables refer to the same object, which is why reference equality is the default. So if we want the equality operator to act differently for a particular class, we’ll need to override the equals()
function in that class.
In the case of this DollarBill
class, one simple way to achieve this is to manually delegate the equals()
call to the amount
property. Let’s try overriding this function in the DollarBill
class. Note that in the Any
class, this function has a parameter of type Any?
, and a return type of Boolean
, so we’ll use the same types in our equals()
function here.3
Well, that didn’t work. The problem is that the other
parameter has a type of Any?
, so that any two objects in Kotlin can be compared to each other, even if their types don’t match. Since other
might not actually be a DollarBill
object, it won’t necessarily have a property named amount
.
To fix this, we’ll need to first check whether the type of other
is DollarBill
.
- If it is, then we can just use a smart cast to compare the
amount
values. - If it isn’t, then we can just return
false
.
With this change, two DollarBill
objects will be equal to one another as long as…
- They’re both instances of
DollarBill
. - They both have the same value for the
amount
property.
When we run the code again, we’ll see that bill
and bill2
are now considered equal!
So, by overriding the equals()
function, we were able to give our DollarBill
class value equality instead of reference equality! However, there’s another problem that shows up when we try to use this class with certain collection types. This brings us to the hashCode()
function.
Overriding hashCode()
Little Fiona is starting a collection of contemporary US dollar bills. In order to complete her collection, she’s going to need one bill of each of the seven denominations: $1, $2, $5, $10, $20, $50, and $100.

As you recall from Chapter 8, multiple objects can be stored in a collection type called a Set
, which guarantees that each of its elements is unique. In other words, if you try to add an object to a set that it already contains, nothing will change.
We can use a Set
to keep track of Fiona’s dollar bills.
Fiona has collected bills of three different denominations so far: $1, $2, and $5. We can add those to her collection, and print out the size, just to make sure it’s got three unique items in it.
Perfect!
One day, Fiona finds a one-dollar bill, but can’t remember if she already collected that one. What happens when we try to add that second one-dollar bill to the set?
Yikes! Instead of rejecting the duplicate, the denominations
set happily included it as a fourth element! Why did this happen? After all, we overrode the equals()
function in the DollarBill
class back in Listing 15.5!
Set
itself is just an interface, and when we call mutableSetOf()
it returns an implementation of Set
, called LinkedHashSet
.
A LinkedHashSet
does not primarily use the equals()
function to determine whether it already contains an object. Instead, it starts with the hashCode()
function, and only calls equals()
when an object with the same hash code already exists in that set.4
This is why we’re supposed to override hashCode()
any time we override equals()
. In our case, since we’re already delegating to the amount
property for equals()
, we can just do the same for hashCode()
.
Easy! With this change, when we run Listing 15.9 again, the set will correctly disregard the duplicate one-dollar bill, resulting in a size of 3 instead of 4.
Well, our DollarBill
class is almost doing everything we want. Before we see how data classes can make our lives easier, let’s override that third and final function from the Any
class, toString()
.
Overriding toString()
Although we’ve used the println()
function frequently, we haven’t examined it closely yet. As you know, we can pass an argument to this function, and it prints it out to the screen. We’ve often passed it a string, like this:
However, we aren’t limited to passing strings; we can pass literally any object to this function! For example, we can send it an instance of our DollarBill
class.
When we run this, we’ll see this printed out:
When we pass an object to the println()
function, it will call into the toString()
function on that object. By default, this function returns a string that looks like the one above. This string has three parts to it:
- The name of the class
- An
@
sign - The hash code of the object, in hexadecimal5
As you recall, the DollarBill
class is delegating the hashCode()
call to the amount
integer in Listing 15.10. For an integer, the hash code is simply the value of the integer itself. So, if we assign the value 100 to an integer variable, its hash code is also 100. However, since toString()
converts that hash code to hexadecimal, it appears as 64.
The toString()
function in this class would be much more helpful if it were to show the name of the class and the value of the amount
property in decimal rather than hexadecimal. While we’re at it, it’d be helpful to include a label indicating what that number represents (i.e., “amount”).
Let’s override the toString()
function and return a string that includes those things.
After making this change, when we call println()
with a DollarBill
object, we’ll get more helpful output.
Fantastic!
Well, we’ve made a lot of changes to our DollarBill
class now. Let’s review everything that we did.
- We made it so that two instances of
DollarBill
are equal to each other, as long as they have the sameamount
values. In other words, we gave it value equality instead of reference equality. - We made it so that they are also treated as equal in sets and maps.
- We created a more helpful implementation of
toString()
.
We achieved all of these things by overriding the three functions from the Any
class - equals()
, hashCode()
, and toString()
. Now that we understand why we might want to override these functions, it’s finally time to see how data classes can make our lives much easier!
Introduction to Data Classes
After all the changes we made, here’s the code that we ended up with:
The DollarBill
example has been rather simple. After all, it only has a single property. What would happen if we tried to achieve the same things for a class that includes multiple properties? Implementing toString()
would be straightforward. However, equals()
would be a bit more difficult, and hashCode()
would be even more involved!
The good news is that whether our class has a single property or a dozen, Kotlin can automatically accomplish everything that we’ve already done in this chapter - overriding equals()
, hashCode()
, and toString()
- and all we have to do is declare our class to be a data class. To do this, we just add the keyword data
before the class declaration, like this:
In this code, we haven’t provided an override for equals()
, hashCode()
, or toString()
. In fact, this DollarBill
doesn’t even have a class body at all! And yet, in just one line, this class does everything that Listing 15.16 does!
Again, the DollarBill
class only includes a single property, but data classes can easily provide structural equality and a nice toString()
result for classes that have multiple properties. For example, here’s an Address
class with three properties.
As this next code listing demonstrates, instances of Address
use value equality, and they print out nicely.
We’ve seen how data classes give us a lot of power, automatically generating useful implementations of equals()
, hashCode()
, and toString()
. The superpowers don’t stop there, though! Data classes also include a function called copy()
, which we’ll look at next.
Copying Data Classes
The properties of a data class can be declared with either val
or var
, but Kotlin developers tend to use data classes primarily for immutable data. In other words, it’s common to use only val
properties in a data class.
As you know, when a class has a mutable property, we can simply assign it a new value. For example, here we have a data class that represents a book. Its title
property is read-only, but its price
property is mutable.
When the price of the book increases, we can just set its new value.
It’s easy enough to change the value of the price
property when it’s declared with var
. But what about properties that are declared with val
?
Naturally, we can’t change the value of a val
property, but instead, we can create a copy of the entire object, substituting the new value in that copy. This approach is similar to the one we used in Chapter 8 when we created new lists by adding an element to an existing list with the plus operator.
Let’s update the Book
class so that the price
property is declared with val
.
With this change, when the price goes up, we can create a new variable called newBook
, which has the same title as the original, but with the new price.
Now, this is easy enough to do with just two properties, but the more properties we add, the more tedious it becomes to make a copy. To demonstrate this, let’s add four more properties to the Book
class.
This is a lot of boilerplate, and it’s easy to accidentally get something mixed up when relaying the values from the old object to the new one. In the code above, did you notice that the height and width values got swapped?
To avoid all that boilerplate and reduce the likelihood of these kinds of errors, data classes have a powerful function called copy()
. For the book example above, instead of manually relaying each property to the Book
constructor, we can just call copy()
on the original book, and give it a new value for price
, like this.
The copy()
function has a parameter for each property in the data class. Since our Book
data class has 6 properties, its copy()
function has a total of 6 parameters, each of which defaults to the current value of the property.
When calling the copy()
function, simply include named arguments for any properties that you want to change, and omit arguments for any properties that you want to stay the same. In Listing 15.26, we only provided the price
parameter, so newBook
will have a price of 20
, but all other properties will have the same values that they had in the original book
object.
As you can see, the copy()
function is an incredibly powerful way to work with immutable classes, allowing our code to effectively change data, without actually mutating the individual properties!
There’s one more feature that data classes provide, which is the ability to destructure its properties. Let’s dive in!
Destructuring
When we’ve got a lot of values that all pertain to some concept, it usually makes sense to put them together. For example, if we’ve got a title, price, author, width, height, and an ISBN, we usually don’t want to deal with them as individual variables. Instead, we want them grouped all together in one structure, such as a Book
class.
Here’s the Book
data class that we used earlier, which groups together all of the variables mentioned above.
When we assemble values into a single class like this, it’s clear that these different values are all associated with one another and that, taken together, they represent the concept of a book. This usually makes it easier for developers to understand - for example, it’s clear that the title
represents the title of a book, not the title of a movie. This also makes it convenient to pass all of these properties from one function to another - instead of passing each property as a separate parameter, we can just pass the Book
object as a whole.6
So again, it often makes sense to put associated values together into a structure.
Sometimes, however, it makes sense to separate the individual values back out of the structure, so that they’re stored in individual variables.
This is called destructuring. Of course, we could do this by hand. For example, in the following code, we pull out all six properties from the book
object into separate variables, some of which have names that differ from the original properties.
When working with a data class, rather than manually extracting each property to a variable, we can use a destructuring assignment to pull out each property to a variable automatically. To do this, rather than declaring a single variable name, declare multiple variable names with commas, and put them all inside parentheses, like this:
This does the same thing as the code in Listing 15.28, but all in a single assignment statement. Note that the values will be assigned based on the order that the properties appear in the primary constructor of the data class. In the case of Listing 15.29 above, this means the title
variable will be assigned the value of book.title
, the cost
variable will be assigned the value of book.price
, and so forth.
Each of the values that come out of a destructuring assignment - title, price, author, and so on - is called a component.7
Finally, note that we don’t have to assign all of the components when using a destructuring assignment. For example, if we only need to pull the title and price out of a book
object, we can choose to only include two variable names within the parentheses.
Destructuring and the Standard Library
Destructuring doesn’t only apply to our own data classes. Some of the types in Kotlin’s standard library can also be destructured. For example, back in Chapter 9, we used a class called Pair
, which can be used with destructuring assignments.
One of the most common ways to use destructuring is with lambda parameters. For example, instead of an individual association, let’s say we’ve got a map of the hardware and tools.
When we loop over these tools, the lambda parameter has a type called Map.Entry
, which has two properties - a key
and a value
. Here’s how we used it way back in Listing 9.20.
A Map.Entry
object can be destructured, so instead of using entry
as a parameter for this lambda, we can use parentheses and two variable names, like this:
When we do this, the argument to this lambda will get destructured into two different variables - the first is the entry’s key, and the second is the entry’s value. In the code above, the key will be assigned to a variable named hardware
and the value will be assigned to a variable named tool
.
Destructuring here is a great idea, because hardware
and tool
are terms related to the problem that our code solves. We might say that these terms are in the problem domain or business domain. Contrast these terms with those like entry, key, and value, which are more focused on the data structures that we’re using to implement a solution. We might say that those terms are in the technical domain.
Now let’s consider the case where we want to print out only the tools in the toolbox, but not the corresponding hardware. Naturally, we could just do this:
Here, the hardware
variable is irrelevant to the lambda. A value is assigned to it, but we’re not using it, so it’s just noise. In other words, it’s something we have to read when looking at this code, but it doesn’t affect the way the code works. In cases like this, where one of the components isn’t needed, we can substitute an underscore _
for the name of the irrelevant variable, like this:
With that change, we no longer have to concern ourselves with a hardware
variable that isn’t used.
Destructuring Non-Data Classes
Destructuring isn’t limited to data classes. Any object can be destructured, as long as its class includes the right functions. The secret is to add functions called component1()
, component2()
, component3()
, and so on. These are called componentN()
functions. For example, here’s the Child
class from Listing 15.1 at the beginning of this chapter, but this one adds a new property for the child’s age.
If we want to add the ability to destructure this class, we don’t have to convert it to a data class. Instead, we can simply add functions called component1()
and component2()
, where each one returns one of the properties.
With this, we can now use destructuring to pull out the child’s name and age.
Note that we had to include the operator
modifier on the two functions in Listing 15.38. This is the first time we’ve created an operator function. When a function includes the operator
modifier, it can still be called like any other function, but it also serves some special purpose - and the particular purpose that it serves depends upon the name of the function. When a function is named as they are in Listing 15.38 (i.e., like componentN()
), that special purpose is that the function will be used when the object is destructured.
We can even create an extension function to add destructuring to a class for which we don’t have the source code. For example, with a little ingenuity, we can create our own extension functions to destructure a Double
into its integer and fractional parts.
So, destructuring can be helpful in certain situations, and data classes are one way to easily add a destructuring capability to our classes!
Limitations of Data Classes
As we’ve seen in this chapter, data classes give us a lot of convenience!
- They create
equals()
andhashCode()
functions for value equality. - They create nice-looking
toString()
output. - They make it easy to create copies of an object.
- They can be used with destructuring assignments.
However, we also give up a few things when we declare a class to be a data class, so let’s take a quick look at those.
Data Classes and Inheritance
The first and most significant disadvantage of a data class is that it cannot be extended by another class. In other words, you cannot add the abstract
or open
modifier to a data class. However, the data class itself can extend another class.
Even though a data class can extend another class, if that superclass requires constructor arguments, things get complicated quickly, because of the second disadvantage, which relates to constructor parameters.
Constructor Parameters
The second disadvantage is that all of the constructor parameters in a data class must be property parameters. In other words, each one must be declared with either val
or var
. This means it’s not possible to add a constructor parameter that is only relayed to a superclass constructor.
Also, a data class must have at least one parameter in its primary constructor.
Finally, while a data class may have properties that are not part of its constructor, they will not be regarded in any of the functions that are generated. For example, in the following code, the serialNumber
property is not in the primary constructor.
A property like serialNumber
won’t be considered in equals()
, hashCode()
, or toString()
, because it’s declared in the body of the class, but not in the primary constructor. It won’t be possible to call copy()
with it, and it won’t be used for destructuring assignments. To demonstrate this, the following code shows how two DollarBill
objects are considered equal, even though they have different values for the serialNumber
.
Even with these disadvantages, data classes are tremendously helpful, especially when dealing with immutable classes that are primarily meant to hold properties.
Summary
In this chapter, we learned all about data classes and destructuring, including:
- The difference between reference equality and value equality.
- How to manually override
equals()
andhashCode()
to achieve value equality. - How to manually override
toString()
so that our classes work well withprintln()
. - How to declare a data class so that we don’t have to manually override
equals()
,hashCode()
, ortoString()
. - How to use the
copy()
function to make it easier to create an object based on another object’s values. - How to use a destructuring assignment when working with data classes.
- How to enhance a non-data class to enable destructuring assignments.
- How to use extension functions to enable destructuring assignments.
- The limitations of data classes.
In the next chapter, we’ll introduce another class modifier, which will enable us to account for every possible subclass of an interface or abstract class. See you then!
Thanks to James Lorenzen for reviewing this chapter!
-
This is sometimes also called identity equality. ↩︎
-
You might also hear this referred to as content equality or structural equality. ↩︎
-
When overriding a function, we often use the same parameter types and return types that are specified by the same function in the superclass. However, Kotlin also allows you to specify a more general parameter type or a more specific return type. This feature is called variance, we’ll learn more about this in Chapter 19 when we look at how variance works for generics. ↩︎
-
Note that this same problem also applies to maps, because
mutableMapOf()
returns a hash-based implementation calledHashMap
. ↩︎ -
Most of us have grown up using numbers where each digit has one of ten possible values - 0 through 9. Since there are ten possible values, this is sometimes called the Base-10 or decimal numeral system. Hexadecimal is a numbering system where each digit can have up to 16 values, so it’s also called Base-16. After 0-9 come A, B, C, D, E, and F. For example, the number “C” in hexadecimal represents the number 12 in decimal, and the number 10 in hexadecimal represents decimal 16. ↩︎
-
Although this is convenient, it’s also worth considering how many of the object’s values are actually needed in the function. When you pass more values than a function needs, it’s called stamp coupling, and it can limit the reusability of the function. For example, if the function only needs a title, it might be best to pass only the title rather than the whole book object, because then it could work for more than just book titles - perhaps it will also work for movie and song titles. ↩︎
-
The term “component” is broad and can refer to many things within the disciplines of software development and system architecture. Here, we’re just using the term in the very narrow context of destructuring. ↩︎