article banner (priority)

Item 1: Limit mutability

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

In Kotlin, we design programs in modules, each of which comprises different kinds of elements, such as classes, objects, functions, type aliases, and top-level properties. Some of these elements can hold a state, for instance, by having a read-write var property or by composing a mutable object:

var a = 10 val list: MutableList<Int> = mutableListOf()

When an element holds a state, the way it behaves depends not only on how you use it but also on its history. A typical example of a class with a state is a bank account (class) that has some money balance (state):

class BankAccount { var balance = 0.0 private set fun deposit(depositAmount: Double) { balance += depositAmount } @Throws(InsufficientFunds::class) fun withdraw(withdrawAmount: Double) { if (balance < withdrawAmount) { throw InsufficientFunds() } balance -= withdrawAmount } } class InsufficientFunds : Exception() val account = BankAccount() println(account.balance) // 0.0 account.deposit(100.0) println(account.balance) // 100.0 account.withdraw(50.0) println(account.balance) // 50.0

Here BankAccount has a state that represents how much money is in this account. Keeping a state is a double-edged sword. On the one hand, it is very useful because it makes it possible to represent elements that change over time. On the other hand, state management is hard because:

  • It is harder to understand and debug a program with many mutating points. The relationship between these mutations needs to be understood, and it is harder to track how they have changed when more of them occur. A class with many mutating points that depend on each other is often really hard to understand and modify. This is especially problematic in the case of unexpected situations or errors.

  • Mutability makes it harder to reason about code. The state of an immutable element is clear, but a mutable state is much harder to comprehend. It is harder to reason about what its value is as it might change at any point; therefore, even though we might have checked a moment ago, it might have already changed.

  • A mutable state requires proper synchronization in multithreaded programs. Every mutation is a potential conflict.

  • Mutable elements are harder to test. We need to test every possible state; the more mutability there is, the more states there are to check. Moreover, the number of states we need to test generally grows exponentially with the number of mutation points in the same object or file, as we need to test all combinations of possible states.

  • When a state mutates, other classes often need to be notified about this change. For instance, when we add a mutable element to a sorted list, if this element changes, we need to sort this list again.

Problems with state consistency and a growing number of mutation points in a project are familiar to developers working in bigger teams. Let’s see an example of how hard it is to manage a shared state. Take a look at the snippet belowfootnote110_note. It shows multiple threads trying to modify the same property, but some of these operations will be lost due to conflicts.

var num = 0 for (i in 1..1000) { thread { Thread.sleep(10) num += 1 } } Thread.sleep(5000) print(num) // Very unlikely to be 1000 // Every time a different number

Using Kotlin coroutines does not help with this problem:

suspend fun main() { var num = 0 coroutineScope { for (i in 1..1000) { launch { delay(10) num += 1 } } } print(num) // Every time a different number }

In real-life projects, we generally cannot just lose some operations, so we need to implement proper synchronization like that presented below. However, implementing proper synchronization is hard, and it gets harder when there are more mutation points. Limiting mutability does help.

val lock = Any() var num = 0 for (i in 1..1000) { thread { Thread.sleep(10) synchronized(lock) { num += 1 } } } Thread.sleep(1000) print(num) // 1000

The drawbacks of mutability are so numerous that there are languages that do not allow state mutation at all. These are purely functional languages, a well-known example of which is Haskell. However, such languages are rarely used for mainstream development since it's very hard to do programming with such limited mutability. A mutating state is a very useful way to represent the state of real-world systems. I recommend using mutability, but do so sparingly and wisely when deciding where the mutating points should be. The good news is that Kotlin has good support for limiting mutability.

Limiting mutability in Kotlin

Kotlin is designed to support limiting mutability: it is easy to make immutable objects or to keep properties immutable. This is a result of many features and characteristics of this language, the most important of which are:

  • Read-only properties val,

  • Separation between mutable and read-only collections,

  • copy in data classes.

Let’s discuss these one by one.

Read-only properties

In Kotlin, we can make each property a read-only val (like "value") or a read-write var (like "variable"). Read-only (val) properties cannot be set to a new value:

val a = 10 a = 20 // ERROR

Notice though that read-only properties are not necessarily immutable or final. A read-only property can hold a mutable object:

val list = mutableListOf(1, 2, 3) list.add(4) print(list) // [1, 2, 3, 4]

A read-only property can also be defined using a custom getter that might depend on another property:

var name: String = "Marcin" var surname: String = "Moskała" val fullName get() = "$name $surname" fun main() { println(fullName) // Marcin Moskała name = "Maja" println(fullName) // Maja Moskała }

In the above example, the value returned by the val changes because when we define a custom getter, it will be called every time we ask for the value.

fun calculate(): Int { print("Calculating... ") return 42 } val fizz = calculate() // Calculating... val buzz get() = calculate() fun main() { print(fizz) // 42 print(fizz) // 42 print(buzz) // Calculating... 42 print(buzz) // Calculating... 42 }

This trait, namely that properties in Kotlin are encapsulated by default and can have custom accessors (getters and setters), is very important in Kotlin because it gives us flexibility when we change or define an API. This will be described in detail in Item 16: Properties should represent state, not behavior. The core idea though is that val does not offer mutation points because, under the hood, it is only a getter. var is both a getter and a setter. That’s why we can override val with var:

interface Element { val active: Boolean } class ActualElement : Element { override var active: Boolean = false }

Values of read-only val properties can change, but such properties do not offer a mutation point, and this is the main source of problems when we need to synchronize or reason about a program. This is why we generally prefer val over var.

Remember that val doesn't mean immutable. It can be defined by a getter or a delegate. This fact gives us more freedom to change a final property into a property represented by a getter. However, when we don’t need to use anything more complicated, we should define final properties, which are easier to reason about as their value is stated next to their definition. They are also better supported in Kotlin. For instance, they can be smart-casted:

val name: String? = "Márton" val surname: String = "Braun" val fullName: String? get() = name?.let { "$it $surname" } val fullName2: String? = name?.let { "$it $surname" } fun main() { if (fullName != null) { println(fullName.length) // ERROR } if (fullName2 != null) { println(fullName2.length) // 12 } }

Smart casting is impossible for fullName because it is defined using a getter; so, when checked it might give a different value than it does during use (for instance, if some other thread sets name). Non-local properties can be smart-casted only when they are final and do not have a custom getter.

Separation between mutable and read-only collections

Similarly, just as Kotlin separates read-write and read-only properties, Kotlin also separates read-write and read-only collections. This is achieved thanks to how the hierarchy of collections was designed. Take a look at the diagram presenting the hierarchy of collections in Kotlin. On the left side, you can see the Iterable, Collection, Set, and List interfaces, all of which are read-only. This means that they do not have any methods that would allow modification. On the right side, you can see the MutableIterable, MutableCollection, MutableSet, and MutableList interfaces, all of which represent mutable collections. Notice that each mutable interface extends the corresponding read-only interface and adds methods that allow mutation. This is similar to how properties work. A read-only property means just a getter, while a read-write property means both a getter and a setter.

The hierarchy of collection interfaces in Kotlin and the actual objects that can be used in Kotlin/JVM. On the left side, the interfaces are read-only. On the right side, the collections and interfaces are mutable.

Read-only collections are not necessarily immutable. They are often mutable, but they cannot be mutated because they are hidden behind read-only interfaces. For instance, the Iterable<T>.map and Iterable<T>.filter functions return ArrayList (which is a mutable list) as a List, which is a read-only interface. In the snippet below, you can see a simplified implementation of Iterable<T>.map from stdlib.

inline fun <T, R> Iterable<T>.map( transformation: (T) -> R ): List<R> { val list = ArrayList<R>() for (elem in this) { list.add(transformation(elem)) } return list }

The design choice to make these collection interfaces read-only instead of truly immutable is very important because it gives us much more freedom. Under the hood, any actual collection can be returned as long as it satisfies the interface; therefore, we can use platform-specific collections.

The safety of this approach is close to what is achieved by having immutable collections. The only risk is when a developer tries to "hack the system" by performing down-casting. This is something that should never be allowed in Kotlin projects. We should be able to trust that when we return a list as read-only, it is only used to read it. This is part of the contract. More about this in Part 2.

Down-casting collections not only breaks their contract and depends on implementation instead of abstraction (as we should), but it is also insecure and can lead to surprising consequences. Take a look at this code:

val list = listOf(1, 2, 3) // DON’T DO THIS! if (list is MutableList) { list.add(4) }

The result of this operation is platform-specific. On JVM, listOf returns an instance of Arrays.ArrayList that implements the Java List interface. This Java List interface has methods like add or set, so it translates to the Kotlin MutableList interface. However, Arrays.ArrayList does not implement some of these operations. This is why the result of the above code is the following:

Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108)

However, there is no guarantee how this will behave in a year from now. The underlying collections might change. They might be replaced with truly immutable collections implemented in Kotlin that do not implement MutableList at all. Nothing is guaranteed. This is why down-casting read-only collections to mutable ones should never happen in Kotlin. If you need to transform from read-only to mutable, you should use the List.toMutableList function, which creates a copy that you can then modify:

val list = listOf(1, 2, 3) val mutableList = list.toMutableList() mutableList.add(4)

This way does not break any contract, and it is safer for us as we can feel safe that when we expose something as List it won’t be modified from outside.

Copy in data classes

There are many reasons to prefer immutable objects – objects that do not change their internal state, like String or Int. In addition to the previously given reasons why we generally prefer less mutability, immutable objects have their own advantages:

  • They are easier to reason about since their state stays the same once they have been created.

  • Immutability makes it easier to parallelize a program as there are no conflicts among shared objects.

  • References to immutable objects can be cached as they will not change.

  • We do not need to make defensive copies of immutable objects. When we do copy immutable objects, we do not need to make a deep copy.

  • Immutable objects are the perfect material to construct other objects, both mutable and immutable. We can still decide where mutability is allowed, and it is easier to operate on immutable objects.

  • We can add them to sets or use them as keys in maps, unlike mutable objects, which shouldn't be used this way. This is because both these collections use hash tables under the hood in Kotlin/JVM. When we modify an element that is already classified in a hash table, its classification might not be correct anymore, therefore we won’t be able to find it. This problem will be described in detail in Item 43: Respect the contract of hashCode. We have a similar issue when a collection is sorted.

val names: SortedSet<FullName> = TreeSet() val person = FullName("AAA", "AAA") names.add(person) names.add(FullName("Jordan", "Hansen")) names.add(FullName("David", "Blanc")) print(s) // [AAA AAA, David Blanc, Jordan Hansen] print(person in names) // true person.name = "ZZZ" print(names) // [ZZZ AAA, David Blanc, Jordan Hansen] print(person in names) // false

At the last check, the collection returned false even though that person is in this set. It couldn't be found because it is at an incorrect position.

As you can see, mutable objects are more dangerous and less predictable. On the other hand, the biggest problem of immutable objects is that data sometimes needs to change. The solution is that immutable objects should have methods that produce a copy of this object with the desired changes applied. For instance, Int is immutable, and it has many methods like plus or minus that do not modify it but instead return a new Int, which is the result of the operation. Iterable is read-only, and collection processing functions like map or filter do not modify it but instead return a new collection. The same can be applied to our immutable objects. For instance, let’s say that we have an immutable class User, and we need to allow its surname to change. We can support it with the withSurname method, which produces a copy with a particular property changed:

class User( val name: String, val surname: String ) { fun withSurname(surname: String) = User(name, surname) } var user = User("Maja", "Markiewicz") user = user.withSurname("Moskała") print(user) // User(name=Maja, surname=Moskała)

Writing such functions is possible but it’s also tedious if we need one for every property. So, here comes the data modifier to the rescue. One of the methods it generates is copy. The method copy creates a new instance in which all primary constructor properties are, by default, the same as in the previous one. New values can be specified as well. copy and other methods generated by the data modifier are described in detail in Item 37: Use the data modifier to represent a bundle of data. Here is a simple example showing how it works:

data class User( val name: String, val surname: String ) var user = User("Maja", "Markiewicz") user = user.copy(surname = "Moskała") print(user) // User(name=Maja, surname=Moskała)

This elegant and universal solution supports making data model classes immutable. This way is less efficient than just using a mutable object instead, but it is safer and has all the other advantages of immutable objects. Therefore it should be preferred by default.

Different kinds of mutation points

Let’s say that we need to represent a mutating list. There are two ways we can achieve this: either by using a mutable collection or by using the read-write var property:

val list1: MutableList<Int> = mutableListOf() var list2: List<Int> = listOf()

Both properties can be modified, but in different ways:

list1.add(1) list2 = list2 + 1

Both of these ways can be replaced with the plus-assign operator, but each of them is translated into a different behavior:

list1 += 1 // Translates to list1.plusAssign(1) list2 += 1 // Translates to list2 = list2.plus(1)

Both these ways are correct, and both have their pros and cons. They both have a single mutating point, but each is located in a different place. In the first one, the mutation takes place on the concrete list implementation. We might depend on the fact that the collection has proper synchronization in the case of multithreading, but such an assumption is also dangerous since it is not guaranteed. In the second one, we need to implement the synchronization ourselves, but the overall safety is better because the mutating point is only a single property. However, in the case of a lack of synchronization, remember that we might still lose some elements:

var list = listOf<Int>() for (i in 1..1000) { thread { list = list + i } } Thread.sleep(1000) print(list.size) // Very unlikely to be 1000, // every time a different number, like for instance 911

Using a mutable property instead of a mutable list allows us to track how this property changes when we define a custom setter or use a delegate (which uses a custom setter). For instance, when we use an observable delegate, we can log every change of a list:

var names by Delegates.observable(listOf<String>()) { _, old, new -> println("Names changed from $old to $new") } names += "Fabio" // Names changed from [] to [Fabio] names += "Bill" // Names changed from [Fabio] to [Fabio, Bill]

To make this possible for a mutable collection, we would need a special observable implementation of the collection. For read-only collections in mutable properties, it is also easier to control how they change as there is only a setter instead of multiple methods mutating this object, and we can make it private:

var announcements = listOf<Announcement>() private set

In short, using mutable collections is a slightly faster option, but using a mutable property instead gives us more control over how the object changes.

Notice that the worst solution is to have both a mutating property and a mutable collection:

// Don’t do that var list3 = mutableListOf<Int>()

We need to synchronize both ways it can mutate (by property change and internal state change). Also, changing it using plus-assign is impossible because of ambiguity:

The general rule is that one should not create unnecessary ways to mutate a state. Every way to mutate a state is a cost. Every mutation point needs to be understood and maintained. We prefer to limit mutability.

Do not leak mutation points

It is an especially dangerous situation when we expose a mutable object that makes a public state. Take a look at this example:

data class User(val name: String) class UserRepository { private val users: MutableList<User> = mutableListOf() fun loadAll(): MutableList<User> = users //... }

One could use loadAll to modify the UserRepository private state:

val userRepository = UserRepository() val users = userRepository.loadAll() users.add(User("Kirill")) //... print(userRepository.loadAll()) // [User(name=Kirill)]

It is especially dangerous when such modifications are accidental. The first change that we should apply is upcasting the mutable objects into a read-only type; this is like upcasting from MutableList to List.

data class User(val name: String) class UserRepository { private val users: MutableList<User> = mutableListOf() fun loadAll(): List<User> = users //... }

But beware because the above implementation is not enough to make this class safe. First, we receive what looks like a read-only list, but it is a reference to a mutable list, so its values might change. This might cause developers to make serious mistakes:

data class User(val name: String) class UserRepository { private val users: MutableList<User> = mutableListOf() fun loadAll(): List<User> = users fun add(user: User) { users += user } } class TestRepository { fun `should add elements`() { val repo = UserRepository() val oldElements = repo.loadAll() repo.add(User("B")) val newElements = repo.loadAll() assert(oldElements != newElements) // This assertion will fail, because both references // point to the same object, and they are equal } }

Second, consider the situation in which one thread reads the list returned using loadAll, and at the same time another thread modifies it. It is illegal to modify a mutable collection that another thread iterates over, so it might also lead to an unexpected exception.

val repo = UserRepository() thread { for (i in 1..10000) repo.add(User("User$i")) } thread { for (i in 1..10000) { val list = repo.loadAll() for (e in list) { /* no-op */ } } }

Exception in thread "Thread-1" java.util.ConcurrentModificationException ...

There are two ways of dealing with this. The first one is to return a copy of an object instead of a real reference. We call this technique defensive copying. Notice that when we copy we might have a conflict with addition; so, if we want to support multithreaded access to our object, this operation needs to be synchronized. Collections can be copied with transformation functions like toList, while data classes can be copied with the copy method.

class UserRepository { private val users: MutableList<User> = mutableListOf() private val LOCK = Any() fun loadAll(): List<User> = synchronized(LOCK) { users.toList() } fun add(user: User) = synchronized(LOCK) { users += user } }

A simpler option is to use a read-only list. This option is easier to secure, and it gives us more ways of tracking changes in objects.

class UserRepository { private var users: List<User> = listOf() fun loadAll(): List<User> = users fun add(user: User) { users = users + user } }

When we use this option and we want to introduce proper support for multithreaded access, we only need to synchronize the operations that modify our list.

class UserRepository { private var users: List<User> = listOf() private val LOCK = Any() fun loadAll(): List<User> = users fun add(user: User) = synchronized(LOCK) { users = users + user } }

Summary

In this chapter, we’ve learned why it is important to limit mutability and to prefer immutable objects. We’ve seen that Kotlin gives us many tools that support limiting mutability. We should use them to limit mutation points. The simple rules are:

  • Prefer val over var.
  • Prefer an immutable property over a mutable one.
  • Prefer objects and classes that are immutable over mutable ones.
  • If you need immutable objects to change, consider making them data classes and using copy.
  • When you hold a state, prefer read-only over mutable collections.
  • Design your mutation points wisely and do not produce unnecessary ones.
  • Do not expose mutable objects.

There are some exceptions to these rules. Sometimes we prefer mutable objects because they are more efficient. Such optimizations should be preferred only in performance-critical parts of our code (Part 3: Efficiency); when we use them, we need to remember that mutability requires more attention when we prepare it for multithreading. The baseline is that we should limit mutability.

footnote110_note:

To test it, just place it in an entry point (main function) and run (similarly to other snippets that do not have an entry point).