1. Introduction

In this tutorial, we’ll be looking at how to create immutable collections in Kotlin.

First, we’ll explore the types of immutability as well as what Kotlin provides as standard. Then, we’ll look at how to leverage Google’s Guava library to create truly immutable collections.

As an alternative, we’ll also look at the Kotlinx Immutable Collections Library for Kotlin.

2. Dependencies

Before creating our immutable collections, we’ll need to import Guava and the Immutable Collections Library.

2.1. Maven

<!-- https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-collections-immutable -->
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-collections-immutable</artifactId>
    <version>0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.1-jre</version>
</dependency>
<repository>
    <snapshots>
        <enabled>false</enabled>
    </snapshots>
    <id>kotlinx</id>
    <name>bintray</name>
    <url>https://dl.bintray.com/kotlin/kotlinx</url>
</repository>

2.2. Gradle

repositories {
    maven {
        url "https://dl.bintray.com/kotlin/kotlinx"
    }
}

// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-collections-immutable
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-collections-immutable', version: '0.1'
// https://mvnrepository.com/artifact/com.google.guava/guava
compile group: 'com.google.guava', name: 'guava', version: '27.1-jre'

3. Types of Immutability

Before we begin, let’s take a look at the different types of immutability a collection can possess:

  1. Mutable – The contents of the list can be freely changed
  2. Read-Only – The contents of the collection are not meant to be changed. However, the underlying data can be changed
  3. Immutable – Nothing can change the contents of the collection

Immutable collections have many uses in programming. For example, we can share them freely between different threads with no risk of running into race conditions. Also, immutable collection implementations are always more memory-efficient than their mutable alternatives.

Further, the use of Immutable collections is a great defensive programming technique – ensuring we don’t make unwanted changes to our data.

4. Kotlin Collections

In Kotlin, all non-mutable collections, such as List, are compile-time read-only by default, and not immutable. While the defined interfaces do not support methods to change data within the collection, the underlying data can still be changed.

Let’s demonstrate this by trying to change our read-only List:

@Test
fun givenReadOnlyList_whenCastToMutableList_checkNewElementsAdded(){

    val list: List<String> = listOf("This", "Is", "Totally", "Immutable")

    (list as MutableList<String>)[2] = "Not"

    assertEquals(listOf("This", "Is", "Not", "Immutable"), list)

}

In our above example, we create a new List and assign it to the list variable. By default, Kotlin’s List interface is read-only and doesn’t allow us to add new elements to our list. However, by casting the List to a MutableList, we are free to add new elements through its add method.

5. Guava

For absolute immutability, we can make use of Guava’s set of immutable collections. Guava provides immutable versions of many Java collections, including ImmutableList, ImmutableSet and ImmutableMap.

Let’s take a look at an ImmutableList in action, by using the ImmutableList.of method:

@Rule
@JvmField
var ee : ExpectedException = ExpectedException.none()

@Test 
fun givenImmutableList_whenAddTried_checkExceptionThrown() { 

    val list: List<String> = ImmutableList.of("I", "am", "actually", "immutable") 
    ee.expect(UnsupportedOperationException::class.java) 
    (list as MutableList<String>).add("Oops") 

}

In our example, we can see that even with a cast to MutableList, our list resists mutation. Rather than accept the new element, an UnsupportedOperationException is thrown at runtime.

Guava also provides alternative methods of instantiating immutable lists.

5.1. CopyOf

Next, let’s look at how to instantiate an ImmutableList from a previously created mutable one. To achieve this, we can make use of the copyOf method, which takes another collection as its argument:

@Rule 
@JvmField 
var ee : ExpectedException = ExpectedException.none()

@Test
fun givenMutableList_whenCopiedAndAddTried_checkExceptionThrown(){

    val mutableList : List<String> = listOf("I", "Am", "Definitely", "Immutable")

    (mutableList as MutableList<String>)[2] = "100% Not"

    assertEquals(listOf("I", "Am", "100% Not", "Immutable"), mutableList)

    val list: List<String> = ImmutableList.copyOf(mutableList)

    ee.expect(UnsupportedOperationException::class.java)

    (list as MutableList<String>)[2] = "Really?"

}

Here, we create a fully mutable list with the use of the listOf method. We then create an immutable one, with use of the copyOf method. Finally, we see that our code throws an exception when an element is added to our ImmutableList.

5.2. A Handy Builder

Finally, let’s take a peek at the useful builders that Guava provides to construct immutable collections. In this example, we’ll create an ImmutableSet. The ImmutableSet.Builder provides all the fundamental methods for adding new single elements as well as copies of other collections:

@Rule 
@JvmField 
var ee : ExpectedException = ExpectedException.none()

@Test
fun givenImmutableSetBuilder_whenAddTried_checkExceptionThrown(){

    val mutableList : List<String> = ArrayList(listOf("Hello", "Baeldung"))
    val set: ImmutableSet<String> = ImmutableSet.builder<String>()
      .add("I","am","immutable") 
      .addAll(mutableList)
      .build() 

    assertEquals(setOf("Hello", "Baeldung", "I", "am", "immutable"), set)

    ee.expect(UnsupportedOperationException::class.java) 

    (set as MutableSet<String>).add("Oops") 

}

In our above example, we construct an ImmutableSet of Strings using Guava’s ImmutableSet.Builder class. First, we add some single Strings using the add method before adding the contents of mutableList using the builder’s addAll method.

After validating the contents, we can see that, as expected, an attempt to add further elements results in an exception being thrown.

6. Kotlinx Immutable Collections Library

JetBrain’s answer to the read-only nature of Kotlin’s collections is the Kotlinx Immutable Collections Library – what a mouthful, let’s call it KICL for short. KICL provides immutable collection interfaces and implementation prototypes for Kotlin.

Weighing in at just a fraction of Guava’s 2.6MB, KICL provides a lightweight alternative to Guava when we only need a bit of immutability in our application.

Let’s take a quick look at this library in action:

@Rule
@JvmField
var ee : ExpectedException = ExpectedException.none()

@Test
fun givenKICLList_whenAddTried_checkExceptionThrown(){

    val list: ImmutableList<String> = immutableListOf("I", "am", "immutable")

    list.add("My new item")

    assertEquals(listOf("I", "am", "immutable"), list)
    
}

Kotlinx’s immutable collection library works slightly differently than Guava’s collections. As we can see above, rather than throwing an UnsupportedOperationException, there is simply no new element added to the ImmutableList.

7. Conclusion

In this article, we’ve taken a look at what Kotlin provides in terms of immutable collections.

Then, we took a deep dive into the immutable collections that Google’s Guava can offer us, and at the Kotlinx Immutable Collection Library.

All code snippets can be found over on GitHub.

Comments are closed on this article!