Ir al contenido
wannacode

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() + " €");
Salida
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() + " €");
Salida
Rechazado: No alcanza el dinero
Saldo: 100 €
  • new crea el objeto de la excepción, como cualquier objeto. El mensaje va a su constructor.
  • IllegalArgumentException es 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.
  • throw funciona con cualquier objeto de una clase hija de Throwable. 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
  1. main llama a payRent para una cuenta con 500 €.
  2. payRent llama a withdraw(800): un marco nuevo encima.
  3. 800 > 500: el método lanza una excepción.
  4. El saldo no cambia: el marco se descarta sin terminar sus líneas.
  5. payRent no tiene catch: «El alquiler está pagado.» no se imprime, el marco se descarta.
  6. 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() + " €");
Respuesta

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.

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; en catch.
  • 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());
}
Salida
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 un catch para la clase padre también la atrapa. Aquí es aparte, así que el orden de los dos bloques catch no 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);
    }
}

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 MusicalInstrument con un nombre, una marca y un precio (campos privados con getters), y un constructor.
  • Una interfaz Playable con un método play().
  • Guitar y Piano heredan del instrumento y pueden tocar. Drum solo hereda.
  • NotPlayableException extends RuntimeException.
  • MusicShop con un método demoInstrument(MusicalInstrument instrument): si el instrumento puede tocar (instanceof Playable playable), lo toca; si no, lanza NotPlayableException.
  • 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.
Salida
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.

Entrada
4
ingresa 50
retira 500
retira -5
retira 120
Salida
Saldo: 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).

Salida
María, 19 años
Pablo: Edad no válida: -3
Juan: Edad no válida: 200

Más tareas

  • Reserva de mesa. Una clase Restaurant con un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propia NoTablesException. 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 Product con una cantidad en existencias y un método sell(int qty). Si no hay suficiente en existencias, lanza OutOfStockException, 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 NotPlayableException un campo con el instrumento que no puede tocar, e imprime su precio en catch.

Test

Ponte a prueba

1. ¿Qué hace throw new IllegalArgumentException("...")?
2. main llama a a, a llama a b, b lanza una excepción. Solo main tiene un catch. ¿Qué pasa con las líneas de a después de la llamada a b?
3. ¿Dónde es mejor atrapar la excepción?
4. Tu propia excepción hereda de Exception, no de RuntimeException. ¿Qué se sigue de eso?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·