60

I was writing a small piece of code in which I internally handle my data in a mutable map, which in turn has mutable lists.

I wanted to expose my data to the API user, but to avoid any unsafe publication of my data I wanted to expose it in immutable collections even when internally being handled by mutable ones.

class School {

    val roster: MutableMap<Int, MutableList<String>> = mutableMapOf<Int, MutableList<String>>()

    fun add(name: String, grade: Int): Unit {
        val students = roster.getOrPut(grade) { mutableListOf() }
        if (!students.contains(name)) {
            students.add(name)
        }
    }

    fun sort(): Map<Int, List<String>> {
        return db().mapValues { entry -> entry.value.sorted() }
                .toSortedMap()
    }

    fun grade(grade: Int) = db().getOrElse(grade, { listOf() })
    fun db(): Map<Int, List<String>> = roster //Uh oh!
}

I managed to expose only Map and List (which are immutable) in the public API of my class, but the instances I am actually exposing are still inherently mutable.

Which means an API user could simply cast my returned map as an ImmutableMap and gain access to the precious private data internal to my class, which was intended to be protected of this kind of access.

I couldn't find a copy constructor in the collection factory methods mutableMapOf() or mutableListOf() and so I was wondering what is the best and most efficient way to turn a mutable collection into an immutable one.

Any advice or recommendations?

Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205

8 Answers8

26

Use Collections to converts a Mutable list to Immutable list, Example:

Mutable list:

val mutableList = mutableListOf<String>()

Converts to Immutable list:

val immutableList = Collections.unmodifiableList(mutableList)
Benny
  • 2,233
  • 1
  • 22
  • 27
  • 1
    What's the time complexity of Collections.unmodifiableList()? What does it do internally? Does it iterate through all elements of the mutableList to create a new immutableList? – PlsWork May 30 '22 at 07:51
  • Time complexity of `Collections.unmodifiableList()` is O(1) as it simply wraps the list with an `UnmodifiableList`. `UnmodifiableList` class overrides all methods that mutate the list to return `UnsupportedOperationException`. [Docs](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Collections.html#unmodifiableList(java.util.List)) | [Source code](https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/util/Collections.java#L1285) – Karthick Vinod Jul 14 '23 at 04:34
18

Currently in Kotlin stdlib there are no implementations of List<T> (Map<K,V>) that would not also implement MutableList<T> (MutableMap<K,V>). However due to Kotlin's delegation feature the implementations become one liners:

class ImmutableList<T>(private val inner:List<T>) : List<T> by inner
class ImmutableMap<K, V>(private val inner: Map<K, V>) : Map<K, V> by inner

You can also enhance the creation of the immutable counterparts with extension methods:

fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> {
    if (this is ImmutableMap<K, V>) {
        return this
    } else {
        return ImmutableMap(this)
    }
}

fun <T> List<T>.toImmutableList(): List<T> {
    if (this is ImmutableList<T>) {
        return this
    } else {
        return ImmutableList(this)
    }
}

The above prevents a caller from modifying the List (Map) by casting to a different class. However there are still reasons to create a copy of the original container to prevent subtle issues like ConcurrentModificationException:

class ImmutableList<T> private constructor(private val inner: List<T>) : List<T> by inner {
    companion object {
        fun <T> create(inner: List<T>) = if (inner is ImmutableList<T>) {
                inner
            } else {
                ImmutableList(inner.toList())
            }
    }
}

class ImmutableMap<K, V> private constructor(private val inner: Map<K, V>) : Map<K, V> by inner {
    companion object {
        fun <K, V> create(inner: Map<K, V>) = if (inner is ImmutableMap<K, V>) {
            inner
        } else {
            ImmutableMap(hashMapOf(*inner.toList().toTypedArray()))
        }
    }
}

fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> = ImmutableMap.create(this)
fun <T> List<T>.toImmutableList(): List<T> = ImmutableList.create(this)

While the above is not hard to implement there are already implementations of immutable lists and maps in both Guava and Eclipse-Collections.

miensol
  • 39,733
  • 7
  • 116
  • 112
  • 1
    A fuller version of this was added to Klutter blocking all related actions such as an `Iterator` recieved from a list, a `subList` from a list, a `entrySet` from a map and more. See: http://stackoverflow.com/a/38002121/3679676 – Jayson Minard Jun 23 '16 at 21:26
10

As mentioned here and here, you'd need to write your own List implementation for that, or use an existing one (Guava's ImmutableList comes to mind, or Eclipse Collections as Andrew suggested).

Kotlin enforces list (im)mutability by interface only. There are no List implementations that don't also implement MutableList.

Even the idiomatic listOf(1,2,3) ends up calling Kotlin's ArraysUtilJVM.asList() which calls Java's Arrays.asList() which returns a plain old Java ArrayList.

If you care more about protecting your own internal list, than about the immutability itself, you can of course copy the entire collection and return it as an List, just like Kotlin does:

return ArrayList(original)
Malt
  • 28,965
  • 9
  • 65
  • 105
  • 4
    Small correction, `Arrays.asList` returns a `java.util.Arrays.ArrayList` (not to be confused with `java.util.ArrayList`). This list is somewhat immutable in that you can't add or remove items, but you can replace items by index. – Kirill Rakhman Jun 21 '16 at 07:24
9

Simply call toMap() on your MutableMap.

val myMap = mutableMapOf<String, String>("x" to "y").toMap()

Done.

The same also works for lists.

Renann
  • 552
  • 1
  • 7
  • 12
  • 2
    maybe this was correct at some point, but looking at the implementation this still results in a mutable map: `else -> toMutableMap()` – dpozinen May 08 '22 at 16:38
  • Kotlin seems to perform some magic, though, despite returning a map which is Mutable. It will happily cast it, but it throws an error trying to mutate it. You can see an example in this playground: https://pl.kotl.in/nwer4AExQ Badly formatted code from playground below: `val a = mutableMapOf(1 to 1) val b: Map = a.toMap() val c = b.toMutableMap() val d: MutableMap = if (b is MutableMap<*,*>) b as MutableMap else mutableMapOf() c[2] = 2 println("a = $a, b = $b, c = $c, d = $d") d[4] = 4 // crashes println("a = $a, b = $b, c = $c, d = $d") ` – Lee K-A Dec 23 '22 at 20:40
3

I know this a Kotlin specific question and @Malt is correct but I would like to add an alternative. In particular I find Eclipse Collections, formally GS-Collections, as a better alternative to Guava for most cases and it supplements Kotlin's built in collections well.

Andrew White
  • 52,720
  • 19
  • 113
  • 137
2

A classic solution is to copy your data, so that even if modified, the change would not affect the private class property:

class School {
    private val roster = mutableMapOf<Int, MutableList<String>>()

    fun db(): Map<Int, List<String>> = roster.mapValuestTo {it.value.toList}
}
voddan
  • 31,956
  • 8
  • 77
  • 87
0

If you need to convert MutableMap<String, Any> strictly to ImmutableMap<String, Any>, this will not work

val iMap : ImmutableMap<String, Any> = mutMap.toMap() // error

I have found the only working way:

val iMap : ImmutableMap<String, Any> =  ImmutableMap.builder<String, Any>().putAll(mutMap).build()
buddemat
  • 4,552
  • 14
  • 29
  • 49
tony
  • 1
-1

Use simple bellow codes to convert Mutable Collections into an Immutable one:

val mutableList = mutableListOf<String>()

val immutableList: List<String> = mutableList 
Alireza Barakati
  • 1,016
  • 2
  • 9
  • 23
  • I'm tempted to downvote this -- The poster already said that they are doing that, but don't like that this exposes the internal structure if someone wanted to cast it. Calling `.toMap()` is a much better choice IMO. – Lee K-A Dec 23 '22 at 20:56