1. Zapoznanie sie z List<*>
Wyobraz sobie prawdziwe zycie (i prawdziwy projekt): piszesz funkcje logowania albo debugowego wypisywania. Dla niej kompletnie nie ma znaczenia, z czego sklada sie lista — Expense, String czy Int. Ważne jest, zeby przejsc po elementach i je wydrukowac. Albo na przyklad pokazac rozmiar, pierwsze kilka elementow, a pozniej taktownie zamilknac, zeby nie spamowac konsoli.
Jesli sprobujesz zrobic „uniwersalna” funkcje przez List<Any?>, dosc szybko trafisz na to, ze tak, List jest kowariantny, ale gdy tylko zechcesz obsluzyc kolekcje modyfikowalne (MutableList) albo kontenery z bardziej zlozonymi parametrami typow, zaczna sie nieprzyjemne ograniczenia. Dlatego Kotlin daje osobna skladnie: List<*>, co czyta sie jako:
„To lista elementow jakiegos typu, ale jakiego dokladnie — nie wiadomo”.
I to „nie wiadomo” to nie filozofia, tylko konkretne reguly kompilatora: co mozna z taka lista robic, a czego nie.
2. Co mozna robic z List<*>
Jesli mamy List<*>, mozemy robic wszystko, co jest „czytaniem”: size, isEmpty(), przechodzic w for, brac elementy po indeksie. Ale jest cena: typ elementu staje sie maksymalnie bezpieczny — Any?.
Mini-przyklad „wydrukuj wszystko”:
fun printAll(xs: List<*>) {
for (x in xs) {
println(x) // x: Any?
}
}
fun main() {
printAll(listOf(1, 2, 3))
// 1
// 2
// 3
printAll(listOf("a", "b"))
// a
// b
}
Dlaczego Any?, a nie Any? Bo Kotlin dopuszcza listy, w ktorych elementy moga byc null (na przyklad List<String?>). Gdy typ jest nieznany, kompilator musi pamietac rowniez o takim wariancie.
Teraz wazna praktyczna mysl: jesli wyciagnales element z List<*>, to dalej pracujesz z nim jak z Any?. Chcesz wywolywac metody napisow — najpierw sprawdzenie typu.
fun printStringLengths(xs: List<*>) {
for (x in xs) {
if (x is String) {
println("${x} -> len=${x.length}") // smart-cast do String
}
}
}
fun main() {
printStringLengths(listOf("Kotlin", "is", "fun"))
// Kotlin -> len=6
// is -> len=2
// fun -> len=3
}
Tutaj Kotlin zachowuje sie bardzo przyjaznie: po if (x is String) robi smart cast i x.length staje sie legalne.
3. Dlaczego nie mozna dodawac do MutableList<*>
Z List<*> wszystko wyglada prawie jak „uniwersalna lista do odczytu”. I tu nowicjusze czesto robia nastepujacy skok logiczny:
„OK, skoro List<*> to lista nieznanych elementow, to MutableList<*> to modyfikowalna lista nieznanych elementow. To przeciez moge cos do niej dodac… chociazby null…”
I wlasnie tutaj Kotlin wlacza tryb „zly ochroniarz w klubie”: dodawac nie wolno.
fun tryToAdd(xs: MutableList<*>) {
// xs.add(1) // Blad kompilacji
// xs.add("hi") // Blad kompilacji
// xs.add(null) // Tez blad kompilacji
println(xs.size)
}
Dlaczego az tak twardo? Bo rzeczywista lista moze byc na przyklad MutableList<String>. Gdyby kompilator pozwolil ci dodac Int, zepsulbys kontrakt listy napisow. A gdyby pozwolil dodac null, zepsulbys MutableList<String> (gdzie null jest zabronione).
Na poziomie formalnego modelu Kotlin opisuje star-projection tak: jesli parametr typu jest nieznany, to odczyt staje sie „jak out ...”, a zapis — „jak in Nothing” (czyli „niczego nie da sie bezpiecznie zapisac”). Jest to opisane wprost w zasadach star-projection: dla inwariantnego parametru Foo<T> gwiazdka zachowuje sie jak bezpieczny odczyt gornej granicy i jak zakaz zapisu przez Nothing.
Intuicyjnie mozna to zapamietac tak: MutableList<*> to „pudelko z napisem: NIE WIADOMO CO”. Jesli zaczniesz tam wkladac przedmioty, ryzykujesz, ze dostaniesz „pudelko na lyzki”, w ktorym nagle znalazl sie srubokret. Pudelko to przetrwa, ale twoj kontrakt typow — nie.
Porownanie: * to nie „Any”
Bardzo czesta pomylka: „gwiazdka to Any”. Nie. Czasem zachowaniem przy odczycie jest podobnie, ale sens jest inny.
Ustalmy roznice na malej tabeli (i bedziemy mowic prostymi slowami, bez wchodzenia w formalna teorie typow):
| Typ | Co obiecuje kodowi wywolujacemu | Co mozna czytac | Co mozna dodawac |
|---|---|---|---|
|
„to lista, ale typ elementow jest nieznany” | |
(nie dotyczy, jest tylko do odczytu) |
|
„to lista, ktorej elementy na pewno sa typu Any?” | |
(nie dotyczy) |
|
„to modyfikowalna lista, ale typ elementow jest nieznany” | |
nic (zabronione przez kompilator) |
|
„to modyfikowalna lista, ktora przechowuje Any?” | |
mozna dodawac Any? (w tym null) |
Czyli MutableList<Any?> to „kontener na wszystko”. A MutableList<*> to „kontener nie wiadomo na co” — i wlasnie dlatego zapis jest zabroniony.
Sprawdzmy na krotkim przykladzie, dlaczego MutableList<*> jest wygodne jako parametr wejsciowy, gdy nie zamierzasz nic zapisywac:
fun printFirst(xs: MutableList<*>) {
val first = xs[0]
println("first=$first") // first: Any?
}
fun main() {
val names: MutableList<String> = mutableListOf("Ann", "Bob")
val nums: MutableList<Int> = mutableListOf(10, 20)
printFirst(names) // first=Ann
printFirst(nums) // first=10
}
Tutaj MutableList<*> jest przydatne: funkcja moze przyjac dowolna MutableList<T>, ale sama obiecuje „ja tylko czytam”. A kompilator gwarantuje, ze przypadkiem nie zaczniesz dodawac tam „czegos nie tego”.
Co by sie zepsulo bez zakazu add
Wyobrazmy sobie na chwile, ze Kotlin pozwalalby robic tak:
fun breakWorld(xs: MutableList<*>) {
xs.add(123) // zalozmy, ze to jest dozwolone (w rzeczywistosci — nie)
}
Teraz wywolujemy:
val names: MutableList<String> = mutableListOf("Ann", "Bob")
breakWorld(names)
println(names[2].length) // i co tutaj?
Gdyby add(123) bylo dozwolone, to names przestaloby byc „lista napisow” w praktyce. I dalej dostalbys albo wywalenie w runtime (na przyklad ClassCastException gdzies daleko), albo co gorsza — cicha logiczna pomylke.
Wlasnie dlatego Kotlin woli „niech sie nie kompiluje, niz ma sie zepsuc u uzytkownika”. A MutableList<*> dokladnie to znaczy: „typ jest nieznany, nie mamy prawa nic zapisywac”.
4. Praktyka: debugowe wypisywanie listy w Expense Tracker
Zeby nie zostalo to czysta teoria, wbudujmy pomysl List<*> w nasz konsolowy Expense Tracker (ten sam, w ktorym przechowujemy wydatki i pokazujemy raporty). Co jakis czas chcemy szybko sprawdzic „co teraz lezy w kolekcji”, nie tworzac dziesieciu roznych funkcji.
Zrobmy mala utilke do „drukowania listy” (zwykla funkcja, bez rozszerzen):
data class Expense(val title: String, val amount: Double)
fun printList(label: String, xs: List<*>) {
println("== $label (size=${xs.size}) ==")
for (x in xs) println(x)
}
fun main() {
val expenses = listOf(
Expense("Coffee", 3.5),
Expense("Book", 12.0)
)
val tags = listOf("food", "education")
printList("Expenses", expenses)
// == Expenses (size=2) ==
// Expense(title=Coffee, amount=3.5)
// Expense(title=Book, amount=12.0)
printList("Tags", tags)
// == Tags (size=2) ==
// food
// education
}
Zwroc uwage na wazny efekt: printList nie moze „wiedziec” nic o typie elementu — i to jest OK. Nie udaje, ze robi logike biznesowa; to narzedzie do wypisywania. W takich miejscach List<*> trafia idealnie.
Podglad: drukowanie pierwszych N elementow
Teraz dodajmy troche wiecej pozytku: drukowac pierwsze N elementow, zeby ogromna lista nie „zabila” konsoli.
fun printPreview(label: String, xs: List<*>, limit: Int) {
val n = if (xs.size < limit) xs.size else limit
println("== $label (showing $n of ${xs.size}) ==")
for (i in 0 until n) println(xs[i])
}
fun main() {
val nums = listOf(1, 2, 3, 4, 5)
printPreview("Numbers", nums, limit = 3)
// == Numbers (showing 3 of 5) ==
// 1
// 2
// 3
}
Zauwaz, jak naturalnie to sie czyta: nie znamy typu elementow — i nie musimy go znac, bo zadanie „debugowe wypisywanie” nie wymaga zadnych operacji poza drukowaniem.
5. Sprawdzenie is List<*> i analiza elementow
W prawdziwym kodzie czasem przylatuje Any (np. z bardzo ogolnego API, z drzewa JSON, z Map<String, Any?>, z zewnetrznej biblioteki). I chcesz zrozumiec: to jest lista czy nie?
Kotlin pozwala na sprawdzenie w stylu x is List<*>. To wazny moment: sprawdzenie „to lista nieznanych elementow” jest dopuszczalne, i Kotlin wprost podaje taki przyklad, zaznaczajac, ze elementy maja wtedy typ Any?.
Zrobmy mini-funkcje:
fun describeValue(x: Any?) {
if (x is List<*>) {
println("This is a list, size=${x.size}") // ok
} else {
println("This is not a list")
}
}
fun main() {
describeValue(listOf(1, 2, 3)) // This is a list, size=3
describeValue("hello") // This is not a list
}
A teraz typowy kolejny krok: „skoro to lista, to sprobujmy wyciagnac z niej napisy”. Tutaj trzeba pamietac, ze elementy to Any?, i bedzie trzeba filtrowac przez sprawdzenia typow.
fun printOnlyStrings(x: Any?) {
if (x !is List<*>) return
for (item in x) {
if (item is String) {
println(item.uppercase())
}
}
}
fun main() {
printOnlyStrings(listOf("a", 1, "b", null))
// A
// B
}
Wyglada to troche bardziej rozwlekle niz „magiczne rzutowanie”, ale dziala przewidywalnie i nie zamienia kodu w pole minowe.
6. Projekcje out/in i *
Bardzo przydaje sie trzymac w glowie roznice miedzy sytuacjami „wiem, jakie to elementy, ale chce ograniczyc operacje” a „w ogole nie wiem, jakie to elementy”.
W MutableList<out Number> wiesz, ze to „lista jakichs liczb”, i dlatego mozesz bezpiecznie czytac jako Number, ale nie mozesz dodawac (bo „jakie dokladnie liczby” jest nieznane). W MutableList<in Int> wiesz, ze bezpiecznie jest dodawac Int, ale czytac bedziesz tylko maksymalnie ogolnie (zwykle jako Any?).
A MutableList<*> to „nic nie wiem”: ani gornej granicy poza Any?, ani dolnej granicy do zapisu. Dlatego jest maksymalnie restrykcyjny w zapisie.
Można to sobie wyobrazic jako taki „suwak informacji”:
flowchart LR
A["MutableList⟨Any?⟩
(wszystko wiem: moge czytac Any? i pisac Any?)"] -->
B["MutableList⟨out Number⟩
(znam gorna granice: czytam Number, nie pisze)"] -->
C["MutableList⟨*⟩
(prawie nic nie wiem: czytam Any?, nie pisze)"]
Tak, to uproszczenie (jak wszystkie dobre wyjasnienia edukacyjne), ale swietnie dziala jako intuicja: im mniej wiesz o typie — tym mniej Kotlin pozwala ci robic.
7. Typowe bledy przy pracy ze star-projection *
Blad nr 1: myslec, ze List<*> to „lista Any”, i od razu wywolywac metody na elementach.
Gdy piszesz val x: Any? = xs[0], a potem probujesz zrobic x.length, kompilator calkiem slusznie nie rozumie, skad wiesz, ze to napis. Poprawna droga to sprawdzenie typu (if (x is String)) i dopiero potem praca z wlasciwosciami napisow.
Blad nr 2: probowac dodawac elementy do MutableList<*> i dziwic sie, ze nie wolno nawet null.
To jeden z najbardziej „bolesnych” momentow dla poczatkujacego, bo wydaje sie, ze null jest uniwersalne. Ale MutableList<*> znaczy, ze na zapis dziala tam logika poziomu „in Nothing”, czyli nie da sie zapisac nic typowo-bezpiecznego (a null tez sie nie nadaje). Jesli musisz dodawac elementy, to kontrakt funkcji powinien miec konkretny typ „wejsciowy”, np. MutableList<in Int> albo MutableList<Any?>, a nie *.
Blad nr 3: mylic MutableList<*> z MutableList<Any?> i wybierac gwiazdke „na wszelki wypadek”.
Gwiazdka nie znaczy „bardziej uniwersalne”, tylko „bardziej restrykcyjne”. Idealnie pasuje do scenariuszy „przeczytac/przejsc/wydrukowac”, ale jest niewygodna tam, gdzie chcesz cos wkladac do srodka. Jesli twoja funkcja z definicji ma wypelniac liste — to nie jest swiat *, tylko swiat in.
Blad nr 4: robic niebezpieczne rzutowania (as MutableList<String>) zamiast gwiazdki, zeby „po prostu zadzialalo”.
Czasem bardzo chce sie „docisnac kompilator”: napisac as i isc dalej. Ale w generics czesto konczy sie to tym, ze program sie kompiluje, a potem wywala w runtime w niespodziewanym miejscu. List<*> i MutableList<*> sa o tyle przydatne, ze pozwalaja uczciwie powiedziec: „nie znam typu” — i kompilator sam nie pozwoli ci wykonac niebezpiecznych operacji.
Blad nr 5: oczekiwac, ze sprawdzenie x is List<*> automatycznie da ci wiedze o typie elementow.
To sprawdzenie odpowiada tylko na pytanie „to jest lista czy nie”. Po nim elementy i tak beda typu Any? — i to jest normalne. Jesli musisz zrozumiec, czy w srodku sa napisy, liczby czy twoje Expense, musisz sprawdzac elementy (is String, is Int, is Expense) albo zbudowac inny kontrakt funkcji, w ktorym typ jest znany z gory.
GO TO FULL VERSION