
var property or by composing a mutable object:var a = 10 val list: MutableList<Int> = mutableListOf()
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
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. We will discuss this in more detail later in the next item. For now, let’s just say that it is hard to manage a shared state.
- 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 consider 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.
-
Read-only properties
val, - Separation between mutable and read-only collections,
-
copyin data classes.
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
val list = mutableListOf(1, 2, 3) list.add(4) print(list) // [1, 2, 3, 4]
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 }
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 }
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 }
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.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 } }
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.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.
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 }
val list = listOf(1, 2, 3) // DON’T DO THIS! if (list is MutableList) { list.add(4) }
listOf returns an instance of Arrays.ArrayList that implements the Java List interface, which has methods like add and set, so it translates to the Kotlin MutableList interface. However, Arrays.ArrayList does not implement add and some other operations that mutate objects. This is why the result of this code is UnsupportedOperationException. On different platforms, the same code could give us different results.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)
List it won’t be modified from outside.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(names) // [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
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)
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)
var property:val list1: MutableList<Int> = mutableListOf() var list2: List<Int> = listOf()
list1.add(1) list2 = list2 + 1
list1 += 1 // Translates to list1.plusAssign(1) list2 += 1 // Translates to list2 = list2.plus(1)
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
var names by 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]
var announcements = listOf<Announcement>() private set
// Don’t do that var list3 = mutableListOf<Int>()
- Prefer
valovervar. - 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.
