Excepciones: try, catch y require
Al terminar: Provocar y capturar errores con throw, try, catch y finally, y comprobar condiciones con require, check y error.
Hasta ahora Torneo.agregar(partido) lanza una excepción si algo está mal, y quien la llama tiene que acordarse de capturarla. Esta lección enseña esa herramienta por dentro: qué es una excepción, cómo se atiende y cómo se comprueban las condiciones con una línea. En la lección siguiente, Result: errores como valores, Torneo.registrar(...) devolverá un Result<Partido> en vez de lanzar. El proyecto no cambia en esta: todo se prueba en el Playground.
Temas: lanzar una excepción con throw y atenderla con try/catch/finally, varios catch, try como expresión, y require, check y error.
Excepciones: un fallo que sube hasta que alguien lo atiende
Una excepción es la forma que tiene un programa de decir «aquí ha pasado algo que no puedo resolver». Interrumpe la función donde ocurre y sube por las funciones que la llamaron hasta encontrar un try/catch que la atienda. Si nadie la atiende, el programa termina con un stack trace, como el que viste en Nulabilidad.
Dos palabras de la cita: una subclase es una clase que es un caso más concreto de otra (IllegalArgumentException es un caso más concreto de Exception), y Throwable es el tipo más general de todos: agrupa las excepciones y los errores graves. Casi siempre escribirás excepciones concretas, no Throwable.
Lanzar una excepción y atenderla
Lanzar una excepción es escribir throw seguido de la excepción que quieres crear, con un texto que explique qué pasó. Atenderla es envolver el código que puede fallar en un try y escribir, en el catch, qué hacer si ocurre.
fun precioConDescuento(precio: Double, descuento: Int): Double {
if (descuento !in 0..100) { // ¿el descuento es un porcentaje posible?
throw IllegalArgumentException("Descuento imposible: $descuento") // throw lanza la excepción
}
return precio - precio * descuento / 100
}
fun main() {
try {
println(precioConDescuento(50.0, 20)) // 40.0: no hay problema y se imprime
println(precioConDescuento(50.0, 150)) // lanza: la ejecución salta al catch
println("esta línea nunca se ejecuta") // la función quedó interrumpida
} catch (e: IllegalArgumentException) { // `e` es el nombre que eliges para la excepción; atiende solo este tipo
println("Fallo: ${e.message}") // message: el texto que se pasó a throw
}
println("el programa sigue")
}Al ejecutarlo, imprime:
40.0
Fallo: Descuento imposible: 150
el programa sigueSi quitaras el try/catch, la excepción subiría hasta el final de main y el programa terminaría con un stack trace en lugar de seguir.
try como expresión y finally
En Kotlin, try también es una expresión: su valor es el del bloque que se ejecute (el try si todo va bien, el catch si algo falla). Así puedes guardar en una variable el resultado o un valor de respaldo. El bloque finally, si lo hay, se ejecuta siempre y no cambia el valor.
fun cantidadValida(unidades: Int): Int {
require(unidades in 1..10) { "Se venden de 1 a 10 unidades, no $unidades" }
return unidades
}
fun main() {
// try es una expresión: su valor es el del bloque que se ejecute.
val unidades: Int? = try {
cantidadValida("3".toInt()) // "3" se convierte en 3 y es válida: este es el valor
} catch (e: NumberFormatException) {
null
} finally {
println("el try terminó") // finally se ejecuta siempre
}
println(unidades)
val otras: Int? = try {
cantidadValida("diez".toInt()) // "diez" no es un número: lanza NumberFormatException
} catch (e: NumberFormatException) {
null // este valor es el de todo el try
} finally {
println("el try terminó")
}
println(otras)
val demasiadas = try {
cantidadValida(50) // lanza IllegalArgumentException y este catch la atiende: el valor es -1
} catch (e: IllegalArgumentException) {
-1
}
println(demasiadas)
}Al ejecutarlo, imprime:
el try terminó
3
el try terminó
null
-1Varios catch
Un try puede llevar varios catch seguidos, uno por cada tipo de excepción, y entra solo el primero que encaja. Sirve para responder distinto a cada fallo. NumberFormatException (el texto no es un número) es un caso más concreto de IllegalArgumentException (un argumento que no vale), y por eso va primero: si fuera al revés, el catch general se llevaría los dos casos.
fun cantidadValida(unidades: Int): Int {
require(unidades in 1..10) { "Se venden de 1 a 10 unidades, no $unidades" }
return unidades
}
fun descripcion(texto: String): String {
return try {
"vale ${cantidadValida(texto.toInt())}"
} catch (e: NumberFormatException) { // el texto no es un número
"no es un número"
} catch (e: IllegalArgumentException) { // es un número, pero require lo rechazó
"fuera de rango"
}
}
fun main() {
println(descripcion("3"))
println(descripcion("diez"))
println(descripcion("50"))
}vale 3
no es un número
fuera de rangorequire, check y error
Escribir if (…) throw … cada vez es largo. Tres funciones lanzan la excepción por ti cuando la condición no se cumple:
| Función | Para qué | Lanza |
|---|---|---|
require(cond) { … } | Comprobar lo que te dan (argumentos) | IllegalArgumentException |
check(cond) { … } | Comprobar el estado del objeto | IllegalStateException |
error("…") | Marcar un caso que no debería ocurrir | IllegalStateException |
class Cuenta(var saldo: Int) {
fun retirar(cantidad: Int) {
require(cantidad > 0) { "La cantidad debe ser positiva" } // comprueba lo que te dan
check(saldo >= cantidad) { "Saldo insuficiente: $saldo" } // comprueba el estado del objeto
saldo -= cantidad
}
}
fun nombreDelDia(numero: Int): String = when (numero) {
1 -> "lunes"
2 -> "martes"
else -> error("Día sin definir: $numero") // error() siempre lanza y nunca termina con normalidad, así que Kotlin acepta esta rama aunque el when deba dar un texto
}
fun main() {
val cuenta = Cuenta(100)
try { cuenta.retirar(-5) } catch (e: IllegalArgumentException) { println("require: ${e.message}") }
try { cuenta.retirar(500) } catch (e: IllegalStateException) { println("check: ${e.message}") }
try { nombreDelDia(9) } catch (e: IllegalStateException) { println("error: ${e.message}") }
}Al ejecutarlo, imprime:
require: La cantidad debe ser positiva
check: Saldo insuficiente: 100
error: Día sin definir: 9Reto · Ejercicio: un minuto que puede no valer
En el Kotlin Playground, fuera de tu proyecto:
- Una función
minutoDesdeTextoque recibe un texto con un minuto de partido, de 0 a 120, y devuelve ese minuto como entero, onullsi el texto no es un número o el número se sale de ese rango. No debe lanzar nunca. El texto puede traer espacios a los lados, ytrim()los quita de los dos lados. - Pruébala con "45", "200", "diez" y " 90 " e imprime los cuatro resultados, uno por línea.
Salida esperada:
45
null
null
90Ver solución
fun minutoDesdeTexto(texto: String): Int? {
return try {
val minuto = texto.trim().toInt() // lanza NumberFormatException si no es un número
require(minuto in 0..120) { "Minuto fuera del partido: $minuto" }
minuto
} catch (e: NumberFormatException) {
null
} catch (e: IllegalArgumentException) {
null
}
}
fun main() {
println(minutoDesdeTexto("45"))
println(minutoDesdeTexto("200"))
println(minutoDesdeTexto("diez"))
println(minutoDesdeTexto(" 90 "))
}Los dos fallos (texto que no es un número y número fuera de rango) se atienden con su propio catch y los dos devuelven null. Como NumberFormatException va primero, el catch general solo recoge lo que quede.
Paso · El proyecto sigue igual
Objetivo. Comprobar que Marcador sigue como lo dejaste. No tocas ningún fichero: en la lección siguiente Torneo.registrar usa estas excepciones por dentro.
Punto de control
- En la Terminal de Android Studio (View → Tool Windows → Terminal),
./gradlew :torneo:test :torneo:runtermina enBUILD SUCCESSFUL. - Abre el informe de tests como en Bucles y tu primer test (
open torneo/build/reports/tests/test/index.html) y comprueba que arriba pone 65 tests, los mismos de antes: esta lección no añade ninguno. - En el Playground, tu ejercicio imprime
45,null,nully90.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.Dentro de un
try, una función ejecutathrow IllegalArgumentException("mal")y hay uncatch (e: IllegalArgumentException). ¿Qué ocurre con las líneas que van después delthrow, dentro deltry?2.¿Cuál es el valor de
val n = try { "diez".toInt() } catch (e: NumberFormatException) { -1 }?3.¿Qué diferencia hay entre
requireycheck?4.Un
trytiene doscatch: primeroNumberFormatExceptiony luegoIllegalArgumentException. El bloque lanza unaNumberFormatException. ¿Quécatchentra?