Lanzar excepciones y crear las tuyas
En resumen: Con throw un método se niega y lanza una excepción que cae a través de las llamadas hasta que algún catch la atrapa; para tu propio caso escribes tu propia clase de excepción.
Problema
El cajero dice «recoge tu dinero»
La cuenta guarda su saldo. Si el importe a retirar es negativo o no hay dinero suficiente, el método simplemente no hace nada:
public void withdraw(int amount) { if (amount > 0 && amount <= balance) { balance -= amount; }}El cajero lo usa así:
Account account = new Account(100);account.withdraw(500);System.out.println("Recoge tu dinero.");System.out.println("Saldo: " + account.getBalance() + " €");Recoge tu dinero.Saldo: 100 €El cliente pide 500 € y tiene 100 €. El método se niega en silencio, el cajero no se da cuenta y dice «Recoge tu dinero.». El error se pierde. El método podría devolver true o false, pero quien lo llama puede olvidarse fácilmente de comprobarlo, y false no dice por qué.
La idea
throw: me niego y digo por qué
En lugar de callarse, el método lanza una excepción:
public void withdraw(int amount) { if (amount <= 0) { throw new IllegalArgumentException("El importe debe ser positivo"); } if (amount > balance) { throw new IllegalArgumentException("No alcanza el dinero"); } balance -= amount;}throw termina el método como return. A la línea balance -= amount nunca se llega. Pero en lugar de un valor, el método envía una excepción, y quien lo llama no puede pasarla por alto:
Account account = new Account(100);try { account.withdraw(500); System.out.println("Recoge tu dinero.");} catch (IllegalArgumentException e) { System.out.println("Rechazado: " + e.getMessage());}System.out.println("Saldo: " + account.getBalance() + " €");Rechazado: No alcanza el dineroSaldo: 100 €newcrea el objeto de la excepción, como cualquier objeto. El mensaje va a su constructor.IllegalArgumentExceptiones la excepción ya hecha para «el argumento es incorrecto». La usas justo en este tipo de comprobación.
Más precisamente, para curiosos
- Otra excepción ya hecha muy común es
IllegalStateException: el objeto no está en un estado que permita esta acción, como retirar de una cuenta cerrada. throwfunciona con cualquier objeto de una clase hija deThrowable. No puedes lanzar un string ni un número.
Míralo
La excepción cae
public void withdraw(int amount) { if (amount > balance) { throw new IllegalArgumentException("No alcanza el dinero"); } balance -= amount;}static void payRent(Account account) { account.withdraw(800); System.out.println("El alquiler está pagado.");}
try { payRent(new Account(500));} catch (IllegalArgumentException e) { System.out.println("Rechazado: " + e.getMessage());}Paso a paso, en palabras
- main llama a payRent para una cuenta con 500 €.
- payRent llama a withdraw(800): un marco nuevo encima.
- 800 > 500: el método lanza una excepción.
- El saldo no cambia: el marco se descarta sin terminar sus líneas.
- payRent no tiene catch: «El alquiler está pagado.» no se imprime, el marco se descarta.
- catch en main atrapa la excepción y el programa sigue desde aquí.
Recuerda la pila de llamadas de la lección de recursión. main llama a payRent, y este llama a withdraw. Los marcos están uno encima de otro.
Cuando withdraw lanza, la excepción busca un catch bajando por la pila. Un marco sin un catch que coincida se descarta enseguida, sin terminar sus líneas. Eso es lo que le pasa también a payRent. Solo main tiene un catch, y el programa sigue desde ahí.
Si main tampoco tuviera catch, también se descartaría. Entonces el programa se detiene e imprime un stack trace: justo estos marcos, de dentro hacia fuera.
Pruébalo
¿Qué llega a la pantalla?
La cuenta tiene 50 €, y el pedido es de 120 €.
¿Qué imprimirá el programa?
static void checkout(Account account) { System.out.println("Pago el pedido"); account.withdraw(120); System.out.println("El pedido está pagado");}
Account account = new Account(50);try { checkout(account); System.out.println("Envío la confirmación");} catch (IllegalArgumentException e) { System.out.println("Rechazado: " + e.getMessage());}System.out.println("Saldo: " + account.getBalance() + " €");«Pago el pedido» va antes del retiro. Después withdraw lanza, y el marco de checkout se descarta: «El pedido está pagado» nunca aparece. En main, «Envío la confirmación» también se salta, porque está en try después de la llamada. catch imprime el rechazo, y el saldo queda intacto.
Pruébalo
Encuentra el error
El alquiler es de 800 €, y la cuenta tiene 500 €. El programa dice «El alquiler está pagado.». ¿En qué línea está el error?
Elige la línea con el error.
catch vacío atrapa la excepción y no hace nada. El error desaparece, igual que con el cajero de la primera tarjeta. Se atrapa donde se puede hacer algo sensato. Aquí payRent no puede encontrar el dinero, así que no debería atrapar. Quita el try/catch: la excepción llegará a main, y ese avisará a la persona.Más precisamente, para curiosos
- A veces un método atrapa, hace algo, como escribir en un registro, y vuelve a lanzar la misma excepción:
throw e;encatch. - También puedes lanzar otra excepción que diga mejor qué pasó. Entonces pasa la antigua como segundo argumento del constructor:
new IllegalStateException("El alquiler no está pagado", e). Verás las dos en el stack trace.
La idea
Tu propia excepción
«No alcanza el dinero» es texto. Si el cajero quiere proponer «Ingresa 30 € más», tiene que saber cuánto falta. Sacar el número del texto es frágil. Mejor haz tu propia clase de excepción con un campo:
class InsufficientFundsException extends RuntimeException { private int shortfall;
public InsufficientFundsException(int shortfall) { super("Faltan " + shortfall + " €"); this.shortfall = shortfall; }
public int getShortfall() { return shortfall; }}Tu propia excepción es una clase normal que hereda de RuntimeException. El constructor pasa el mensaje a la clase padre con super(...). El método la lanza cuando no alcanza el dinero:
public void withdraw(int amount) { if (amount <= 0) { throw new IllegalArgumentException("El importe debe ser positivo"); } if (amount > balance) { throw new InsufficientFundsException(amount - balance); } balance -= amount;}Y quien lo llama atrapa los dos casos por separado:
Account account = new Account(100);try { account.withdraw(130);} catch (InsufficientFundsException e) { System.out.println(e.getMessage()); System.out.println("Ingresa " + e.getShortfall() + " € más o elige un importe menor.");} catch (IllegalArgumentException e) { System.out.println("Rechazado: " + e.getMessage());}Faltan 30 €Ingresa 30 € más o elige un importe menor.Más precisamente, para curiosos
- Los nombres de las excepciones siempre terminan en
Exception. - Tu propia excepción también puede heredar de una clase ya hecha más concreta, como
IllegalArgumentException. Entonces uncatchpara la clase padre también la atrapa. Aquí es aparte, así que el orden de los dos bloquescatchno importa.
La idea
¿Exception o RuntimeException?
Si tu propia excepción hereda de Exception y no de RuntimeException, es comprobada. El método tiene que declararla con throws, y cada uno que lo llame tiene que atraparla o declararla también:
class InsufficientFundsException extends Exception { ... }
public void withdraw(int amount) throws InsufficientFundsException { ...}RuntimeException, cuando el error está en los argumentos o en las reglas del programa. Quien llama puede evitarlo con una comprobación. La mayoría de las excepciones propias hoy son así.Exception, cuando quien llama tiene que estar preparado para ella cada vez y el compilador debe recordárselo.
Una excepción ya hecha, cuando el significado coincide: IllegalArgumentException para un argumento incorrecto, IllegalStateException para una acción en el momento equivocado. Una propia, cuando quien llama tiene que distinguirla de las demás y reaccionar a su manera, o cuando la excepción tiene que llevar datos, como shortfall.
Pruébalo
Rellena el hueco
Una excepción para una edad no válida. ¿Qué va en el hueco para que el mensaje llegue a la clase padre?
class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
(message);
}
}super(message) llama al constructor de la clase padre. La clase padre guarda el mensaje, y getMessage() lo devuelve.
Ejercicio
Excepciones en clases
Resuélvelas en tu editor.
1. Tienda de música. La tienda toca instrumentos en una demostración, pero no todos se pueden tocar.
- Una clase abstracta
MusicalInstrumentcon un nombre, una marca y un precio (campos privados con getters), y un constructor. - Una interfaz
Playablecon un métodoplay(). GuitaryPianoheredan del instrumento y pueden tocar.Drumsolo hereda.NotPlayableException extends RuntimeException.MusicShopcon un métododemoInstrument(MusicalInstrument instrument): si el instrumento puede tocar (instanceof Playable playable), lo toca; si no, lanzaNotPlayableException.
- En main, haz la demostración de una guitarra Fender Stratocaster (1299 €), una batería Pearl Export (849 €) y un piano Yamaha P-45 (499 €) desde un array. Atrapa la excepción de cada instrumento por separado, para que la demostración siga.
Fender Stratocaster, 1299.00 €La guitarra toca un acorde.Pearl Export, 849.00 €Pearl Export no se puede usar en una demostración.Yamaha P-45, 499.00 €El piano toca una escala.2. Una cuenta con reglas. Añade a Account un método deposit(int amount) que lance IllegalArgumentException con un importe de 0 o menos. Retirar lanza InsufficientFundsException de la lección. La cuenta empieza con 100 €. Lee el número de operaciones y después las propias operaciones, cada una en su propia línea: «ingresa 50» o «retira 500». Después de cada una, imprime el saldo o por qué se rechazó la operación.
4ingresa 50retira 500retira -5retira 120Saldo: 150 €Faltan 350 €. Saldo: 150 €El importe debe ser positivo. Saldo: 150 €Saldo: 30 €3. Edad no válida. Person tiene un nombre y una edad. setAge lanza InvalidAgeException de la tarjeta anterior si la edad es menor que 0 o mayor que 150. El constructor también pasa por setAge, para que nadie pueda crear una persona con una edad no válida. Crea a María (19), Pablo (−3) y Juan (200).
María, 19 añosPablo: Edad no válida: -3Juan: Edad no válida: 200Más tareas
- Reserva de mesa. Una clase
Restaurantcon un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propiaNoTablesException. Si el número de invitados es 0 o menos, lanza la excepción ya hecha para un argumento incorrecto. - Un producto con existencias. Una clase
Productcon una cantidad en existencias y un métodosell(int qty). Si no hay suficiente en existencias, lanzaOutOfStockException, que lleva cuántas unidades hay. La tienda atrapa la excepción y ofrece vender las que hay en existencias. - Tienda de música con motivo. Añade a
NotPlayableExceptionun campo con el instrumento que no puede tocar, e imprime su precio encatch.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces · Comprobaciones de tipo · Excepciones · try, catch y finally
- Continúa en: Datos en archivos · record, switch y patrones · Patrones de diseño · Tests con JUnit
- Ejercicios sobre el tema: Errores y excepciones