Capitolo 8

Le stringhe e i loro metodi

Le stringhe sono il tipo di dato che userai più spesso: nomi, frasi, codici, righe di un file. Kotlin gli mette a disposizione decine di metodi pronti: questa è la cassetta degli attrezzi per pulire, tagliare, cercare e trasformare il testo.

In questo capitolo impari

Ripasso lampo: mappe e conteggi

Le stringhe si spezzano in liste e si contano con le mappe: ripasso del capitolo 7.

Che cosa stampa questo programma?

fun main() {
    val prezzi = mutableMapOf("caffe" to 1)
    println(prezzi["te"])
    println(prezzi["te"] ?: 0)
}
null e poi 0Esatto: la chiave assente dà null, e ?: sostituisce il valore di riserva.
Un erroreNo: la chiave assente non è un errore, è un valore mancante.
0 e poi 0La prima riga non ha nessun ?:: stampa proprio null.

Una chiave assente non è un errore: la mappa restituisce null, e il valore di riserva lo decide l'operatore ?:. Il programma si ferma solo se usi quel null come se ci fosse un valore.

Che cosa stampa questo programma?

fun main() {
    val m = mutableMapOf("a" to 40, "b" to 15)
    for ((k, v) in m) println("$k=$v")
}
a=40 e b=15Esatto: nella coppia (k, v) il primo è la chiave e il secondo il valore.
40=a e 15=bAttenzione all'ordine: prima la chiave, poi il valore.
Un errore: non si possono usare due variabili nel forSi può: è la destrutturazione della coppia chiave-valore.

Nel ciclo su una mappa la coppia si spacchetta nell'ordine in cui è stata creata: prima la chiave, poi il valore. Scriverlo al contrario non dà errore, ma stampa le cose invertite.

8.1 Una stringa è una sequenza di caratteri

length dice quanti caratteri contiene la stringa, e ogni carattere ha un indice che parte da 0: s[0] è il primo, s[s.length - 1] l'ultimo. Un singolo carattere è di tipo Char.

Metodi utili sui singoli caratteri

c.isDigit(), c.isLetter(), c.isLetterOrDigit(), c.isWhitespace(), c.isUpperCase(), c.isLowerCase(), c.uppercaseChar(), c.lowercaseChar(), c.digitToInt(). Si usano dentro ai cicli sul testo.

8.2 Pulire e sistemare il testo

Spesso il testo arriva "sporco": spazi davanti e dietro, maiuscole messe a caso. trim() toglie gli spazi esterni, uppercase() e lowercase() cambiano il caso delle lettere.

  1. s.length conta tutti i caratteri, spazi compresi: 14.
  2. trim() toglie gli spazi davanti e dietro, quindi s.trim().length è 10.
  3. uppercase() lavora sulla stringa ripulita e restituisce CIAO MONDO. La variabile s resta però identica: i metodi delle stringhe non modificano l'originale.
Base ed extra in questo capitolo

Per non appesantire lo studio, considera base (da sapere subito): length, s[i], trim, uppercase/lowercase, contains, startsWith, substring, split, toIntOrNull. Considera extra (da consultare quando serve, vedi tabella del riepilogo): padStart, padEnd, "%.2f".format(...), repeat. L'esempio 5 e l'esempio 12 usano l'extra formattazione: se li trovi difficili, torna al riepilogo dopo aver fatto gli esercizi base.

8.3 Cercare dentro al testo

contains dice se una parte di testo c'è, startsWith e endsWith controllano inizio e fine, indexOf dice in che posizione si trova (e restituisce -1 se non c'è).

Che cosa stampa questo programma?

fun main() {
    val frase = "gatto"
    frase.replace("a", "o")
    println(frase)
}
gottoAttenzione: replace restituisce una nuova stringa, ma qui il risultato non viene salvato da nessuna parte.
gattoEsatto: la stringa originale resta identica. Servirebbe val nuova = frase.replace("a", "o").
Un errore, perché replace non esistereplace esiste eccome: semplicemente non modifica la stringa su cui lo chiami.

Vale per replace, trim, uppercase, substring: le stringhe sono immutabili, quindi il risultato va assegnato a una variabile.

8.4 Tagliare e dividere: substring e split

substring(a, b) prende i caratteri dalla posizione a (compresa) alla b (esclusa). split(separatore) spezzetta una stringa in una lista di pezzi.

Che cosa stampa "programmare".substring(0, 7)?

programm (8 caratteri)Attenzione: il secondo numero è escluso, quindi la parte presa va dall'indice 0 all'indice 6.
program (7 caratteri)Esatto: substring(a, b) prende da a compreso a b escluso.
rogrammaQuesto è il risultato di substring(1, 8): il primo numero è l'inizio, non il conteggio.

Con substring vale la stessa regola degli indici: si conta da 0 e la fine è esclusa. Per gli ultimi caratteri si usa substring(n).

  1. split(" ") taglia la frase su ogni spazio: si ottiene una lista di tre parole.
  2. joinToString("-") fa il lavoro inverso: rimette insieme gli elementi della lista separandoli con un trattino.
  3. Da qui le due righe: 3 e Il-gatto-dorme.
split e joinToString sono complementari

split spezza un testo in una lista; joinToString(", ") fa l'operazione inversa, unendo una lista in un unico testo. Nel prossimo esempio li usi insieme.

8.5 Conversioni e confronti

Per confrontare due testi si usa == (in Kotlin confronta il contenuto, non l'indirizzo di memoria). Le conversioni fra testo e numeri si fanno con toInt(), toDouble(), toIntOrNull() e toString().

Costruire una stringa pezzo per pezzo

Quando devi comporre un testo lungo (per esempio le iniziali di tutte le parole) usa una var vuota e il +=: var sigla = "" e poi sigla += parola[0] dentro al ciclo.

8.6 Riepilogo

Come leggere questa tabella: base ed extra

Le righe base sono quelle da sapere a memoria per gli esercizi. Le righe extra (padStart, padEnd, repeat, "%.2f".format) servono per i report ordinati del capitolo 11: consultale quando ti servono, senza memorizzarle ora.

Prima decidi che cosa vuoi ottenere, poi cerca il metodo

Con tanti metodi a disposizione è facile perdersi: non partire dal nome, parti dall'intento. Queste sono le quattro domande più comuni e la famiglia di metodi che risponde.

Metodo o proprietàCosa fa
s.length (base)numero di caratteri
s[i], s.get(i) (base)il carattere in posizione i (tipo Char)
s.first(), s.last() (base)primo e ultimo carattere
uppercase(), lowercase() (base)tutto maiuscolo / minuscolo
trim(), trimStart(), trimEnd() (base)toglie gli spazi esterni
isEmpty(), isNotEmpty(), isBlank() (base)stringa vuota / non vuota / solo spazi
contains(x) (base)contiene la parte di testo x
startsWith(x), endsWith(x) (base)inizia / finisce con x
indexOf(x), lastIndexOf(x) (base)posizione di x (oppure -1)
replace(a, b), replaceFirst(a, b) (base)sostituisce parti di testo
substring(a), substring(a, b) (base)taglia da a (incluso) a b (escluso)
split(sep) (base)divide in una lista di pezzi
reversed() (base)la stringa al contrario
repeat(n), padStart(n, c), padEnd(n, c) (extra)ripete / completa a sinistra / a destra
toInt(), toDouble(), toIntOrNull(), toDoubleOrNull() (base)conversioni in numero
"%.2f".format(x) (extra)formatta un numero con due decimali
forEach { } (base)esegue qualcosa per ogni carattere
Le stringhe non si modificano "sul posto"

s.replace("a", "b") restituisce una nuova stringa: quella originale resta identica. Se vuoi conservare il risultato devi assegnarlo: var s = s.replace("a", "b") oppure val nuovo = s.replace(...). Lo stesso vale per trim, uppercase, substring.

%.2f in un progetto vero: punto o virgola?

Qui nel manuale "%.2f".format(2.5) stampa sempre 2.50, con il punto. In un progetto IntelliJ il risultato può dipendere dalla lingua impostata sul computer: su un sistema in italiano la stessa istruzione può stampare 2,50, con la virgola. È lo stesso tema del "il punto, non la virgola" del capitolo 2 e 3 — ma al contrario: qui è il computer a scegliere il separatore quando formatta per farlo leggere a chi usa il programma. Nella console del manuale la scelta è fissa al punto, così non ci sono sorprese mentre impari; quando incontrerai la virgola in IntelliJ, sappi che non è un errore.

8.7 Esercizi guidati

Leggi una parola, puliscila con trim().lowercase() e stabilisci se è palindroma, cioè se si legge uguale da sinistra a destra e da destra a sinistra (per esempio "ingegni", "anna", "osso").

Il metodo più corto è if (parola == parola.reversed()). Se preferisci farlo a mano, confronta parola[i] con parola[parola.length - 1 - i] dentro un ciclo.

  1. trim().lowercase() normalizza l'input: così "Anna " e "anna" vengono considerate uguali.
  2. reversed() restituisce una nuova stringa con i caratteri al contrario: non modifica parola.
  3. Il confronto con == in Kotlin confronta il contenuto delle due stringhe, quindi funziona come ci si aspetta.
  4. Nel ramo else stampiamo anche la versione invertita: è utile per capire a colpo d'occhio cosa non tornava.

Leggi una frase e stampa: quante parole contiene, la parola più lunga, quante parole iniziano con la lettera "i" e la frase con le iniziali maiuscole di ogni parola.

Per la parola più lunga parti dalla prima parola come riferimento e aggiorna se ne trovi una con length maggiore. Per la frase con le iniziali maiuscole usa una var titolo = "" e aggiungi parola.substring(0, 1).uppercase() + " " a ogni giro.

  1. split(" ") trasforma la frase in una lista di parole: da lì in poi si lavora con cicli e metodi delle liste.
  2. Per il confronto sull'iniziale si usa lowercase() prima di startsWith("i"): così funzionano anche "Impariamo" e "I".
  3. titolo è una var che cresce con +=: prima lettera maiuscola, resto minuscolo, spazio in fondo.
  4. Alla fine titolo.trim() toglie lo spazio finale che l'ultima parola ha lasciato.

Leggi nome e cognome separati da uno spazio (es. "mario rossi") e l'anno di nascita. Costruisci un codice tessera con questo formato: 3 lettere del cognome in maiuscolo + iniziale del nome + ultime due cifre dell'anno, per esempio ROSSM08. Attenzione al caso limite: se il cognome ha meno di 3 lettere (es. "Li", "Fo"), completa con X fino a 3 caratteri (es. "Li" diventa LIX).

Il cognome è parti[1], il nome parti[0]. Le ultime due cifre dell'anno sono anno.substring(anno.length - 2). Per il cognome corto usa if (cognome.length >= 3) cognome.substring(0, 3).uppercase() else cognome.uppercase().padEnd(3, 'X').

  1. split(" ") separa nome e cognome: parti[0] è il nome, parti[1] il cognome.
  2. substring(0, 3) prende i primi tre caratteri (indici 0, 1, 2), ma solo se esistono: con "Li" il programma si fermerebbe con un errore di indice.
  3. Il ramo else con padEnd(3, 'X') completa i cognomi corti ("Li" diventa "LIX"): il codice resta sempre di 3 + 1 + 2 = 6 caratteri.
  4. substring(anno.length - 2) prende gli ultimi due caratteri: con "2008" si ottiene "08".

I dati arrivano come 2026-05-17 e il programma deve stampare 17/05/2026 e poi il mese in maiuscolo (05). Non funziona: correggi.

I metodi si chiamano con le parentesi tonde, anche quando non hanno argomenti: uppercase(). E nel template si scrive $mese, non mese.toString().

  1. parti[1].uppercase senza parentesi non chiama il metodo: il programma si ferma perché si aspetta una chiamata.
  2. La forma corretta è parti[1].uppercase(), che restituisce 05: le cifre non hanno maiuscolo.
  3. Per stampare il nome del mese (MAGGIO) servirebbe una mappa dei mesi; uppercase() cambia solo le lettere che trova.
  4. println(mese) stampa già il testo: toString() qui è inutile, perché Kotlin lo applica da solo dentro a println.
  • so usare length e s[i] per lavorare sui singoli caratteri
  • so pulire un testo con trim() e cambiarne le maiuscole
  • so cercare dentro un testo con contains, startsWith, indexOf
  • so tagliare e dividere con substring e split
  • so ricomporre un testo con joinToString e formattare i numeri con "%.2f".format(...)
  • so spiegare perché il risultato di replace va assegnato a una variabile
  • so leggere un messaggio di errore e correggere il programma da solo