1. Errori di compilazione ed errori di esecuzione
Se finora avete percepito gli errori come “la sottolineatura rossa nell’IDE”, ora e importante separare due mondi. Ci sono errori che Kotlin trova prima dell’avvio (in fase di compilazione) e ci sono problemi che si manifestano solo durante l’esecuzione del programma. Le eccezioni appartengono al secondo tipo: compaiono quando il programma e gia in esecuzione e incontra una situazione in cui non puo continuare “secondo il piano”.
Per chiarezza, e comodo confrontare queste cose in una tabella:
| Che cos’e | Quando si verifica | Cosa vedi | Esempio |
|---|---|---|---|
| Errore di compilazione | Prima dell’avvio del programma | L’IDE segnala un errore, kotlinc non compila | |
| Eccezione (runtime exception) | Durante l’esecuzione | Il programma va in crash, viene stampato un messaggio e lo stack trace | |
Un’eccezione non e “un altro if”. E un meccanismo d’emergenza: come un allarme antincendio. Se scatta e nessuno sa cosa fare, le persone (cioe l’esecuzione del programma) corrono verso l’uscita (cioe il programma termina).
In Kotlin le eccezioni sono rappresentate da oggetti che sono discendenti di Throwable. In termini generali: Throwable → Exception / Error. Di solito nel codice applicativo incontriamo Exception, mentre Error riguarda piuttosto problemi seri dell’ambiente di esecuzione (per esempio, mancanza di memoria).
Che cos’e un’eccezione
Immaginate che il vostro programma sia un treno che viaggia sui binari delle righe di codice. Il normale flusso di esecuzione e: riga 1, riga 2, riga 3… Un’eccezione e una situazione in cui sui binari, all’improvviso, c’e un blocco di cemento: non si puo andare avanti e il treno non “scavalchera” l’ostacolo con delicatezza. Si ferma e il controllo viene passato a un meccanismo speciale di gestione degli errori.
Formalmente (ed e molto importante ricordarlo): quando viene lanciata un’eccezione, l’esecuzione normale viene interrotta. Questo significa letteralmente: “la riga successiva a quella problematica non verra eseguita”, se l’eccezione non viene catturata.
Mini-esempio — il classico “divisione per zero”:
fun main() {
println("start")
val a = 10
val b = 0
println(a / b) // ArithmeticException
println("end") // non verra eseguito
}
Notate: la riga println("end") sembra innocua, ma non verra eseguita. Perche il programma e “uscito di strada” dal percorso normale sulla riga a / b.
2. L’eccezione interrompe l’esecuzione e non lascia “andare avanti”
Con le eccezioni, ai principianti di solito confonde una cosa: siamo abituati a pensare che un programma sia un “elenco di comandi” eseguiti in ordine. E poi all’improvviso scopriamo che l’ordine puo interrompersi bruscamente. Quindi ripeto l’idea ancora una volta, con altre parole: un’eccezione non e un “messaggio di errore”, e un cambio di controllo.
Quando compare un’eccezione, la funzione corrente termina non con return, ma in modo anomalo. E poi Kotlin/JVM cerca di trovare un punto che sia pronto a gestire questa eccezione. Se un punto del genere non esiste, il programma termina e voi vedete un messaggio di errore e lo stack delle chiamate.
Un altro dettaglio importante: un’eccezione puo verificarsi non dove avete scritto il codice “pericoloso”, ma “in profondita” dentro funzioni che avete chiamato. Per esempio, chiamate toInt(), e al suo interno Kotlin prova a fare il parsing della stringa e, se non ci riesce, lancia NumberFormatException. Questa eccezione “esce” verso l’esterno fino al vostro codice.
Esempio:
fun main() {
val text = "12a"
val n = text.toInt() // NumberFormatException
println("n = $n") // non verra eseguito
}
Cioe non l’avete creata voi, ma l’eccezione compare comunque, perche la funzione di libreria ha deciso: “non si puo continuare”.
3. Da dove vengono, in generale, le eccezioni
A questo punto e utile non cercare di memorizzare cento nomi di eccezioni, ma vedere i “tipi di situazioni” che le causano. Nei semplici programmi didattici emergono piu spesso tre famiglie di problemi: aritmetica, conversione di stringhe e indicizzazione degli array.
Aritmetica: divisione per zero
Con l’aritmetica e tutto intuitivo: alcune operazioni non sono definite matematicamente. La JVM non “indovinera” cosa intendevate e lancera un’eccezione.
fun main() {
val total = 100
val days = 0
val perDay = total / days
println(perDay) // non ci arriviamo
}
Conversione di una stringa in numero: “non e un numero”
Conoscete gia il percorso sicuro toIntOrNull(). Ma toInt() e la variante “rigida”. Se la stringa non e un intero valido, si verifica NumberFormatException.
fun main() {
val raw = "sorok dva"
val n = raw.toInt() // NumberFormatException
println(n)
}
Indici dell’array: fuori dai limiti
Gli array hanno dimensione fissa e gli indici devono stare nell’intervallo 0..lastIndex. Se no, otterrete un’eccezione a runtime (sulla JVM spesso e ArrayIndexOutOfBoundsException).
fun main() {
val xs = intArrayOf(10, 20, 30)
println(xs[2]) // 30
println(xs[5]) // ArrayIndexOutOfBoundsException
}
La trappola psicologica qui e: “ma l’indice e un numero, l’ho pure parsato!”. Si, e un numero. Ma non un numero qualsiasi: deve essere valido. E l’eccezione comunica proprio questo: “la regola e stata violata”.
4. Gerarchia delle eccezioni: Throwable, Exception, Error
Quando vedete un messaggio di errore, quasi sempre c’e il nome del tipo di eccezione. Per non percepirlo come un insieme di incantesimi casuali, e utile capire il livello superiore della gerarchia.
La radice della gerarchia e Throwable. Ha due importanti “discendenti di primo livello”: Exception e Error. L’idea e: Exception e qualcosa che in linea di principio puo far parte della vita normale di un programma (errore di input, risorsa non disponibile, ecc.), mentre Error di solito indica problemi seri dell’ambiente, che “riparare” a livello della vostra logica non ha senso (per esempio, OutOfMemoryError).
A questo punto ci basta questo schema:
flowchart TD
T[Throwable]
E[Exception]
ER[Error]
T --> E
T --> ER
E --> NFE[NumberFormatException]
E --> AE[ArithmeticException]
E --> AIOOBE[ArrayIndexOutOfBoundsException]
Importante: questo non significa che dobbiate conoscere tutta la gerarchia a memoria. Ma quando vedete ArithmeticException, dovreste pensare: “ok, e un’Exception, quindi e un errore a livello di esecuzione del mio scenario”.
Causa e posizione: nome dell’eccezione e stack trace
Quando il programma va in crash, un principiante spesso ha la sensazione: “si e rotto da qualche parte”. Ma il meccanismo delle eccezioni e progettato proprio per darvi due indizi: che cosa e successo (la causa) e dove e successo (la posizione).
La causa e spesso codificata nel nome del tipo e nel messaggio. Per esempio, NumberFormatException significa letteralmente: “la stringa non va bene come formato numerico”. La posizione di solito si vede nello stack trace: e la catena di chiamate di funzioni che ha portato all’errore. Kotlin/JVM, quando un’eccezione non viene gestita, stampa automaticamente lo stack trace.
Mini-esempio che crea apposta un errore:
fun main() {
throw ArithmeticException("Esempio di eccezione")
}
Output indicativo (puo variare, ma il senso e lo stesso):
Exception in thread "main" java.lang.ArithmeticException: Esempio di eccezione
at MainKt.main(Main.kt:2)
Per ora non serve saper leggere lo stack trace in modo perfetto (impareremo a farlo a parte). Ma gia adesso e utile sapere: se un programma e caduto, di solito ha lasciato una “ricevuta” su dove esattamente.
5. Esempio pratico
Per evitare che le eccezioni restino un’astrazione, continuiamo a scrivere una piccola applicazione console. Chiamiamola BudgetBuddy: l’utente inserisce le spese per piu giorni e il programma calcola la somma e la “media al giorno”.
Per ora scriviamo nel modo piu semplice possibile, senza gestire le eccezioni (sara la lezione successiva). Il nostro obiettivo qui e vedere come si rompe esattamente il flusso di esecuzione.
Versione ingenua: “tutto secondo i piani”
Partiamo da uno scenario semplice: leggiamo il numero di giorni, poi leggiamo le spese giornaliere in un array e poi calcoliamo la somma.
fun main() {
print("Quanti giorni? ")
val days = readln().toInt()
val expenses = IntArray(days)
for (i in 0 until days) {
print("Spesa per il giorno ${i + 1}: ")
expenses[i] = readln().toInt()
}
var total = 0
for (x in expenses) total += x
println("Totale: $total")
}
Se l’utente inserisce numeri normali, va tutto bene. Ma i “numeri normali” sono un sogno irrealizzabile.
Scenario di crash: l’utente ha inserito non un numero
Supponiamo che l’utente inserisca cosi:
- “Quanti giorni?” → 3
- “Spesa per il giorno 1” → 100
- “Spesa per il giorno 2” → sto (oppure 100r)
Alla riga readln().toInt() si verifichera NumberFormatException, e il programma terminera. E questo avverra all’interno del ciclo, quindi non arriveremo ne alla somma ne alla println("Totale...") finale.
Scenario di crash: l’utente ha inserito 0 giorni (e noi volevamo la media)
Aggiungiamo la media. In modo ingenuo:
fun main() {
print("Quanti giorni? ")
val days = readln().toInt()
print("Somma delle spese: ")
val total = readln().toInt()
val avg = total / days
println("Media al giorno: $avg")
}
Se days = 0, alla riga total / days ci sara ArithmeticException. E di nuovo: la riga println(...) non verra eseguita.
Cio che e particolarmente insidioso qui e: anche se days e stato parsato con successo (cioe e davvero un numero), puo comunque essere non valido per la vostra logica. Le eccezioni spesso non nascono nel parsing, ma nell’uso del valore.
Scenario di crash: uso errato dell’indice
Immaginiamo di voler mostrare “l’ultima spesa inserita”, ma sbagliamo e prendiamo l’indice days invece di days - 1. (E un dolore vissuto da generazioni di programmatori, e la tradizione continuera.)
fun main() {
val expenses = intArrayOf(100, 200, 300)
val days = expenses.size
println(expenses[days]) // days == 3, e gli indici sono 0..2
}
Questo produrra ArrayIndexOutOfBoundsException. Il programma terminera di nuovo in modo anomalo.
Cosa succede con un’eccezione: mini-schema
Per fissare il modello, e utile immaginare il comportamento del programma come uno schema. Il nostro programma esegue le righe finche va tutto bene. Se una riga lancia un’eccezione, avviene un “uscita d’emergenza” dalla funzione corrente e una risalita lungo lo stack delle chiamate.
flowchart TD
A[Eseguiamo riga per riga] --> B{Si e verificato un errore?}
B -->|no| A
B -->|si: eccezione| C[Interrompiamo la funzione corrente]
C --> D[Cerchiamo un gestore piu in alto nello stack]
D --> E{Trovato un gestore?}
E -->|no| F[Stampiamo lo stack trace e terminiamo il programma]
E -->|si| G[Passiamo il controllo al gestore]
Per oggi vi basta ricordare il ramo “non trovato un gestore → il programma e terminato”. Del ramo “trovato un gestore” parleremo in dettaglio nella prossima lezione, quando comparira try/catch.
6. Errori tipici
Errore n. 1: confondere un errore di compilazione con un’eccezione a runtime.
Se l’IDE sottolinea il codice prima dell’avvio, e compilazione: il programma non parte nemmeno. Se invece il programma e partito, ha fatto in tempo a stampare qualcosa e poi all’improvviso e “caduto” mostrando un testo lungo, e un’eccezione, cioe un problema di runtime.
Errore n. 2: aspettarsi che il codice dopo la “riga problematica” venga comunque eseguito.
E la cosa piu comune. Sembra che println("end") debba per forza stampare, visto che e “dopo” la divisione. Ma l’eccezione rompe la linearita: l’esecuzione viene interrotta. Nella testa bisogna cambiare interruttore: “eccezione = uscita d’emergenza”.
Errore n. 3: pensare che se usiamo toIntOrNull(), allora non ci saranno piu eccezioni.
toIntOrNull() salva davvero da NumberFormatException, ma non protegge dalla divisione per zero, dall’uscita dai limiti dell’array e dagli errori logici. Non e “un’armatura contro tutto”, ma uno strumento contro una causa specifica.
Errore n. 4: ignorare il nome dell’eccezione e leggere lo stack trace come “un lenzuolo spaventoso”.
Anche senza abilita di debug, il nome dell’eccezione e spesso molto informativo: NumberFormatException — non e un numero, ArithmeticException — aritmetica, ArrayIndexOutOfBoundsException — indice fuori range. Kotlin/JVM vi fornisce apposta questi indizi.
Errore n. 5: pensare che un’eccezione sia “la fine del mondo” e quindi sia meglio “non toccare”.
In realta le eccezioni sono un normale meccanismo di lavoro. Il problema non e la loro esistenza, ma il fatto che per ora non sappiamo gestirle correttamente e trasformarle in un comportamento comprensibile del programma. Ed e proprio quello che faremo dopo: impareremo a catturare le eccezioni previste e a continuare a lavorare, senza trasformare la console in un teatro drammatico.
GO TO FULL VERSION