42
votes

I am learning Kotlin and it is looking likely I may want to use it as my primary language within the next year. However, I keep getting conflicting research that Kotlin does or does not have immutable collections and I'm trying to figure out if I need to use Google Guava.

Can someone please give me some guidance on this? Does it by default use Immutable collections? What operators return mutable or immutable collections? If not, are there plans to implement them?

5
There is a lightweight way to protect Kotlin collections as immutable rather than only as read-only, see: stackoverflow.com/a/38002121/3679676Jayson Minard

5 Answers

32
votes

Kotlin's List from the standard library is readonly:

interface List<out E> : Collection<E> (source)

A generic ordered collection of elements. Methods in this interface support only read-only access to the list; read/write access is supported through the MutableList interface.

Parameters
E - the type of elements contained in the list.

As mentioned, there is also the MutableList

interface MutableList<E> : List<E>, MutableCollection<E> (source)

A generic ordered collection of elements that supports adding and removing elements.

Parameters
E - the type of elements contained in the list.

Due to this, Kotlin enforces readonly behaviour through its interfaces, instead of throwing Exceptions on runtime like default Java implementations do.

Likewise, there is a MutableCollection, MutableIterable, MutableIterator, MutableListIterator, MutableMap, and MutableSet, see the stdlib documentation.

28
votes

It is confusing but there are three, not two types of immutability:

  1. Mutable - you are supposed to change the collection (Kotlin's MutableList)
  2. Readonly - you are NOT supposed to change it (Kotlin's List) but something may (cast to Mutable, or change from Java)
  3. Immutable - no one can change it (Guavas's immutable collections)

So in case (2) List is just an interface that does not have mutating methods, but you can change the instance if you cast it to MutableList.

With Guava (case (3)) you are safe from anybody to change the collection, even with a cast or from another thread.

Kotlin chose to be readonly in order to use Java collections directly, so there is no overhead or conversion in using Java collections..

17
votes

As you see in other answers, Kotlin has readonly interfaces to mutable collections that let you view a collection through a readonly lens. But the collection can be bypassed via casting or manipulated from Java. But in cooperative Kotlin code that is fine, most uses do not need truly immutable collections and if your team avoids casts to the mutable form of the collection then maybe you don't need fully immutable collections.

The Kotlin collections allow both copy-on-change mutations, as well as lazy mutations. So to answer part of your questions, things like filter, map, flatmap, operators + - all create copies when used against non lazy collections. When used on a Sequence they modify the values as the collection as it is accessed and continue to be lazy (resulting in another Sequence). Although for a Sequence, calling anything such as toList, toSet, toMap will result in the final copy being made. By naming convention almost anything that starts with to is making a copy.

In other words, most operators return you the same type as you started with, and if that type is "readonly" then you will receive a copy. If that type is lazy, then you will lazily apply the change until you demand the collection in its entirety.

Some people want them for other reasons, such as parallel processing. In those cases, it might be best to look at really high performance collections designed just for those purposes. And only use them in those cases, not in all general cases.

In the JVM world it is hard to avoid interop with libraries that want standard Java collections, and converting to/from these collections adds a lot of pain and overhead for libraries that do not support the common interfaces. Kotlin gives a good mix of interop and lack of conversion, with readonly protection by contract.

So if you can't avoid wanting immutable collections, Kotlin easily works with anything from the JVM space:

Also, the Kotlin team is working on Immutable Collections natively for Kotlin, that effort can be seen here: https://github.com/Kotlin/kotlinx.collections.immutable

There are many other collection frameworks out there for all different needs and constraints, Google is your friend for finding them. There is no reason the Kotlin team needs to reinvent them for its standard library. You have a lot of options, and they specialize in different things such as performance, memory use, not-boxing, immutability, etc. "Choice is Good" ... therefore some others: HPCC, HPCC-RT, FastUtil, Koloboke, Trove and more...

There are even efforts like Pure4J which since Kotlin supports Annotation processing now, maybe can have a port to Kotlin for similar ideals.

8
votes

Kotlin 1.0 will not have immutable collections in the standard library. It does, however, have read-only and mutable interfaces. And nothing prevents you from using third party immutable collection libraries.

Methods in Kotlin's List interface "support only read-only access to the list" while methods in its MutableList interface support "adding and removing elements". Both of these, however, are only interfaces.

Kotlin's List interface enforces read-only access at compile-time instead of deferring such checks to run-time like java.util.Collections.unmodifiableList(java.util.List) (which "returns an unmodifiable view of the specified list... [where] attempts to modify the returned list... result in an UnsupportedOperationException." It does not enforce immutability.

Consider the following Kotlin code:

import com.google.common.collect.ImmutableList
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

fun main(args: Array<String>) {
    val readOnlyList: List<Int> = arrayListOf(1, 2, 3)
    val mutableList: MutableList<Int> = readOnlyList as MutableList<Int>
    val immutableList: ImmutableList<Int> = ImmutableList.copyOf(readOnlyList)

    assertEquals(readOnlyList, mutableList)
    assertEquals(mutableList, immutableList)

    // readOnlyList.add(4) // Kotlin: Unresolved reference: add
    mutableList.add(4)
    assertFailsWith(UnsupportedOperationException::class) { immutableList.add(4) }

    assertEquals(readOnlyList, mutableList)
    assertEquals(mutableList, immutableList)
}

Notice how readOnlyList is a List and methods such as add cannot be resolved (and won't compile), mutableList can naturally be mutated, and add on immutableList (from Google Guava) can also be resolved at compile-time but throws an exception at run-time.

All of the above assertions pass with exception of the last one which results in Exception in thread "main" java.lang.AssertionError: Expected <[1, 2, 3, 4]>, actual <[1, 2, 3]>. i.e. We successfully mutated a read-only List!

Note that using listOf(...) instead of arrayListOf(...) returns an effectively immutable list as you cannot cast it to any mutable list type. However, using the List interface for a variable does not prevent a MutableList from being assigned to it (MutableList<E> extends List<E>).

Finally, note that an interface in Kotlin (as well as in Java) cannot enforce immutability as it "cannot store state" (see Interfaces). As such, if you want an immutable collection you need to use something like those provided by Google Guava.


See also ImmutableCollectionsExplained · google/guava Wiki · GitHub

2
votes

NOTE: This answer is here because the code is simple and open-source and you can use this idea to make your collections that you create immutable. It is not intended only as an advertisement of the library.

In Klutter library, are new Kotlin Immutable wrappers that use Kotlin delegation to wrap a existing Kotlin collection interface with a protective layer without any performance hit. There is then no way to cast the collection, its iterator, or other collections it might return into something that could be modified. They become in effect Immutable.

Klutter 1.20.0 released which adds immutable protectors for existing collections, based on a SO answer by @miensol provides a light-weight delegate around collections that prevents any avenue of modification including casting to a mutable type then modifying. And Klutter goes a step further by protecting sub collections such as iterator, listIterator, entrySet, etc. All of those doors are closed and using Kotlin delegation for most methods you take no hit in performance. Simply call myCollection.asReadonly() (protect) or myCollection.toImmutable() (copy then protect) and the result is the same interface but protected.

Here is an example from the code showing how simply the technique is, by basically delegating the interface to the actual class while overriding mutation methods and any sub-collections returned are wrapped on the fly.

/**
 * Wraps a List with a lightweight delegating class that prevents casting back to mutable type
 */
open class ReadOnlyList <T>(protected val delegate: List<T>) : List<T> by delegate, ReadOnly, Serializable {
    companion object {
        @JvmField val serialVersionUID = 1L
    }

    override fun iterator(): Iterator<T> {
        return delegate.iterator().asReadOnly()
    }

    override fun listIterator(): ListIterator<T> {
        return delegate.listIterator().asReadOnly()
    }

    override fun listIterator(index: Int): ListIterator<T> {
        return delegate.listIterator(index).asReadOnly()
    }

    override fun subList(fromIndex: Int, toIndex: Int): List<T> {
        return delegate.subList(fromIndex, toIndex).asReadOnly()
    }

    override fun toString(): String {
        return "ReadOnly: ${super.toString()}"
    }

    override fun equals(other: Any?): Boolean {
        return delegate.equals(other)
    }

    override fun hashCode(): Int {
        return delegate.hashCode()
    }
}

Along with helper extension functions to make it easy to access:

/**
 * Wraps the List with a lightweight delegating class that prevents casting back to mutable type,
 * specializing for the case of the RandomAccess marker interface being retained if it was there originally
 */
fun <T> List<T>.asReadOnly(): List<T> {
    return this.whenNotAlreadyReadOnly {
        when (it) {
            is RandomAccess -> ReadOnlyRandomAccessList(it)
            else -> ReadOnlyList(it)
        }
    }
}

/**
 * Copies the List and then wraps with a lightweight delegating class that prevents casting back to mutable type,
 * specializing for the case of the RandomAccess marker interface being retained if it was there originally
 */
@Suppress("UNCHECKED_CAST")
fun <T> List<T>.toImmutable(): List<T> {
    val copy = when (this) {
        is RandomAccess -> ArrayList<T>(this)
        else -> this.toList()
    }
    return when (copy) {
        is RandomAccess ->  ReadOnlyRandomAccessList(copy)
        else -> ReadOnlyList(copy)
    }
}

You can see the idea and extrapolate to create the missing classes from this code which repeats the patterns for other referenced types. Or view the full code here:

https://github.com/kohesive/klutter/blob/master/core-jdk6/src/main/kotlin/uy/klutter/core/common/Immutable.kt

And with tests showing some of the tricks that allowed modifications before, but now do not, along with the blocked casts and calls using these wrappers.

https://github.com/kohesive/klutter/blob/master/core-jdk6/src/test/kotlin/uy/klutter/core/collections/TestImmutable.kt