programowanieŚredni programista Android

W jaki sposób w Kotlin zrealizowano niemutowalność (immutability) kolekcji i obiektów, i na czym polega różnica w praktyce względem Javy?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

Historia pytania:

W Javie niemutowalność kolekcji osiąga się za pomocą opakowań typu Collections.unmodifiableList() oraz projektowania klas immutable ręcznie (pola final, brak setterów). W praktyce programowania na Androida i w rozwoju serwerów stało się to kosztowne i niewygodne, a bezpieczeństwo wątków i przewidywalność danych zostały utracone.

Problem:

Kolekcje Javy są domyślnie niezabezpieczone, zawsze można uzyskać referencję i zmieniać obiekt. Utrudnia to kontrolę nad niemutowalnością, zwłaszcza w środowiskach wielowątkowych, i prowadzi do trudnych do wykrycia błędów.

Rozwiązanie:

Kotlin został zaprojektowany z podziałem na kolekcje read-only i mutable, a tworzenie naprawdę niemutowalnych obiektów stało się prostsze dzięki użyciu val, data class oraz List/Set/Map bez przedrostka mutable. Na tym tle - wykorzystanie kopiowania przez metody copy().

Przykład kodu:

val nums: List<Int> = listOf(1, 2, 3) // niemutowalna lista // nums.add(4) // błąd kompilacji val user = User(name = "Alex", age = 30) val updated = user.copy(age = 31) // Przykład z niemutowalnym map val state: Map<String, Int> = mapOf("a" to 1, "b" to 2)

Kluczowe cechy:

  • Kompilacja przy próbie zmiany kolekcji read-only zgłosi błąd
  • W Kotlinie val gwarantuje niemutowalność referencji, ale niekoniecznie obiektu
  • Dla naprawdę niemutowalnych — używaj tylko interfejsów read-only i zakazuj zmiany wewnętrznego stanu

Pytania pułapki.

Czy val gwarantuje absolutną niemutowalność zagnieżdżonego obiektu?

Nie! val gwarantuje tylko to, że zmienna nie może wskazywać na inny obiekt, ale pola obiektu można zmieniać (jeśli wewnętrzne właściwości var).

Czy można uzyskać mutowalny obiekt z kolekcji read-only?

Tak — jeśli kolekcja zawiera zmienny obiekt, można zmieniać jego stan, mimo interfejsu read-only kolekcji.

data class User(var name: String) val users: List<User> = listOf(User("Vasya")) users[0].name = "Petya" // dozwolone

Co się stanie przy rzutowaniu List<Int> na MutableList<Int> poprzez as?

Skopiuje się, ale wywoła ClassCastException w czasie wykonywania, jeśli pierwotny typ był interfejsem read-only.

Typowe błędy i antywzorce

  • Przyjmowanie val jako absolutnej gwarancji niemutowalności obiektu
  • Manipulacja referencjami do zmiennych obiektów wewnątrz "niemutowalnych" kolekcji
  • Rzutowanie kolekcji read-only na mutowalne

Przykład z życia

Negatywny przypadek

Aplikacja na Androida przechowuje stan w val List<User>, sądząc, że jest to niemutowalne. Jeden z programistów modyfikuje user.name. W innym fragmencie zaktualizowana lista niespodziewanie wyświetla niespójne dane — błąd pojawia się w wersji produkcyjnej.

Zalety:

  • Szybka praca z kolekcjami

Wady:

  • Utrata kontroli nad mutacjami
  • Ukryta mutowalność

Pozytywny przypadek

Użycie immutable data class wyłącznie z polami val, filtrowanie dostępu do zagnieżdżonych obiektów, aktualizacja stanu przez copy().

Zalety:

  • Wysoka przewidywalność i bezpieczeństwo wątkowe
  • Brak nieoczekiwanych efektów ubocznych

Wady:

  • Czasami wymaga więcej alokacji i kopii
  • Nie wszystkie biblioteki zewnętrzne oferują naprawdę niemutowalne obiekty