Capitolo 6

Array e liste: tanti valori in una sola variabile

Finora ogni variabile conteneva un solo valore. Con gli array e le liste puoi conservare molti valori insieme (i voti di una classe, i nomi degli studenti, le temperature della settimana) e lavorarci con cicli e metodi pronti.

In questo capitolo impari

Ripasso lampo: cicli e accumulatori

Array e liste si usano quasi sempre dentro un ciclo: ripasso del capitolo 5.

Che cosa stampa questo programma?

fun main() {
    for (i in 1..5) {
        if (i % 2 == 0) print("$i ")
    }
}
1 2 3 4 5No: la if filtra, non stampa tutti i valori.
2 4Esatto: i % 2 == 0 è vero solo per i numeri pari, e in 1..5 i pari sono 2 e 4.
1 3 5Quelli sono i dispari: sarebbe i % 2 != 0.

Una if dentro a un ciclo non cambia quante volte il ciclo si ripete: decide soltanto che cosa succede in quel giro.

Che cosa stampa questo programma?

fun main() {
    var somma = 0
    for (i in 1..4) somma = somma + i
    println(somma)
}
10Esatto: 1 + 2 + 3 + 4 = 10. È lo schema dell'accumulatore: parti da 0 e sommi a ogni giro.
4No: somma = somma + i aggiunge ogni valore, non tiene solo l'ultimo.
0La variabile somma cambia dentro al ciclo.

L'accumulatore parte da zero e cresce a ogni giro. Le due cose da controllare sono il valore iniziale e il punto in cui si somma: se lo sposti fuori dal ciclo, il conto non funziona più.

6.1 Array a dimensione fissa

Un array è una fila di caselle numerate: si crea elencando i valori fra parentesi tonde e la sua dimensione resta quella — non puoi aggiungere elementi, ma puoi leggere e sostituire quelli che ci sono. Ogni casella ha un indice che parte da 0.

  1. size è 3: l'array contiene tre elementi.
  2. voti[0] è il primo elemento, 7: gli indici partono da 0.
  3. Sostituendo voti[0] con 10 l'array diventa 10, 8, 6. La dimensione, invece, non cambia: resta 3.
  4. voti.last() è l'ultimo elemento, cioè voti[voti.size - 1]: 6.
Attenzione all'indice!

Con 4 elementi gli indici validi sono 0, 1, 2, 3. voti[4] fa apparire l'errore "Indice 4 fuori dai limiti". L'ultimo indice è sempre size - 1.

Il tipo dell'array segue i valori che ci metti

Il nome della funzione dice che cosa c'è nelle caselle. Indici, size e for funzionano allo stesso modo. Per ora usa questi tre.

Un array ha 3 elementi. Che cosa succede con voti[3]?

fun main() {
    val voti = intArrayOf(7, 8, 6)
    println(voti[3])
}
Stampa il terzo votoAttenzione: il "terzo" elemento è voti[2], perché si conta da 0.
Il programma si ferma con un errore di indice fuori dai limitiEsatto: con 3 elementi gli indici validi sono 0, 1 e 2. L'ultimo è sempre size - 1.
Stampa nullNo: gli errori di indice non danno null, fermano il programma.

Con 3 elementi gli indici validi sono 0, 1 e 2. L'ultimo è sempre size - 1. Per scorrere tutte le caselle: for (i in 0 until voti.size). until esclude size, quindi non chiedi una casella che non esiste.

Gli array possono viaggiare in parallelo: la posizione 0 di ognuno riguarda lo stesso prodotto, la posizione 1 un altro, e così via. In un unico ciclo si riempiono tutti e tre; dopo, basta trovare la posizione di un nome per leggere subito sconto e prezzo con lo stesso indice.

Extra: array generati da una regola (si può saltare alla prima lettura)

Quando i valori seguono una regola — i quadrati, i doppi, le potenze — si può dire a Kotlin di calcolarli invece di scriverli: IntArray(6) { it * it } crea sei caselle con i quadrati da 0 a 5. Che cosa significhi quel pezzo fra graffe è spiegato nel §6.3, quando servirà davvero: per ora basta sapere che esiste.

6.2 Le liste: listOf e mutableListOf

Una lista è una collezione come l'array, ma con due differenze importanti: si crea con listOf(...) o mutableListOf(...), e la seconda può crescere e accorciarsi — add e remove funzionano solo su di lei.

listOf è il modo più diretto di creare una lista: gli elementi si scrivono fra le parentesi, uno dopo l'altro — listOf("pane", "latte", "mele") — e da quel momento la lista resta com'è: niente add, niente remove. È l'analogo di intArrayOf e arrayOf per gli array. Con mutableListOf si può fare la stessa cosa — mutableListOf("pane", "latte") — oppure partire da una lista vuota, mutableListOf<String>(), e riempirla un elemento alla volta. Cominciamo dalla lista che cambia.

  1. Si parte da due elementi, add ne aggiunge uno (pane, latte, uova).
  2. remove("latte") toglie l'elemento con quel valore: restano pane e uova.
  3. println(spesa) stampa la lista fra parentesi quadre con le virgole: [pane, uova]. size ora è 2.

Negli esempi finora gli elementi erano scritti nel codice. Di solito però arrivano da chi usa il programma: la lista nasce vuota e cresce a ogni lettura.

Le liste modificabili possono anche viaggiare in parallelo: nello stesso giro del ciclo si aggiunge un elemento a ciascuna, così la posizione 0 di tutte riguarda lo stesso elemento — un libro e il suo prezzo, uno studente e il suo voto.

listOf o mutableListOf?

listOf(...) crea una lista non modificabile: gli elementi sono quelli scritti fra le parentesi, decisi una volta per tutte quando scrivi il programma. Con una lista del genere si può fare una cosa sola: leggere. E si legge in tutti i modi che già conosci — con gli indici (giorni[0]), con size, con un ciclo for, e con i metodi della tabella del §6.3 come sum(), filter e sorted: è quello che fa l'Esempio 8, che non modifica mai giorni né voti.

Quello che non si può fare è cambiarla: né aggiungere o togliere elementi (add, remove, clear), né riordinarla (sort), né sostituire un elemento con un'assegnazione come giorni[0] = "dom". Se ci provi, il programma si ferma con un errore come questo: "Questa lista è immutabile (creata con listOf). Per modificarla usa mutableListOf(...)". Il rimedio è nella frase stessa: se la lista deve cambiare, va dichiarata mutableListOf fin dall'inizio.

mutableListOf(...) è invece la lista che cresce e si accorcia: serve quando il numero di elementi lo decidono i dati o chi usa il programma — i voti letti finché l'utente non scrive -1, le voci di una spesa che si aggiungono una alla volta.

Quindi la regola è: se la lista non deve cambiare, usa listOf — dice chiaramente che quei valori sono fissi, e chi legge il programma lo sa subito. Se poi ti serve poter aggiungere qualcosa, cambi listOf in mutableListOf e il resto del programma resta uguale.

Questo programma si ferma con un errore. Quale correzione lo fa funzionare?

fun main() {
    val l = listOf(1, 2, 3)
    l.add(4)
    println(l)
}
Togliere la riga l.add(4)Funziona, ma perdi l'elemento che volevi aggiungere: non è la correzione giusta se ti serve una lista che cresce.
Usare mutableListOf(1, 2, 3)Esatto: listOf crea una lista immutabile, mutableListOf una che si può modificare con add e remove.
Usare intArrayOf(1, 2, 3)Anche l'array è a dimensione fissa: add non esiste nemmeno.

Regola pratica: se l'elenco deve crescere o accorciarsi, dichiara mutableListOf; se i valori sono sempre quelli, listOf va benissimo.

6.3 I metodi che userai di più

Qui trovi i metodi già usati negli esempi. Impara prima size, gli indici, add e remove. Gli altri li consulti quando ti servono.

Attenzione a due famiglie: add, clear e sort cambiano la lista e non danno un valore da salvare; sum, sorted e filter danno un risultato: lo stampi o lo metti in una variabile. remove risponde true o false secondo che abbia trovato l'elemento. Le righe che aggiungono, tolgono o riordinano sul posto valgono solo per mutableListOf.

MetodoCosa fa
lista.sizequanti elementi contiene
lista[i], lista[i] = xlegge / cambia l'elemento in posizione i (cambiarlo: solo liste modificabili)
lista.add(x), lista.add(i, x)aggiunge in fondo / in posizione i (solo liste modificabili)
lista.remove(x), lista.clear()toglie un valore / svuota la lista
lista.contains(x), lista.indexOf(x)c'è? in che posizione?
lista.isEmpty()è vuota? Utile prima di max() e average()
sum(), average(), max(), min()somma, media, massimo, minimo
sorted(), sortedDescending()una nuova lista ordinata / al contrario: la lista di partenza non cambia
sort()ordina la lista stessa (solo liste modificabili): non restituisce niente, perché il risultato è la lista stessa, già ordinata — non serve riassegnarla. Quindi a.sort() ordina a, ma b = a.sort() non mette nessuna lista in b: per una copia ordinata, lasciando intatta l'originale, si usa b = a.sorted()
filter { }, map { }, count { }tiene solo alcuni / li trasforma / li conta
joinToString(", "), contentToString()trasforma la collezione in un testo da stampare
Questi li incontrerai più avanti: non serve ricordarli adesso

Esistono e li ritroverai leggendo codice altrui, ma in questo capitolo non li hai ancora usati: consulta la tabella quando ti serve uno di questi, senza impararla a memoria ora.

MetodoCosa fa
lista.get(i), lista.set(i, x), lista.removeAt(i)la versione "a metodo" di lista[i] e lista[i] = x / toglie l'elemento in posizione i
first(), last()primo e ultimo elemento
sortBy { ... }, forEach { }ordina secondo un criterio / ripete qualcosa per ogni elemento
distinct(), reversed()una nuova lista senza duplicati / con l'ordine inverso
any { }, all { }almeno uno / tutti
take(n), drop(n), subList(a, b)i primi n / tutti tranne i primi n / una parte della lista
maxOrNull(), minOrNull()massimo e minimo che danno null su una lista vuota (vedi il riquadro qui sotto)
toMutableList()una copia modificabile di una lista letta: serve per aggiungere a un risultato di filter o sorted
Attenzione: su una collezione vuota max(), min() e average() si fermano

Hanno bisogno di almeno un elemento: se la lista è vuota fermano il programma con un errore. Quando può capitare (per esempio i voti letti finché l'utente non scrive -1) usa maxOrNull() e minOrNull(), che danno null, oppure controlla prima lista.isEmpty().

Le liste che escono dai metodi sono non modificabili

filter, map, sorted, distinct e reversed non toccano la lista di partenza: restituiscono una lista nuova, dello stesso tipo di quelle create con listOf, quindi non modificabile. È il comportamento giusto — voti resta com'era e il risultato è un elenco a sé — ma è anche il motivo per cui voti.filter { ... }.add(10) si ferma con l'errore "questa lista è immutabile". Se ti serve poter aggiungere a quel risultato, chiedine una copia modificabile con toMutableList(), oppure costruisci una mutableListOf e riempila con un ciclo.

Quel pezzo tra graffe

In filter { it >= 6 }, it è l'elemento di adesso. Kotlin lo passa uno alla volta. Tieni il voto se è ≥ 6. Puoi anche scriverlo filter { voto -> voto >= 6 }: stesso lavoro, con un nome scelto da te. Per ora basta questo. Non è un argomento nuovo: è il modo corto di dire «per ogni elemento, questa condizione».

Lo schema "primo elemento come riferimento"

Per trovare il massimo (o il minimo) si parte dal primo elemento, non da zero: così funziona anche con numeri tutti negativi. Poi si confronta ogni elemento e si aggiorna il riferimento. Con max() e min() Kotlin fa lo stesso lavoro in una riga, ma sapere scriverlo a mano serve per capire come funziona e per i casi più complessi (per esempio il massimo fra voti con il nome dello studente).

6.4 Array, listOf o mutableListOf?

AspettointArrayOf, arrayOf, IntArraylistOfmutableListOf
Dimensionefissa: decisa alla creazionefissa: decisa alla creazionevariabile: cresce e diminuisce
Aggiungere e togliereno (errore)no (errore)si: add, remove, clear
Cambiare un elementosi: a[0] = 5no (errore)si: l[0] = 5
Metodi delle collezioni (sum, filter, sorted…)sisisi
Quando usarlaquantità nota e tipi semplici (Int, Double…)valori decisi quando scrivi il programmaquantità che dipende dai dati o dall'utente
Consiglio pratico

Se non sai in anticipo quanti elementi avrai (per esempio i voti letti finché l'utente non scrive -1), usa mutableListOf: il programma resta semplice. Se invece i valori sono quelli che hai scritto nel codice (i giorni della settimana, le materie, le tariffe), listOf è la scelta più chiara: dice subito che quell'elenco non cambia. L'array va bene quando la quantità è certa e i valori sono numeri semplici, per esempio le temperature dei 7 giorni.

6.5 Riepilogo

ScriviCosa fa
val voti = intArrayOf(7, 8, 9)array di interi a dimensione fissa, con i valori già dentro
val zeri = IntArray(4), DoubleArray(3), Array(3) { "" }array vuoto da riempire dopo: zeri, 0.0, stringhe vuote
IntArray(6) { it * it }array costruito da una lambda: it è la posizione
voti[1], voti[1] = 10legge e cambia l'elemento in posizione i (si parte da 0)
voti.size, voti.indicesquanti elementi ha / le posizioni da 0 a size - 1
voti.contentToString()l'array come testo da stampare: [7, 8, 9]
val spesa = mutableListOf<String>()lista modificabile (anche vuota): cresce e si accorcia
spesa.add("pane"), spesa.remove("pane")aggiunge in fondo / toglie la prima voce uguale
listOf(1, 2, 3)lista non modificabile, con i valori decisi nel programma: add e lista[0] = x darebbero errore
lista.toMutableList()una copia modificabile di una lista letta (per esempio del risultato di filter)
spesa.size, spesa.contains("pane")quanti elementi / c'è quel valore?
voti.sum(), max(), min(), average()somma, massimo, minimo, media degli elementi
voti.filter { it >= 6 }, map { it * 2 }tiene solo alcuni elementi / li trasforma
voti.joinToString(", ")mette gli elementi in un testo, separati da virgole
for (x in voti), for (i in voti.indices)scorre i valori / scorre le posizioni

6.6 Esercizi guidati

Leggi 5 voti in un array, poi calcola e stampa: tutti i voti, la media, il voto massimo e il voto minimo, quanti voti sono sufficienti (da 6 in su) e l'elenco dei soli insufficienti. Con i voti 7, 5, 9, 6, 8 le righe sono: Voti letti: 7, 5, 9, 6, 8, Media: 7.0, Massimo: 9, Minimo: 5, Sufficienti: 4 e Insufficienti: [5].

L'array è già riservato, ma è vuoto: riempilo con un ciclo for (i in 0 until 5) e readln().toInt(), come hai imparato nel capitolo 5. Per i voti insufficienti puoi creare una lista vuota e riempirla nel ciclo, oppure usare voti.filter { it < 6 }. Per la media usa voti.average().

  1. IntArray(5) crea un array di cinque zeri: le posizioni esistono già, basta riempirle.
  2. for (i in 0 until 5) visita gli indici 0, 1, 2, 3, 4: esattamente le posizioni valide dell'array.
  3. average() restituisce un Double anche partendo da interi: la media mantiene i decimali.
  4. filter { it < 6 } crea una nuova lista con i soli voti insufficienti; it è ogni voto, preso uno alla volta.

Parti da una lista vuota. Aggiungi, in questo ordine, i testi pane, latte, mele, pasta. Poi: inserisci burro in seconda posizione, togli latte, ordina in ordine alfabetico, stampa con joinToString e mostra quanti prodotti restano. Le due righe finali sono Lista: burro, mele, pane, pasta e Prodotti da comprare: 4. Non serve leggere da tastiera: i prodotti sono questi.

Per ordinare la lista stessa usa spesa.sort() (funziona sui testi: ordine alfabetico). Per inserire in seconda posizione: spesa.add(1, "burro").

  1. mutableListOf<String>() crea una lista vuota di testi: si possono aggiungere elementi quando serve.
  2. add("pane") mette in fondo; add(1, "burro") inserisce nella posizione 1 e sposta tutto il resto.
  3. remove("latte") toglie dall'elenco, non dal frigorifero: la lista resta con 4 elementi.
  4. sort() ordina la lista stessa (non restituisce una nuova lista): per questo non c'è bisogno di riassegnare nulla.

Leggi 7 voti in una lista modificabile, calcola la media e poi conta quanti voti sono sopra la media e quanti sotto. Stampa i voti ordinati, la media e i due conteggi. Con i voti 6, 4, 8, 7, 5, 9, 6: tre sopra e quattro sotto. Le quattro righe sono Voti ordinati: [4, 5, 6, 6, 7, 8, 9], la media (con i decimali che calcola Kotlin), Sopra la media: 3 e Sotto la media: 4.

Dopo aver calcolato la media, usa un ciclo (o count { it > media }) per contare i voti sopra e sotto. Attenzione: i voti esattamente uguali alla media non sono né sopra né sotto.

  1. La lista parte vuota e cresce con add: è il caso tipico in cui serve mutableListOf.
  2. La media si calcola prima del conteggio, perché serve come termine di confronto.
  3. I due if sono separati: un voto uguale alla media non entra in nessuno dei due contatori.
  4. voti.sorted() restituisce una copia ordinata: la lista originale conserva l'ordine di inserimento.

Chiedi all'utente quanti brani vuole inserire, leggi i titoli uno alla volta in una mutableListOf<String> che parte vuota, poi stampa la playlist completa e il numero di brani. Alla fine chiedi un titolo da cercare e stampa se c'è, e in che posizione (contando da 1). Con 3 brani e ricerca Albachiara le stampe dopo la lettura sono: Playlist: Albachiara, Sarà perché ti amo, Ma il cielo è sempre blu, Brani in playlist: 3 e "Albachiara" è nella playlist, in posizione 1.

La lista nasce vuota con mutableListOf<String>() e cresce con playlist.add(readln()) a ogni giro del ciclo. Per la ricerca servono contains e indexOf: attenzione, indexOf conta da 0.

  1. mutableListOf<String>() crea la lista vuota: il numero di brani lo decide l'utente mentre il programma gira, quindi non può essere scritto nel codice.
  2. add(readln()) legge una riga e la mette in fondo alla lista: dopo il ciclo la playlist contiene i titoli nell'ordine di inserimento.
  3. contains risponde sì/no, indexOf dà la posizione: è a base 0, quindi per parlarne con una persona si aggiunge 1.
  4. Se il titolo mancasse, indexOf darebbe -1: per questo la stampa della posizione sta dentro l'if del contains.

Due liste modificabili che viaggiano in parallelo: chiedi quanti libri registrare e, per ognuno, leggi il titolo in una mutableListOf<String> e il prezzo in una mutableListOf<Double>. Poi stampa il catalogo riga per riga (titolo: prezzo), il prezzo medio e il titolo del libro più costoso. Dopo le domande stampa il titolo === CATALOGO ===, una riga per libro (I promessi sposi: 12.5 euro), poi Prezzo medio: 11.2 euro e Il libro più costoso: I promessi sposi.

Nello stesso giro del ciclo fai due add: così la posizione 0 di entrambe le liste riguarda lo stesso libro. Per il catalogo scorri titoli.indices e leggi titoli[i] e prezzi[i]. Per il massimo tieni da parte la posizione, non solo il prezzo: ti serve per ritrovare il titolo.

  1. Le due liste partono vuote e crescono insieme: dopo il ciclo, titoli[i] e prezzi[i] parlano dello stesso libro. È lo schema degli elementi collegati.
  2. Il catalogo si stampa con titoli.indices: lo stesso indice legge da entrambe le liste.
  3. average() funziona direttamente su prezzi: i valori sono tutti nella seconda lista, non serve ricalcolare nulla.
  4. La ricerca del massimo parte da posizioneMax = 0 e tiene l'indice, come nell'Esempio 7c: il prezzo massimo serve solo da confronto, ma il titolo va recuperato con lo stesso indice.

Questo programma parte senza errori, ma con i voti 6, 4, 8, 7, 5, 9, 6 dice che i sufficienti sono 3 invece di 5: il 6 non viene contato. Trova l'errore logico nella condizione del filter e correggilo.

La sufficienza parte da 6 compreso: il confronto giusto è it >= 6, non it > 6. Un solo carattere cambia il conteggio.

  1. Il codice compila: l'errore è logico. Con it > 6 il voto 6 viene scartato dal filtro.
  2. Con it >= 6 il 6 è incluso: da 3 sufficienti si passa a 5, che è il risultato corretto.
  3. Gli errori di un carattere (> contro >=) non danno messaggi: si trovano solo confrontando l'output atteso con quello reale.
  4. La stessa distinzione vale per count, any e all: controlla sempre se l'estremo è compreso.

Scrivi da zero un programma che registra i voti di una verifica. Il programma legge, in quest'ordine:

  1. una prima riga con il numero di voti da inserire (per esempio 4);
  2. poi una riga per ogni voto. Se una riga non è un numero intero, considerala 0 (usa toIntOrNull() ?: 0).

Metti i voti in una lista (il numero non è noto in anticipo). Se il numero iniziale è 0 (o la prima riga non è un numero) non leggere nessun voto e stampa solo Nessun voto inserito. In quel caso non chiamare max() né average(): su una lista vuota fermano il programma.

Altrimenti stampa, in quest'ordine e con questi testi:

Voti: 7, 0, 9, 6
Quantità: 4
Somma: 22
Media: 5.5
Massimo: 9
Sufficienti: 3

Con l'input di prova 4, 7, assente, 9, 6 i voti sono quattro e il terzo vale 0.

Riempi la lista con un ciclo for (i in 1..quanti) e converti ogni riga con toIntOrNull() ?: 0. Le statistiche si fanno con sum(), average(), max() e count { it >= 6 }, ma solo dentro un if (voti.isEmpty()) ... else ...: così il caso della lista vuota è coperto prima di chiedere il massimo o la media.

  1. La lista è la scelta giusta perché il numero di voti non è noto in anticipo: con un array dovresti decidere la dimensione prima di leggere.
  2. 1..quanti con quanti uguale a 0 non fa nessun giro: il ciclo si occupa da solo del caso limite.
  3. toIntOrNull() ?: 0 trasforma una riga non numerica in 0, così il programma non si ferma su un dato sporco.
  4. Il controllo voti.isEmpty() è obbligatorio prima di max() e average(): su una lista vuota non sanno che cosa restituire.
  5. count { it >= 6 } conta i sufficienti senza costruire una seconda lista: il 6 è compreso.

Il programma deve stampare i voti e la loro media, ma si ferma con un errore e poi calcola male. Con i voti 6, 7, 8 la media è 7.0.

Gli indici partono da 0 e l'ultimo è size - 1: per scorrere tutto l'array si usa 0 until voti.size. E la divisione fra due Int taglia i decimali.

  1. for (i in 1..voti.size) produce gli indici 1, 2 e 3: voti[3] non esiste e il programma si ferma con "Indice 3 fuori dai limiti".
  2. La forma corretta è 0 until voti.size, che dà 0, 1, 2.
  3. Nel testo stamperei l'indice: ${i + 1} serve a mostrare "Voto 1" per l'elemento di indice 0.
  4. 6 + 7 + 8 = 21, e 21 / 3 fra interi fa 7: per ottenere 7.0 serve almeno un Double, per esempio voti.sum().toDouble().
  • so creare un array con intArrayOf, doubleArrayOf, arrayOf e IntArray(n) { }
  • so usare gli indici sapendo che partono da 0 e che l'ultimo è size - 1
  • so scegliere fra array, listOf e mutableListOf e so spiegare la differenza
  • so aggiungere e togliere elementi con add e remove
  • so usare sum, max, sorted, filter, map, joinToString
  • so scrivere a mano la ricerca del massimo partendo dal primo elemento
  • so leggere un messaggio di errore e correggere il programma da solo