Kotlin ‘immutability’ is just an illusion
When I tried Kotlin for the first time, I quickly noticed the distinction between List<T>
and MutableList<T>
types in Kotlin collections API. I really liked it, until I discovered how it works. Then I tried to use some defensive programming techniques to prevent using certain implementations. It turned out that they also don’t fully work in some cases. In this article we will discuss the Kotlin approach to immutability. We will look at use cases, where it might hit us. Finally, we will try to answer if there is still a place for API-s like Guava ImmutableList
in Kotlin ecosystem.
tl;dr;
Kotlin collections API does not provide a true immutability. Kotlin data classes help creating immutable structures, but the syntax prevents using certain defensive programming techniques.
Why immutability is important?
Immutable classes have their own prominent place in the world of concurrent programming. If we can prove that something does not change, we do not need to synchronize the access to it. We also don’t need more than one copy. Before digging into Kotlin immutability, let’s take a look at its parent Java language. In Java, creating an immutable class is quite easy. However, until 2017, there was no built-in support for immutable collections. All we had was so-called ‘unmodifiable collection’ API. Let’s remind a classic example:
var mutableList = new ArrayList<String>();
mutableList.add("foo");
mutableList.add("bar");
var unmodifiableList = Collections.unmodifiableList(mutableList);
mutableList.add("joe");
// will print 'foo', 'bar' and 'joe'
System.out.println(unmodifiableList.toString());
The problem with this API was that it was just a wrapper over the original collection. It blocked methods like add()
or remove()
. Unfortunately, if we kept the reference to the original collection, we could still modify it. Let’s compare it with immutable API added in Java 9, and expanded in later versions:
var mutableList = new ArrayList<String>();
mutableList.add("foo");
mutableList.add("bar");
var immutableList = List.copyOf(mutableList);
mutableList.add("joe");
// will print 'foo' and 'bar'
System.out.println(immutableList.toString());
Unmodifiable API is useless, when we receive the collection as an argument. There’s no way to know if someone kept the original mutable reference or not. In fact, there’s even no way to check if we received an unmodifiable wrapper. We always have to create a defensive copy that consumes memory:
public class Foo {
private final List<String> strings;
public Foo(List<String> input) {
strings = new ArrayList<String>(input.size());
strings.addAll(input);
}
public List<String> getStrings() {
var result = new ArrayList<>(strings.size());
result.addAll(strings);
return result; // defensive copy here, too
}
}
We can illustrate the general rule for using mutable and immutable state safely in this way:
In short…
If you can’t prove that your collections are truly immutable, you should always create defensive copies when accepting and returning collections.
Immutability in Kotlin collections
At the first sight, Kotlin provides a great improvement. We have List<T>
and MutableList<T>
interfaces. The variant without “mutable” word does not have functions like add()
or remove()
. This means that we can’t accidentally call them.
class Foo(val strings: List<String>) {
fun doSomething() {
strings.add("foo"); // compilation error
}
fun printStrings() {
print(strings);
}
}
Nice! But what does it actually mean? One might say that you need to pass an unmodifiable list as an argument. Wrong! List<T>
does not put any constraints on the caller about the list type. It just says that the class Foo is not going to modify the collection. But the fully mutable ArrayList<T>
is both MutableList<T>
and List<T>
. This means that you can pass the mutable collection as an argument, and keep the reference to modify it from the outside:
val mutableList = ArrayList<String>();
mutableList.add("Foo");
mutableList.add("Bar");
val foo = Foo(mutableList);
mutableList.add("Joe");
foo.print(); // arghhh!!!!!! Why do I see 'Joe' here?!!
If we look closer, we’ll notice that Kotlin uses mutable list implementations almost everywhere. It does not even try to call List.of(...)
from Java 9:
val list1 = listOf("x", "y")
val list2 = mutableListOf("x", "y")
println(list1::class.simpleName); // prints 'ArrayList'
println(list2::class.simpleName); // prints 'ArrayList'
It turns out that Kotlin immutability is just an illustion, when it comes to collections. Can things get worse?
In short…
Using List<T>
over MutableList<T>
in function arguments does not prevent the caller from passing a mutable collection. In fact, you will likely get one!
Kotlin immutability vs defensive programming
We know that by using Kotlin collections API, we don’t make them immutable. In Java, we’d use some defensive programming techniques to make the actual copies. Let’s try that in Kotlin. A typical place, where we might want to enforce immutability, are data classes:
data class Foo(val strings: List<String>) {
init {
strings = ArrayList(strings) // compilation error: 'val' cannot be reassigned
}
}
In the first trial, the Kotlin constructor syntax actually prevented us from making the copy. The value was assigned to a final field before we could hook into it. Let’s try another time:
// compilation error:
// 'Data class primary constructor must only have property (val / var) parameters'
data class Foo(inputStrings: List<String>) {
val strings: List<String> = ArrayList(inputStrings)
}
This time, Kotlin complains about our primary constructor. For data classes, it must only contain property parameters. Therefore, we cannot use this way as a workaround. Maybe let’s try another time:
data class Foo private constructor(val strings: List<String>) {
companion object {
fun create(strings: List<String>) = Foo(ArrayList(strings))
}
}
It looks like it worked. The code compiles and even runs as expected. However, have we really protected our class from the mutable state? Unfortunately, no. If we use IntelliJ IDEA, it prints out a small note about our constructor:
Private primary constructor is exposed via the generated copy() method of a ‘data’ class.
This is one hack that we can use to inject a malicious reference to our class. Another one is casting the returned getter collection:
val foo = Foo.create(listOf("a", "b"))
val mutable = mutableListOf("c", "d")
val copied = foo.copy(strings = mutable)
mutable.add("e")
println(copied) // arghh!!!
val hack = foo.strings as MutableList<String>
hack.add("z")
println(foo.strings) // arghh!!!
In short…
You can’t do defensive copies in Kotlin data classes.
Can we fix Kotlin immutability?
Both Java and Kotlin have class types designed for building larger data structures: records and data classes. They remove a lot of boilerplate code. At the same time, they prevent using certain defensive programming techniques. In such a world, there’s only one valid approach to immutable collections. The immutable collection API must become an explicit, first-class citizen. In this way, we could declare our data class to require immutability:
data class Foo(val strings: ImmutableList<String>)
In longer term, a hypothetical future compiler could have a special handling for this declaration. It could implicitly make an immutable copy, if a mutable collection is passed as an argument. For now, we need to turn to third party libraries:
- Google Guava – a de-facto standard for immutable collections in Java world, with explicit
ImmutableXYZ<T>
types. - Vavr.io – provides its own collection API where immutability is the default approach. Unfortunately, it is not fully compatible with Java Collections API.
- Immutable Collections Library for Kotlin – immutable collection library for Kotlin developed by JetBrains. It looks like a work in progress, but it supports native and JS targets.
In short…
Currently, third party libraries are the only option to immutability in Kotlin collections.
Summary
Do we need to be so explicit in our Kotlin applications? It depends. If you write a library, an explicit immutable API is very helpful to stop users doing harmful things. In applications, you can apply a proper discipline among developers to avoid mutating the state by default. However, be careful here, too. The example I’m going to show now is not stricly about immutable collections, but I think it is useful anyway. I once had a class which ran some algorithm on the passed map. This algorithm only worked for maps that preserved the insertion order (LinkedHashMap
, but not HashMap
). Unfortunately, I could not enforce LinkedHashMap
in the arguments. The map could be read from the configuration by some third party serialization library. I had no way to control what implementation this library would produce. All I could do was leaving Map<K, V>
as an argument type, and… praying :).
What does it mean for us? You can write hundreds of unit tests to prove the correctness of your code, and everything can fail anyway, because someone just passed wrong collection type as an argument. The used collection types and their properties matter. The more explicit we are about them, the better the compiler will protect us against accidental mistakes.
Great article. One minor thing – I think that List.copyOf() was introduced in Java 10. Java 9 introduced only factory methods like List.of(), Set.of() etc.
Thanks, indeed the API was expanded in later versions and the method mentioned in the example appeared in Java 10. I have updated the article to avoid confusion.