2016-06-20 14 views
20

Pisałem mały fragment kodu, w którym wewnętrznie przetwarzam moje dane w zmiennej mapie, która z kolei ma zmienne listy.Jak zmienić kolekcję z możliwością modyfikacji w niezmienny?

Chciałem udostępnić moje dane użytkownikowi API, ale aby uniknąć niebezpiecznej publikacji moich danych, chciałem udostępnić je w niezmiennych kolekcjach, nawet jeśli wewnętrznie są obsługiwane przez zmienne.

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

udało mi się wystawiać tylko Map i List (które są niezmienne) w publicznych API mojej klasy, ale przypadki Jestem rzeczywiście eksponujące są nadal natury zmienny.

Co oznacza, że ​​użytkownik API może po prostu rzucić moją zwróconą mapę jako ImmutableMap i uzyskać dostęp do cennych prywatnych danych wewnętrznych mojej klasy, która miała być chroniona przed tego rodzaju dostępem.

Nie mogłem znaleźć konstruktora kopii w metodach fabryki kolekcji mutableMapOf() lub mutableListOf() i zastanawiałem się, jaki jest najlepszy i najskuteczniejszy sposób przekształcenia zmieniającej się kolekcji w niezmienny.

Wszelkie porady lub rekomendacje?

Odpowiedz

9

Obecnie w Kotlin stdlib nie istnieją implementacje List<T> (Map<K,V>), które nie byłoby również wdrażająMutableList<T> (MutableMap<K,V>). Jednak ze względu na Kotlin's delegation feature implementacje stać jeden wkładki:

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 

Można również wzmacniać tworzenie niezmiennych odpowiednikami z metod rozszerzeń:

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

Powyższy zapobiega dzwoniącego z modyfikując List (Map) przez rzutowanie na inną klasę.Jednak nadal istnieją powody, aby utworzyć kopię oryginalnego pojemnika, aby zapobiec subtelne kwestie jak 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) 

Chociaż powyższy nie jest trudne do wdrożenia nie są już implementacje niezmiennych wykazów i map zarówno Guava i Eclipse-Collections.

+1

Pełniejsza wersja tego została dodana do Kluttera blokującego wszystkie powiązane akcje, takie jak 'Iterator' otrzymany z listy,' subList' z listy, 'entrySet' z mapy i więcej. Zobacz: http://stackoverflow.com/a/38002121/3679676 –

5

Jak wspomniano here i here, trzeba by napisać własny List implementację że albo użyć istniejącego (ImmutableList guawy przychodzi do głowy, albo Eclipse Collections jak Andrew sugerowane).

Kotlin wymusza zmianę (im) zmienności tylko przez interfejs. Nie ma implementacji List, które nie implementują również MutableList.

Nawet idiomatyczne listOf(1,2,3) dzwoniąc Kotlin na ArraysUtilJVM.asList() który wzywa Java Arrays.asList() która zwraca zwykły stary Java ArrayList.

Jeśli bardziej dbać o ochronę własnej listy wewnętrznej, niż o samej niezmienności, można oczywiście skopiować całą kolekcję i zwraca go jako List, podobnie jak Kotlin robi:

return ArrayList(original) 
+2

małą korektę, '' Arrays.asList' zwraca java.util. Arrays.ArrayList' (nie mylić z 'java.util.ArrayList'). Ta lista jest w pewnym stopniu niezmienna, ponieważ nie można dodawać ani usuwać elementów, ale można zastąpić elementy według indeksu. –

3

wiem to pytanie specyficzne dla Kotlin i @Malt jest poprawne, ale chciałbym dodać alternatywę. W szczególności uważam, że Eclipse Collections, formalnie GS-Collections, jest dla większości przypadków lepszą alternatywą dla Guava i uzupełnia dobrze wbudowane kolekcje Kotlin.

2

Klasycznym rozwiązaniem jest skopiowanie danych, tak, że nawet w przypadku zmian, zmiana nie wpłynie na własności prywatnej Klasa:

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

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