Kotlin is a very rich language with much subtlety tucked away. In this occasional series (meaning that there will be occasional, standalone posts covering distinct areas) we’ll explore some of these subtleties. In this post we’ll take a look at mutability.
Mutability is a core concept in Kotlin, but all is perhaps not what it seems. The fundamental concept here is if we declare variables using var
then they are mutable and can be reassigned with another value, whereas if we declare variables using val
then they are immutable and cannot be reassigned. However, it is important to remember that var
and val
only control the variable itself, and not the object instance that is assigned. We can illustrate this with a simple example:
data class MyData(var value: String) ... var mutable = MyData("Foo") val immutable = MyData("Bar") mutable = immutable // This is fine immutable = mutable // This will not compile because // immutable cannot be reassigned
We can happily change mutable
as much as we like, but we cannot reassign immutable
with a different value. This may give the impression that we cannot change immutable
, but that is actually not the case. While the variable itself is immutable, the instance of MyClass that it references is mutable:
immutable.value = "Foo" // This is fine
At first this feels a little counter intuitive, but it makes perfect sense when we consider what is actually going on. While the variable named immutable
cannot be changed it references a data class which has a single field named value
which can be changed because it is declared as a var
.
If we want to make the variable named immutable
truly behave as an immutable object then we would need to change the implementation of MyClass
itself to be immutable by making its property named value
immutable by declaring it as a val
:
data class MyData(val value: String)
On the face of it this would appear to now break the mutability of the variable named mutable
but this is not the case. While we can no longer directly alter the value property of the MyClass
instance that it references, we can assign it with a new instance of MyClass
which as a different value
property:
var mutable = MyData("Foo") mutable = MyData("Bar") // This is fine
Of course there is the overhead of having to create a new object each time, but it does give us a much cleaner and safer mutability contract. It is for this reason that we should strive for immutability wherever possible because it is easier to relax an immutable object to a mutable one (through new instance creation) than it is to tighten a mutable object to be an immutable one.
We can leverage Kotlin sealed classes to allow us to create mutable and immutable variants of the same underlying class:
sealed class MyValue(default: String) { open val value: String = default class Immutable(default: String): MyValue(default) { fun mutate(): Mutable = Mutable(value) } class Mutable(default: String) : MyValue(default) { fun immutate(): Immutable = Immutable(value) override var value = default } }
It is not possible to directly create an instance of MyValue
, instead we create an instance of either MyValue.Immutable
or MyValue.Mutable
. The underlying value
property in MyValue
is a val
, but we override this in MyValue.Mutable
to make it a var
. Kotlin allows us to override a val
property in the base class as a var
, but not the other way around.
We can also include some convenience methods which allow us to convert between mutable an immutable variants of the same base class.
Using this quite simple pattern we get concrete enforcement of our mutability contracts at compile-time:
val mutable = MyValue.Mutable("Foo") val immutable = MyValue.Immutable("Foo") mutable.value = "Bar" // This is fine immutable.value = "Bar" // This will not compile because // value cannot be reassigned val mutated = immutable.mutate() mutated.value = "Bar" // This is fine immutable.value = "Bar" // This will not compile because // value cannot be reassigned val immutated = mutated.immutate() immutated.value = "Bar" // This will not compile because // value cannot be reassigned
It is also worth mentioning that there’s a really nice trick that we can use to make a var property immutable:
class MyOtherValue(default: String) { var value: String = default private set }
Even though the property named value
is declared as a var
, we can make its setter private and it will behave as though it was declared are a val
:
val myOtherValue = MyOtherValue("Foo") myOtherValue.value = "Bar" // This will not compile because // value cannot be reassigned
Kotlin provides us with some nice, clean mechanisms for controlling the mutability of objects, but we also need to think carefully about how we structure or classes to provide consistency.
As the concepts covered here are largely self-contained, and the code snippets can be copied / pasted directly there is no accompanying source code repo for this article.
© 2018 – 2022, Mark Allison. All rights reserved.
Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
The underlying value property in MyValue is a var, but we override this in MyValue.Mutable to make it a var. Kotlin allows us to override a val property in the base class as a var, but not the other way around.
I think it should be “The underlying value property in MyValue is a val”.
You’re right. It is now fixed. Thanks for letting me know.
I’m an Android developer currently experimenting with Kotlin, so this is really helpful. Thank you.
Great pattern, I tried it out for one of my class models for representing a discard pile in Mahjong and I really like it!
“`
package models
import java.util.*
sealed class DiscardPile(protected val tiles: Stack) {
init {
// Push an initial tile so that there’s always a tile.
tiles.push(Tile.NONE)
}
fun peekRecentlyDiscardedTile(): Tile {
val discardedTile = tiles.peek()!!
require(discardedTile != Tile.NONE)
return discardedTile
}
fun getTiles(): List {
return tiles.filter { it != Tile.NONE }.toList()
}
fun makeString(): String {
return tiles.filter { it != Tile.NONE }.windowed(15, step = 15, partialWindows = true)
.joinToString(separator = “\n”) { it.joinToString() }
}
protected fun tilesCopy(): Stack {
val stackCopy = Stack()
stackCopy.addAll(tiles)
return stackCopy
}
class Mutable(tiles: Stack? = null) : DiscardPile(tiles ?: Stack()) {
fun takeRecentlyDiscardedTile(): Tile {
val discardedTile = tiles.pop()!!
require(discardedTile != Tile.NONE)
return discardedTile
}
fun accept(tile: Tile) {
require(tile != Tile.NONE)
require(!tile.isFlowerTile())
tiles.push(tile)
}
fun immutable(): Immutable {
return Immutable(tilesCopy())
}
}
class Immutable(tiles: Stack? = null) : DiscardPile(tiles ?: Stack()) {
fun mutable(): Mutable {
return Mutable(tilesCopy())
}
}
}
“`