By Dmitry Jemerov and Svetlana Isakova

Save 37% off Kotlin in Action (all formats) with code fccjemerov.

 

 

The Java platform defines several methods that need to be present in many classes and are usually implemented in a mechanical way, such as equalshashCode, and toString. Fortunately, Java IDEs can automate the generation of these methods, and you usually don’t need to write them by hand. In this case, your codebase contains the boilerplate code. The Kotlin compiler takes a further step: it can perform the mechanical code generation, behind the scenes, without cluttering your source code files with the results.

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: toStringequals, 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.

== for equality

In Java, you can use the == operator to compare primitive and reference types. If applied to primitive types, Java’s == compares values, whereas for == on reference types, it compares references. In Java, there’s the well-known practice of always calling equals, and there’s the well-known problem of forgetting to do this.

In Kotlin, the == operator is the default way to compare two objects; it compares their values by calling equals under the hood. If equals is overridden in your class, you can safely compare its instances using ==. For reference comparison, you can use the === operator, which works the same as == in Java by comparing the object’s references.

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: toStringequals, 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 as HashMap;
  • 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, download the free first chapter of Kotlin in Action and see this Slideshare presentation. Use code fccjemerov to save 37% off Kotlin in Action (all formats).