1. Perche all’improvviso gli array non bastano piu
Se in questo momento pensi: «Perche mi servono altre collezioni, se ho gia gli array?», e una reazione normale. Gli array sono davvero ottimi per i compiti in cui il numero di elementi e noto in anticipo e quasi non cambia. Ma non appena inizi a scrivere un programma “per la vita reale”, e non “per il compito in classe”, salta fuori una cosa spiacevole: la vita reale non ama le dimensioni fisse.
Immagina di fare un mini registro spese da console (svilupperemo questa app piu avanti nel corso). Quante voci ci saranno? 5? 50? 500? Non lo sai. E se scegli un array, o imposti una dimensione enorme “per sicurezza”, oppure inizi a ricreare l’array di continuo. Entrambe le opzioni non sono il massimo: la prima spreca memoria e sembra strana, la seconda trasforma il codice in uno sport di trasloco di scatoloni.
L’array come una scatola con celle numerate
Un array e comodo da immaginare come una scatola con delle celle, e ogni cella ha un numero (indice). La scatola viene creata subito “con N celle”, e non puoi ingrandirla: puoi solo creare una nuova scatola piu grande e spostare li tutto il contenuto. In Kotlin gli array sono sempre mutabili: un elemento puo essere sostituito tramite arr[i] = ..., ma la dimensione rimane la stessa. E importante ricordarlo, perche un “array mutabile” non e un “array che cresce”.
Ecco un esempio minimale che mostra che l’array “mantiene la dimensione”:
fun main() {
val arr = arrayOf(1, 2, 3)
println(arr.size) // 3
arr[0] = 10
println(arr[0]) // 10
}
Qui abbiamo cambiato il valore, ma la dimensione — da 3 — e rimasta 3.
Come “espandevamo” un array prima e perche da fastidio
Quando bisogna “aggiungere un elemento” a un array, il percorso tipico e creare un nuovo array di dimensione maggiore e copiare dentro quello vecchio. Anche se lo fai con cura, il cervello resta costantemente occupato non dal problema del programma, ma dalla contabilita della memoria: “quanti posti”, “dove copiare”, “non dimenticare l’indice”.
Esempio in stile “aggiungiamo un elemento” (idea, non stile finale dell’app):
fun main() {
var names = arrayOf("Ann", "Bob")
println(names.contentToString()) // [Ann, Bob]
names = names.copyOf(names.size + 1)
names[names.lastIndex] = "Cara"
println(names.contentToString()) // [Ann, Bob, Cara]
}
Funziona? Si. E comodo? Non molto. E soprattutto: non appena questi “aggiungi” diventano tanti, il codice inizia a sembrare come se stessi scrivendo non un programma di gestione spese, ma un manager di traslochi per array.
2. Collezioni: contenitori a dimensione variabile
Le collezioni sono il passo logico successivo: contenitori che sanno memorizzare un numero variabile di elementi (anche zero, anche mille) e che forniscono operazioni comode di aggiunta, rimozione e ricerca.
Con le collezioni, piu spesso pensi «cosa voglio fare con i dati», e non «come spostare manualmente i dati in una nuova scatola». E uno di quei momenti in cui un linguaggio di programmazione rende davvero la vita piu semplice, e non aggiunge solo nuove parole al tuo vocabolario.
Tre forme di base: List, Set, Map
Quando si dice “collezioni”, di solito si intendono tre grandi famiglie: lista, insieme e mappa.
Per scegliere piu facilmente “per significato”, tieni davanti agli occhi questa tabella:
| Tipo | Cosa contiene | Cosa conta | Duplicati | Uso tipico |
|---|---|---|---|---|
|
elementi | ordine e indice | ammessi | lista spese, lista task, cronologia eventi |
|
elementi | unicita | non memorizzati separatamente | insieme di comandi, insieme di categorie, “gia visto” |
|
coppie chiave → valore | accesso per chiave | chiavi uniche | impostazioni, dizionario, “id → oggetto”, contatori |
Ora non approfondiamo iterazioni, ordinamenti e “operazioni belle sulle collezioni” — verra piu avanti. Il nostro compito di oggi e piu semplice: capire perche le collezioni non sono uguali agli array, in cosa differiscono List/Set/Map, e imparare a crearle.
Un dettaglio importante: gli array non sono collezioni
E una finezza che sorprendentemente ti salva dalla confusione: in Kotlin gli array sono considerati un’entita separata. Nella documentazione c’e persino una nota esplicita che gli array non sono un tipo di collezione.
In pratica significa due cose. Primo: un array ha il suo insieme di possibilita e funzioni, e le collezioni hanno il loro. Secondo: quando leggi codice, e importante non pensare “array e lista sono la stessa cosa, solo scritta in modo diverso”. Sono simili all’esterno (entrambi hanno elementi), ma la logica d’uso spesso e diversa.
3. Read-only vs mutable: perche Kotlin separa “leggere” e “modificare”
In Kotlin c’e un’idea importante: “il tipo dice cosa puoi farci”. Per questo le collezioni si dividono in interfacce read-only e interfacce mutable. E non e solo “amore per complicare la vita allo studente”, ma una protezione pratica contro modifiche accidentali dei dati.
Nella libreria standard, per ogni tipo base di collezione c’e una coppia di interfacce: read-only (solo lettura) e mutable (lettura + modifica). E proprio per questo che esistono le coppie List/MutableList, Set/MutableSet, Map/MutableMap. Sembra “due parole quasi identiche”, ma il significato e diverso.
Una collezione read-only significa “da questo riferimento non puoi modificare”
Un tipo read-only (List, Set, Map) significa: tramite questa variabile non hai operazioni di modifica (add, remove, assegnazioni per indice, ecc.). Puoi leggere la dimensione, prendere elementi, verificare la presenza, ma non “ricostruire il contenitore”.
E molto utile quando vuoi dire esplicitamente: «qui leggiamo soltanto». Per esempio, una funzione riceve una lista di spese e deve solo calcolare la somma (e non “cancellare accidentalmente meta delle spese durante il calcolo”, perche qualcuno ha confuso i metodi).
Un esempio “solo lettura” e cosi:
fun main() {
val names: List<String> = listOf("Ann", "Bob", "Ann")
println(names.size) // 3
println(names) // [Ann, Bob, Ann]
}
Qui names e una lista read-only. Puo contenere duplicati, e l’ordine degli elementi si mantiene.
Una collezione mutable significa “puoi modificare il contenuto”
Un tipo mutable (MutableList, MutableSet, MutableMap) significa: puoi aggiungere, rimuovere, aggiornare elementi.
L’esempio piu basilare:
fun main() {
val numbers = mutableListOf(1, 2, 3)
numbers.add(4)
println(numbers) // [1, 2, 3, 4]
}
Qui succede la “magia” per cui siamo venuti: la lista e cresciuta senza ricreare la scatola.
val e la “mutabilita interna”: una fonte frequente di confusione
Una regola molto importante, che ti “rompe il cervello” esattamente una volta (e poi diventa ovvia): val vieta di riassegnare la variabile, ma non vieta di modificare l’oggetto a cui la variabile fa riferimento.
Guarda l’esempio e senti la differenza:
fun main() {
val xs = mutableListOf("one", "two", "three")
xs.add("four") // si puo
println(xs) // [one, two, three, four]
// xs = mutableListOf("X") // no: con val non puoi riassegnare
}
E uno stile super diffuso: “collezione mutable, ma variabile val”. Cosi dici: “il contenitore puo cambiare, ma il contenitore (come oggetto) non viene sostituito per sbaglio con un altro”.
Read-only non significa “assolutamente immutabile in tutto l’universo”
Un altro dettaglio sottile: un tipo read-only non promette che i dati non cambino mai. Promette solo che tramite questo specifico riferimento non puoi chiamare metodi di modifica.
Se da qualche parte esiste un altro riferimento alla stessa collezione mutable, le modifiche possono avvenire “dall’altra parte”. A questo punto del corso non serve entrare in temi complessi sulle “reference condivise”, ma e utile ricordare l’intuizione: read-only e una limitazione dell’API, non una cupola magica di immutabilita.
4. Creazione delle collezioni: listOf, mutableListOf, setOf, mapOf
Finora abbiamo creato array tramite arrayOf(...) o IntArray(...). Con le collezioni e ancora piu semplice: in Kotlin ci sono funzioni factory che creano il tipo necessario a partire dai valori. Si legge quasi come testo normale: “crea una lista con questi elementi”.
E si, queste funzioni si usano continuamente: le vedrai negli esempi, nella documentazione e nel codice reale. Quindi e importante farci l’abitudine adesso, prima di iniziare a costruire logica basata sulle collezioni.
listOf(...) e mutableListOf(...)
listOf(...) crea una List read-only. E comodo quando hai un “insieme di valori” che non prevedi di modificare.
mutableListOf(...) crea una MutableList, che puo crescere e ridursi.
Un esempio affiancato per confrontare:
fun main() {
val readOnly = listOf("A", "B", "C")
println(readOnly) // [A, B, C]
val mutable = mutableListOf("A", "B", "C")
mutable.add("D")
println(mutable) // [A, B, C, D]
}
Consiglio di significato per il futuro (senza fanatismo): se non sei sicuro di dover modificare, parti dal read-only. Prendi il mutable quando le modifiche sono davvero necessarie.
setOf(...) e mutableSetOf(...)
setOf(...) crea un insieme di elementi unici. Se ripeti accidentalmente un elemento, l’insieme non diventa “piu lungo”, perche riguarda l’unicita.
Esempio base:
fun main() {
val tags = setOf("kotlin", "kotlin", "jvm")
println(tags) // per esempio: [kotlin, jvm]
println(tags.size) // 2
}
Nota: non usiamo l’ordine di output come parte della logica. Interpretiamo Set come un “sacchetto di valori unici”.
Ecco la variante mutable:
fun main() {
val commands = mutableSetOf("add", "list")
commands.add("remove")
commands.add("add") // duplicato
println(commands) // [add, list, remove] (il duplicato non e stato aggiunto)
}
mapOf(...), mutableMapOf(...) e la funzione infissa to
Una mappa (Map) memorizza coppie “chiave → valore”. Le chiavi sono uniche.
Creare una mappa e comodo tramite to:
fun main() {
val prices = mapOf(
"tea" to 90,
"coffee" to 150
)
println(prices) // {tea=90, coffee=150}
}
Con mutableMapOf puoi modificare e aggiungere valori:
fun main() {
val stock = mutableMapOf("apple" to 3)
stock["apple"] = 4
stock["banana"] = 1
println(stock) // {apple=4, banana=1}
}
Una piccola onesta in anticipo: leggere map[key] puo restituire null se la chiave non esiste. E un modello di sicurezza normale. Lo analizzeremo con calma in una lezione separata su Map; per ora tienilo a mente come fatto, per non stupirti.
5. Collezioni vuote e inferenza del tipo: perche il compilatore chiede un indizio
Quando crei una collezione non vuota, Kotlin di solito deduce automaticamente il tipo degli elementi. Ma se la collezione e vuota, il compilatore “non ha a cosa agganciarsi”: non vede nemmeno un elemento e non puo capire quale tipo dovrebbe esserci. Per questo le collezioni vuote spesso si creano con un’indicazione esplicita del tipo.
E un punto pratico importante: creerai collezioni vuote continuamente, soprattutto nelle applicazioni in cui i dati si accumulano man mano che il programma gira.
Collezione non vuota: il tipo si deduce da solo
Esempio in cui Kotlin capisce il tipo:
fun main() {
val a = listOf(1, 2, 3) // List<Int>
val b = setOf("kotlin", "jvm") // Set<String>
val c = mapOf("a" to 1) // Map<String, Int>
println(a) // [1, 2, 3]
println(b) // [kotlin, jvm]
println(c) // {a=1}
}
Qui e tutto semplice: gli elementi hanno suggerito il tipo.
Collezione vuota: il tipo va indicato
Ora creiamo correttamente collezioni vuote:
fun main() {
val items = mutableListOf<String>()
val categories = mutableSetOf<String>()
val counts = mutableMapOf<String, Int>()
items.add("milk")
categories.add("food")
counts["milk"] = 1
println(items) // [milk]
println(categories) // [food]
println(counts) // {milk=1}
}
Fai attenzione: in ogni riga <String> o <String, Int> non e “rumore inutile”, ma un indizio per il compilatore, senza il quale non e tenuto a indovinare.
6. Mini abbozzo di applicazione: perche ci servono le collezioni nella realta
Da oggi nel corso iniziamo a costruire un’applicazione console pratica (senza classi e OOP in questa fase). Per non avere la sensazione di “collezioni per il gusto delle collezioni”, leghiamo tutto a un compito semplice e reale: memorizzare le voci di spesa inserite dall’utente. Oggi faremo solo l’inizio: storage in memoria e un paio di azioni. Comandi, normalizzazione dell’input e logica completa li vedremo piu avanti — ora e importante vedere che le collezioni risolvono il problema della dimensione.
Per ora immaginiamo una voce di spesa come una semplice stringa (tipo "food coffee 300"). Si, non e ancora un modello dati ideale — ma a questo livello di conoscenze e un formato temporaneo onesto e funzionante.
Versione con array: scomoda e rumorosa
Ecco come apparirebbe la “raccolta delle spese” con un array (semplificato):
fun main() {
var expenses = emptyArray<String>()
println("Inserisci una spesa (o una riga vuota per uscire):")
while (true) {
val line = readln().trim()
if (line.isEmpty()) break
expenses = expenses.copyOf(expenses.size + 1)
expenses[expenses.lastIndex] = line
}
println("Voci salvate: ${expenses.size}") // per esempio: 2
}
Funziona, ma la sensazione e che non stiamo gestendo spese, bensi reimpacchettando dati.
Versione con MutableList: piu corta, piu pulita, piu vicina al significato
E ora la stessa cosa con le collezioni:
fun main() {
val expenses = mutableListOf<String>()
println("Inserisci una spesa (o una riga vuota per uscire):")
while (true) {
val line = readln().trim()
if (line.isEmpty()) break
expenses.add(line)
}
println("Voci salvate: ${expenses.size}") // per esempio: 2
}
La differenza non e solo nel numero di righe. La differenza e in cosa pensi mentre scrivi il codice. Qui nella testa hai “aggiungi una voce”, non “ricrea l’array e copia”. Ed e proprio per questo che le collezioni sono amate nel codice applicativo.
Come scegliere velocemente il contenitore: uno schema semplice per principianti
All’inizio e facile bloccarsi: “ma cosa scelgo? lista? insieme? mappa?”. Normale. Fissiamo un modello di scelta molto semplice, senza entrare nei dettagli di performance e implementazione interna.
Se ti importa l’ordine degli elementi e i duplicati sono ammessi, quasi sempre e List. Se ti serve l’unicita dei valori, prendi Set. Se vuoi memorizzare una corrispondenza “chiave → valore” e recuperare per chiave, prendi Map.
Puoi anche immaginarlo come un mini diagramma di flusso:
flowchart TD
A["Devi memorizzare piu valori"] --> B{"Serve accesso per chiave?"}
B -->|Si| M["Map⟨K, V⟩"]
B -->|No| C{"Serve unicita?"}
C -->|Si| S["Set⟨T⟩"]
C -->|No| L["List⟨T⟩"]
Questo schema non e “matematica superiore”, ma aiuta a decidere rapidamente nella maggior parte dei casi quotidiani.
7. Errori tipici al primo incontro con le collezioni
Errore n. 1: confondere read-only e immutable (“se non posso modificare tramite List, allora nessuno puo modificare affatto”).
I tipi read-only limitano le operazioni tramite uno specifico riferimento, ma non garantiscono che altrove non esista un riferimento mutable agli stessi dati. All’inizio del corso basta ricordare una regola semplice: read-only riguarda l’API e i metodi disponibili, non una magica immutabilita del mondo.
Errore n. 2: tenere una collezione mutable in var “per ogni evenienza”.
Spesso i principianti scrivono var list = mutableListOf(...), perche “se ci sono modifiche allora var”. In realta le modifiche avvengono tramite i metodi della collezione (add, remove), non tramite riassegnazione della variabile. Quindi uno stile comodo e sicuro e val list = mutableListOf(...): cosi non sostituirai accidentalmente la collezione con un’altra.
Errore n. 3: creare una collezione vuota senza tipo e sorprendersi dell’errore di compilazione.
Quando scrivi mutableListOf() senza elementi, il compilatore non e tenuto a indovinare di che cosa vuoi una MutableList. La cura e semplice: indicare il tipo esplicitamente (mutableListOf<String>(), mutableMapOf<String, Int>()) oppure definire il tipo atteso della variabile.
Errore n. 4: considerare Set come “una lista senza indici, ma per il resto uguale”.
Un insieme ha un significato diverso: l’unicita. Se inizi ad aspettarti da Set “il primo elemento”, “il secondo elemento”, “l’ordine di inserimento”, stai costruendo una logica fragile. Al livello del tema di oggi trattiamo Set come un “insieme di valori unici”, e questo e sufficiente.
Errore n. 5: provare ad “aggiornare” listOf/setOf/mapOf, come se fossero mutable.
Le factory listOf, setOf, mapOf creano collezioni read-only. Se sai in anticipo che modificherai il contenuto, scegli mutableListOf, mutableSetOf, mutableMapOf. Non e un capriccio, ma un modo per rendere chiare le intenzioni del codice senza commenti.
Errore n. 6: dimenticare che array e collezioni sono entita diverse.
A volte un principiante vede arrayOf(...), poi vede listOf(...) e pensa che sia “la stessa cosa, solo un’altra parola”. Ma gli array sono un tipo a parte, con le proprie regole (per esempio, gli array sono sempre mutabili per elemento). Quando scegli un contenitore, scegli anche uno stile di lavoro con i dati, quindi e meglio tenere subito la differenza ben chiara.
GO TO FULL VERSION