![]() |
From Kotlin in Action by Dmitry Jemerov and Svetlana Isakova
The Java platform defines several methods that need to be present in many classes and are usually implemented in a mechanical way, such as |
Save 37% on Kotlin in Action. Just enter code fccjemerov into the discount code box at checkout at manning.com.
Let’s look at examples of cases where the Kotlin compiler generates typical methods that are useful for simple data classes and greatly simplifies the class-delegation pattern.
Universal object methods
As is the case in Java, all Kotlin classes have several methods you may want to override: toString
, equals
, and hashCode
. Let’s look at what these methods are and how Kotlin can help you generate their implementations automatically. As a starting point, you’ll use a simple Client
class that stores a client’s name and postal code.
Listing 1. A simple Client
class example
class Client(val name: String, val postalCode: Int)
Let’s see how class instances are represented as strings.
String representation: tostring()
All classes in Kotlin, as in Java, provide a way to get a string representation of the class’ objects. This is primarily used for debugging and logging, although you can use this functionality in other contexts as well. By default, the string representation of an object appears as “Client@5e9f23b4”, which isn’t useful. To change this, you need to override the toString
method.
class Client(val name: String, val postalCode: Int) {
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
Now the representation of a client looks like this:
>>> val client1 = Client("Alice", 342562)
>>> println(client1)
Client(name=Alice, postalCode=342562)
Much more informative, isn’t it?
Object equality: equals()
All the computations with the Client
class take place outside of it. This class stores the data; it’s meant to be plain and transparent. Nevertheless, you may have some requirements for the behavior of such a class. For example, suppose you want the objects to be considered equal if they contain the same data:
>>> val client1 = Client("Alice", 342562)
>>> val client2 = Client("Alice", 342562)
>>> println(client1 == client2) ❶
false
❶ In Kotlin, ==
checks whether the objects are equal, not the references. It’s compiled to a call of “equals”.
You see that the objects aren’t equal. That means you must override equals
for the Client
class.
Let’s look at the changed Client
class:
Listing 2. A changed Client
class
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean { ❶
if (other == null || other !is Client) ❷
return false
return name == other.name && ❸
postalCode == other.postalCode
}
override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
❶ “Any” is the analogue of java.lang.Object: a superclass of all classes in Kotlin. The nullable type “Any?” means “other” can be null.
❷ Checks whether “other” is a Client
❸ Checks whether the corresponding properties are equal
As a reminder, the is
check in Kotlin is the analogue of instanceof
in Java. It checks whether a value has the specified type. Like the !in
operator, which is a negation for the in
check, the !is
operator denotes the negation of the is
check. Such operators make your code easier to read. Because the override
modifier is mandatory in Kotlin, you’re protected from accidentally writing fun equals(other: Client)
, which would add a new method instead of overriding equals
. After you override equals
, you may expect that clients with the same property values are equal. Indeed, the equality check client1 == client2
in the previous example returns true
now. If you want to do more complicated things with clients, it doesn’t work. The usual interview question is, “What’s broken, and what’s the problem?” You may say that the problem is that hashCode
is missing. This is true, and we’ll now discuss why this is important.
Hash containers: hashCode()
The hashCode
method should be always overridden together with equals
. Let’s see why.
Let’s create a set with one element: a client named Alice. Then you create a new Client
instance containing the same data and check whether it’s contained in the set. You’d expect the check to return true
, because the two instances are equal, but in fact it returns false
:
>>> val processed = hashSetOf(Client("Alice", 342562))
>>> println(processed.contains(Client("Alice", 342562)))
false
The reason is that the Client
class is missing the hashCode
method. Therefore, it violates the general hashCode
contract: if two objects are equal, they must have the same hash code. The processed set is a HashSet
. Values in a HashSet
are compared in an optimized way – at first their hash codes are compared, and then, only if they’re equal
, the actual values are compared. The hash codes are different for two different instances of the Client
class in the previous example, and the set decides it doesn’t contain the second object, even though equals would return true
. Therefore, if the rule isn’t followed, the HashSet
can’t work correctly with such objects.
To fix that, you can add the implementation of hashCode
to the class:
Listing 3. hashCode
added to the class
class Client(val name: String, val postalCode: Int) {
...
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
Now you have a class that works as expected in all scenarios, but you managed to write quite a bit of code in the process. Fortunately, the Kotlin compiler can help you by generating these methods automatically. Let’s see how you can ask it to do just that.
Data Classes: autogenerated implementations of universal methods
If you want your class to be a convenient holder for your data, you need to override these methods: toString
, equals
, and hashCode
. Usually, the implementations of those methods are straightforward, and IDEs like IntelliJ IDEA can help you generate them automatically and verify that they’re implemented correctly and consistently.
The good news is you don’t have to generate these methods in Kotlin. If you add the modifier data
to your class, the necessary methods are automatically generated for you.
Listing 4. Let’s add a data
modifier
data class Client(val name: String, val postalCode: Int)
Easy, right? Now you have a class that overrides all the standard Java methods:
equals
for comparing instances;hashCode
for using them as keys in hash-based containers such asHashMap;
toString
for generating string representations showing all the fields in declaration order.
The equals and hashCode
methods consider the properties declared in the primary constructor. The generated equals
method checks that the values of all the properties are equal. The hashCode
method returns a value that depends on the hash codes of all the properties. Note that properties that aren’t declared in the primary constructor don’t take part in the equality checks and hashcode
calculation.
This isn’t a complete list of useful methods generated for data
classes. We will talk about two more in the course of the article.
Data classes and immutability: the copy()
method
Note that even though the properties of a data class aren’t required to be val
—you can use var
as well— it’s strongly recommended that you only use read-only properties, making the instances of the data class immutable. This is required if you want to use such instances as keys in a HashMap
or a similar container, because otherwise the container could get into an invalid state if the object used as a key was modified after it was added to the container. Immutable objects are easier to reason about, particularly in multithreaded code; once an object has been created, it remains in its original state, and you don’t need to worry about other threads modifying the object while your code is working with it.
To make it even easier to use data classes as immutable objects, the Kotlin compiler generates one more method for them – a method that allows you to copy the instances of your classes, changing the values of some properties. Creating a copy is usually a good alternative to modifying the instance in place, because the copy has a separate lifecycle and can’t affect the places in the code that refer to the original instance. Here’s what the copy
method would look like if you implemented it manually:
class Client(val name: String, val postalCode: Int) {
...
fun copy(name: String = this.name,
postalCode: Int = this.postalCode) =
Client(name, postalCode)
}
And here’s how the copy
method can be used:
>>> val bob = Client("Bob", 973293)
>>> println(bob.copy(postalCode = 382555))
Client(name=Bob, postalCode=382555)
You’ve seen how the data
modifier makes value-object classes more convenient to use. Now let’s talk about the other Kotlin feature that allows you avoid IDE-generated boilerplate code: class delegation.
Class delegation: using the “by” keyword
A common problem in the design of large object-oriented systems is fragility caused by implementation inheritance. When you extend a class and override some of its methods, your code becomes dependent on the implementation details of the class you’re extending. When the system evolves and the implementation of the base class changes or new methods are added to it, the assumptions that you’ve made about its behavior in your class can become invalid, and your code may end up behaving incorrectly.
The design of Kotlin recognizes this problem and treats classes as final
by default. This ensures that only those classes that’re designed for extensibility can be inherited from. When working on such a class, you see that it’s open, and you can keep in mind that modifications need to be compatible with derived classes.
However, often you need to add behavior to another class, even if it wasn’t designed to be extended. A commonly-used way to implement this is known as the Decorator pattern. The essence of the pattern is that a new class is created, implementing the same interface as the original class, and storing the instance of the original class as a field. Methods in which the behavior of the original class doesn’t need to be modified are forwarded to the original class instance.
One downside of this approach is that it requires a large amount of boilerplate code (enough that IDEs like IntelliJ IDEA have dedicated features to generate that code for you). For example, this is how much code you need for a decorator that implements an interface as simple as Collection
, even when you don’t modify any behavior:
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int get() = innerList.size
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun contains(element: T): Boolean = innerList.contains(element)
override fun iterator(): Iterator<T> = innerList.iterator()
override fun containsAll(elements: Collection<T>): Boolean =
innerList.containsAll(elements)
}
The good news is that Kotlin includes first-class support for delegation as a language feature. Whenever you’re implementing an interface, you can say that you’re delegating the implementation of the interface to another object, using the by
keyword. Here’s how you can use this approach to rewrite the previous example:
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
All the method implementations in the class are gone. The compiler will generate them, and the implementation is like that in the DelegatingCollection
example. Because there’s little interesting content in the code, there’s no point in writing it manually when the compiler can do the same job for you automatically.
Now, when you need to change the behavior of methods, you can override them, and your code will be called instead of the generated methods. You can leave out methods for which you’re satisfied with the default implementation of delegating to the underlying instance.
Let’s see how you can use this technique to implement a collection that counts the number of attempts to add an element to it. For example, if you’re performing deduplication, you can use such a collection to measure how efficient the process is, by comparing the number of attempts to add an element with the resulting size of the collection.
Listing 5. Implementing a collection
class CountingSet<T>(
val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet { ❶
var objectsAdded = 0
override fun add(element: T): Boolean { ❷
objectsAdded++
return innerSet.add(element)
}
override fun addAll(c: Collection<T>): Boolean { ❷
objectsAdded += c.size
return innerSet.addAll(c)
}
}
>>> val cset = CountingSet<Int>()
>>> cset.addAll(listOf(1, 1, 2))
>>> println("${cset.objectsAdded} objects were added, ${cset.size} remain")
3 objects were added, 2 remain
❶ Delegates the MutableCollection
implementation to innerSet
❷ Doesn’t delegate; provides a different implementation
As you see, you override the add
and addAll
methods to increment the count, and you delegate the rest of the implementation of the MutableCollection
interface to the container you’re wrapping.
The important part is that you aren’t introducing any dependency on how the underlying collection is implemented. For example, you don’t care whether that collection implements addAll
by calling add
in a loop, or if it uses a different implementation optimized for a case. You have full control over what happens when the client code calls your class, and you rely only on the documented API of the underlying collection to implement your operations, and rely on it continuing to work.
If you’re interested in learning more about the inner workings of Kotlin, check out the book on liveBook here and see this Slideshare presentation.