Как превратить Mutable Collection в неизменяемую

Я писал небольшой фрагмент кода, в котором я внутренне обрабатываю свои данные на изменяемой карте, которая, в свою очередь, имеет изменяемые списки.

Я хотел выставить свои данные пользователю API, но чтобы избежать какой-либо небезопасной публикации моих данных, я хотел разоблачить ее в неизменяемых коллекциях, даже когда внутренне обрабатываются изменчивыми.

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!
}

Мне удалось отобразить только public Map и List (которые являются неизменяемыми) в публичном API моего класса, но экземпляры, которые я фактически просматриваю, по-прежнему по своей сути изменяемы.

Это означает, что пользователь API может просто передать мою возвращенную карту как ImmutableMap и получить доступ к ценным личным данным, внутренним для моего класса, который должен был быть защищен от такого доступа.

Я не смог найти конструктор копирования в методах коллекции factory mutableMapOf() или mutableListOf(), и поэтому мне было интересно, что является лучшим и наиболее эффективным способом превратить изменчивую коллекцию в неизменяемую.

Любые советы или рекомендации?

Ответ 1

В настоящее время в Kotlin stdlib нет реализаций List<T> (Map<K,V>), что также не будет реализовывать MutableList<T> (MutableMap<K,V>). Однако из-за функции делегации Котлина реализации становятся одним лайнером:

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

Вы также можете улучшить создание неизменяемых копий с помощью методов расширения:

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)
    }
}

Вышеизложенное не позволяет вызывающему пользователю модифицировать List (Map) путем кастинга в другой класс. Однако по-прежнему существуют причины для создания копии исходного контейнера, чтобы предотвратить такие тонкие проблемы, как 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)

В то время как вышесказанное не так сложно реализовать, уже есть реализации неизменяемых списков и карт в Guava и Eclipse-Collections.

Ответ 2

Как упоминалось здесь и здесь, вам нужно написать для этого собственную реализацию List или использовать существующую (на ум приходит Guava ImmutableList, или Eclipse Коллекции, как предложил Андрей).

Kotlin обеспечивает изменяемость списка (im) только интерфейсом. Нет никаких реализаций List, которые также не реализуют MutableList.

Даже идиоматический listOf(1,2,3) заканчивает тем, что вызывает Kotlin ArraysUtilJVM.asList(), который вызывает Java Arrays.asList(), который возвращает простую старую Java ArrayList.

Если вы больше заботитесь о защите своего внутреннего списка, чем о самой неизменности, вы, конечно, можете скопировать всю коллекцию и вернуть ее как List, как это делает Котлин:

return ArrayList(original)

Ответ 3

Я знаю это конкретный вопрос Котлин и @Malt, но я хотел бы добавить альтернативу. В частности, я нахожу Eclipse Collections, формально GS-Collections, как лучшую альтернативу Guava для большинства случаев, и это хорошо дополняет коллекцию Kotlin.

Ответ 4

Классическим решением является копирование ваших данных, так что даже если они будут изменены, это изменение не повлияет на свойство private class:

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

    fun db(): Map<Int, List<String>> = roster.mapValuestTo {it.value.toList}
}

Ответ 5

Преобразует изменяемый список в неизменяемый список:

Изменяемый список:

val mutableList = mutableListOf<String>()

В неизменный список:

val immutableList = Collections.unmodifiableList(mutableList)