Capitolo 7

Array associativi: le mappe chiave-valore

In un array l'elemento si trova con un numero (l'indice). In una mappa si trova con una chiave: un nome, una parola, un codice. Servono per le rubriche, i dizionari, i conteggi e tutte le volte che "il valore giusto" dipende da un'etichetta.

In questo capitolo impari

Ripasso lampo: array e liste

Le mappe sono collezioni come le liste: ripasso del capitolo 6. E prima ancora, un salto indietro ai cicli e agli accumulatori del capitolo 5.

Che cosa stampa questo programma?

fun main() {
    val a = arrayOf(10, 20, 30)
    for (i in a.indices) print("${a[i]} ")
    println()
    println(a.size - 1)
}
10 20 30 e poi 3Attenzione: a.size - 1 è 2, cioè l'indice dell'ultimo elemento.
10 20 30 e poi 2Esatto: indices va da 0 a size - 1, e l'ultimo indice è appunto 2.
Un errore, perché a.indices non esisteEsiste: è la proprietà che ti dà tutti gli indici validi di un array o di una lista.

indices dà tutti gli indici validi, da 0 a size - 1. size è quanti elementi ci sono: non è l'ultimo indice, è uno in più.

Che cosa stampa questo programma?

fun main() {
    val numeri = listOf(4, 7, 2, 9)
    var somma = 0
    var quanti = 0
    for (n in numeri) {
        somma = somma + n
        quanti = quanti + 1
    }
    println("$somma $quanti")
}
22 4Esatto: 4 + 7 + 2 + 9 = 22 e il ciclo fa quattro giri, quindi quanti arriva a 4.
22 3Attenzione: il contatore parte da 0 e cresce a ogni giro, anche all'ultimo. I giri sono quattro.
21 4No: somma parte da 0, quindi il totale è 22. Se partisse da -1 sarebbe 21: è il valore iniziale dell'accumulatore a decidere il risultato.

Due variabili, due ruoli: somma è l'accumulatore (somma = somma + n a ogni giro), quanti è il contatore (quanti = quanti + 1 a ogni giro). Entrambe partono da 0 e crescono dentro al ciclo: cambiare il valore iniziale cambia il risultato, non solo l'ultimo passaggio.

7.1 Che cos'è una mappa

Una mappa collega una chiave a un valore. Si legge come una tabella a due colonne: a sinistra le chiavi (uniche), a destra i valori. In altri libri e linguaggi la stessa struttura si chiama array associativo o dizionario: sono nomi diversi per lo stesso strumento — qui useremo sempre i nomi di Kotlin, Map e mutableMapOf.

Chiave (String)Valore (Int)
"Anna"30
"Luca"25
La parola "to"

"Anna" to "333-1112233" crea una coppia chiave-valore. Nella creazione della mappa le coppie si separano con la virgola.

7.2 Leggere e scrivere

Il valore si legge con le parentesi quadre, come per gli array, ma dentro ci va la chiave. Per aggiungere o aggiornare una voce si usa la stessa scrittura seguita da =.

  1. eta["Anna"] trova la chiave e dà 30.
  2. eta["Sara"] non trova niente e dà null: una chiave assente non è un errore, è un valore mancante.
  3. La riga eta["Sara"] = 19 aggiunge una voce nuova: la mappa passa da 2 a 3 elementi.
  4. eta.keys stampa l'insieme delle chiavi: [Anna, Luca, Sara].

Come per le liste, una mappa può nascere vuota e riempirsi mentre il programma gira, con i dati di chi lo usa: si dichiara mutableMapOf<String, Int>() e a ogni lettura si aggiunge una voce con mappa[nome] = valore. Dopo, cercare dentro la mappa è il suo lavoro migliore: la chiave trova il valore in un colpo solo, senza scorrere le coppie una per una.

Cercare in una mappa: due modi

eta.containsKey(cercato) risponde true o false: è la strada giusta quando le due risposte sono due messaggi diversi, come nell'esempio. Se invece ti serve solo il valore, puoi leggere direttamente eta[cercato] ?: 0: se la chiave manca, al posto di null usi il valore di riserva. E se registri due persone con lo stesso nome? La seconda età sostituisce la prima: in una mappa le chiavi sono tutte diverse, una voce per ciascuna.

La mappa prezzi non contiene la chiave "te". Che cosa fa prezzi["te"]?

Restituisce nullEsatto: una chiave assente non è un errore, è un valore mancante da gestire con ?: o con containsKey.
Fa fermare il programma con un erroreNo: l'errore arriva solo se usi quel null come se ci fosse un valore, per esempio chiamandoci un metodo sopra.
Restituisce 0Kotlin non inventa valori: ti dà null, e il valore di riserva lo scegli tu con ?:.

Il modo più comodo: prezzi["te"] ?: 0, che si legge "il prezzo del tè, oppure zero se non c'è".

La chiave assente non dà errore: dà null

prezzi["caffe"] restituisce null, non un errore. Per calcolare qualcosa su quel valore usa ?: (valore di riserva) o controlla prima con containsKey. Se invece chiami un metodo su un valore null, il programma si ferma.

7.3 Scorrere una mappa

Il ciclo più usato è for ((chiave, valore) in mappa): le due variabili prendono ogni volta la coppia chiave-valore. In alternativa ci sono keys, values e entries.

filterValues non è obbligatorio

Se il metodo non ti serve, fai lo stesso lavoro con un ciclo e un if: for ((p, q) in magazzino) if (q < 20) println(p). Il manuale ti mostra i metodi extra, ma i cicli bastano sempre.

7.4 Lo schema del conteggio

La mappa è perfetta per contare le occorrenze: la chiave è quello che stai contando, il valore è il contatore. La scrittura (mappa[c] ?: 0) + 1 si legge "prendi il valore attuale; se non c'è ancora, parti da zero; poi aggiungi uno".

split: un solo strumento nuovo, per contare le parole

Finora hai contato le lettere di una parola: for (c in parola). Per contare le parole di una frase ti serve split(" "): taglia il testo a ogni spazio. "ciao mondo ciao".split(" ") dà tre pezzi: ciao, mondo, ciao. Il capitolo 8 lo riprende per intero. Qui usalo solo per questo schema.

  1. split(" ") spezza la frase in tre parole: ciao, mondo, ciao.
  2. La prima volta che vede ciao la mappa non ha quella chiave: conteggio["ciao"] ?: 0 dà 0, poi il conteggio diventa 1.
  3. La seconda volta la chiave esiste già e vale 1, quindi diventa 2: conteggio["ciao"] è 2.
  4. Nella mappa ci sono due chiavi distinte, ciao e mondo: size è 2.
Quale forma scegliere

La forma che ti basta: mappa[k] = (mappa[k] ?: 0) + 1. getOrPut(k) { 0 } fa lo stesso in due passi. L'esempio 9 (lista di voti per studente) è un extra: puoi saltarlo alla prima lettura.

L'ordine delle voci

Le mappe di questo manuale, quando scorri con for ((k, v) in mappa), presentano le voci nell'ordine in cui le hai aggiunte: per questo il conteggio delle lettere sopra parte da p, poi r, e così via. La ricerca del massimo con lo schema "parti dal primo come riferimento" ne trae vantaggio: se due chiavi hanno lo stesso valore, vince la prima incontrata. La scelta di questo ordine non è scontata per una mappa: in generale una mappa garantisce di trovare il valore di una chiave, non in che ordine presentarle — quando ti serve un ordine preciso, ordina tu le chiavi con keys.sorted().

7.5 Cercare una parola nel dizionario

Sai già leggere una voce con mappa["chiave"]. Ora unisci mappa e if: se la parola c'è, traduci; se manca, dillo.

trim e lowercase: un assaggio dal capitolo 8

trim() toglie gli spazi ai bordi del testo e lowercase() trasforma le maiuscole in minuscole: così la parola "CASA" e "casa" diventano la stessa richiesta. Li vedrai spiegati per bene nel capitolo 8: qui sono solo uno strumento per rendere la ricerca nel dizionario più comoda.

Questo programma dà errore. Perché?

fun main() {
    val m = mapOf("a" to 1)
    m["b"] = 2
}
Manca una voce "b" già scritta nella mappaNo: il problema non è la chiave, è che questa mappa non si può cambiare.
Con mapOf non si possono aggiungere voci: serve mutableMapOfEsatto: mapOf è fissa. Per scrivere m["b"] = 2 la mappa deve essere mutableMapOf.
Le chiavi devono essere numeri, non lettereNo: le chiavi possono essere testo. Qui "a" va benissimo.

Come per le liste: mapOf se non cambia, mutableMapOf se devi aggiungere o aggiornare voci.

7.6 Riepilogo

ScriviSignificato
val m = mutableMapOf("a" to 1, "b" to 2)mappa modificabile con due voci
val m = mapOf("a" to 1)mappa non modificabile
mutableMapOf<String, Int>()mappa vuota: chiavi String, valori Int
m["a"] / m.get("a")legge il valore (o null)
m["a"] = 5 / m.put("a", 5)aggiunge o aggiorna
m.containsKey("a"), m.remove("a")controlla se c'è, toglie la voce
m.size, m.keys, m.valuesquante voci, tutte le chiavi, tutti i valori
for ((k, v) in m)scorre chiavi e valori
m.getOrDefault(k, 0), m[k] ?: 0valore di riserva se la chiave non c'è
Errori tipici con le mappe

7.7 Esercizi guidati

Leggi tre coppie nome / numero (6 righe). Poi leggi un settimo nome da cercare. Stampa in questo ordine: 1. il numero di quel nome, oppure la riga Contatto non trovato; 2. poi una riga vuota, il titolo --- Rubrica completa (3 contatti) --- e tutti i contatti, uno per riga, nel formato nome → numero. Con ricerca Anna la prima riga è Anna: 333-1112233.

Per la ricerca usa containsKey (oppure rubrica[ricerca] ?: "Contatto non trovato"). Per stampare tutti i contatti usa for ((nome, numero) in rubrica).

  1. Il ciclo legge due righe per volta con readln(): prima il nome (chiave), poi il numero (valore).
  2. rubrica[nome] = numero funziona sia per aggiungere un contatto nuovo sia per aggiornare uno esistente.
  3. containsKey evita di stampare null quando il contatto non esiste.
  4. Il ciclo stampa le voci nell'ordine di inserimento: le mappe del manuale conservano quest'ordine.

Leggi una parola e conta quante volte compare ogni lettera usando una mappa Char → Int. Alla fine stampa: la parola, la mappa completa, il numero di lettere diverse e la lettera che compare più spesso. In parità tieni la prima lettera che raggiunge il massimo. Con programmazione l'ultima riga è La lettera più frequente è 'r' (2 volte) e le lettere diverse sono 10.

Per trovare la lettera più frequente puoi scorrere la mappa con un ciclo e confrontare i valori, partendo dalla prima voce come riferimento.

  1. La mappa usa Char come chiave e Int come valore: ogni lettera nuova crea automaticamente una voce.
  2. (conteggio[c] ?: 0) + 1 gestisce insieme il primo incontro della lettera (valore assente → 0) e gli incontri successivi.
  3. Il secondo ciclo cerca il valore massimo scorrendo la mappa: lo schema "parti dal primo come riferimento" funziona anche qui, con le due variabili massimo e letteraPiuFrequente.
  4. Se due lettere comparissero lo stesso numero di volte, vince la prima incontrata nel ciclo: è un comportamento accettabile per questo esercizio.

Leggi quattro coppie prodotto/quantità. Le quantità dello stesso prodotto si sommano (per le matite arriveranno due volte). Poi: stampa l'inventario, il totale dei pezzi, i prodotti con meno di 20 pezzi (subito sotto, la riga -> sotto scorta!) e il prodotto più abbondante. Con i dati di prova le matite diventano 50, il totale 73 e il più abbondante è matite.

Per sommare: inventario[prodotto] = (inventario[prodotto] ?: 0) + quantita. Per i prodotti sotto scorta scorri la mappa con un ciclo e un if.

  1. La somma (inventario[prodotto] ?: 0) + quantita funziona sia alla prima comparsa del prodotto sia alle successive: le matite finiscono con 50 pezzi.
  2. Un solo ciclo sulla mappa calcola tre cose insieme: la stampa, il totale e il massimo. Non serve ripetere lo scorrimento.
  3. massimo parte da -1 (e non da 0) per funzionare anche con quantità tutte negative o nulle.
  4. Stampare "sotto scorta" dentro al ciclo evita di raccogliere prima l'elenco e stamparlo dopo.

Il programma deve contare quante volte compare ogni parola della frase ciao mondo ciao, ma non funziona. Alla fine deve stampare ciao: 2 e mondo: 1.

Una mappa creata con mapOf non si modifica: serve mutableMapOf. E la prima volta che incontri una parola la chiave non esiste ancora: il suo valore va preso con ?:. split(" ") è l'assaggio dal capitolo 8 che hai visto nella sezione 7.4: divide la frase nelle singole parole.

  1. mapOf crea una mappa immutabile: l'assegnazione conteggio[parola] = ... è l'errore che ferma il programma.
  2. Anche con mutableMapOf resta un problema: la prima volta che compare una parola, conteggio[parola] è null.
  3. (conteggio[parola] ?: 0) + 1 si legge "il valore attuale, oppure 0 se la chiave non c'è ancora, più uno".
  4. Le chiavi sono uniche: la seconda volta che vede ciao la mappa non aggiunge una voce, aggiorna quella che c'è già.
  • so creare una mappa con mutableMapOf e mapOf
  • so leggere una voce con mappa["chiave"] e so che cosa succede se manca
  • so aggiungere e aggiornare voci e so togliere con remove
  • so scorrere una mappa con for ((chiave, valore) in mappa)
  • so usare lo schema del conteggio (m[k] ?: 0) + 1
  • so usare keys, values e containsKey
  • so leggere un messaggio di errore e correggere il programma da solo