====== 14 - Excepciones ======
Mira el siguiente trozo de código:
public class Main {
public static void main(String[] args) {
Scanner reader = new Scanner(System.in);
int dividendo, divisor;
System.out.print("Dividendo: ");
dividendo = reader.nextInt();
System.out.print("Divisor: ");
divisor = reader.nextInt();
System.out.println("Resultado: " + (dividendo / divisor));
}
}
La aplicación es muy sencilla. Lo único que hace es pedir dos números y mostrar por pantalla el resultado de la división entera. Si probamos nuestra aplicación con 6 y 4, por ejemplo, todo funciona como toca:
Dividendo: 6
Divisor: 4
Resultado: 1
¿Qué pasa si probamos con 6 y 0?:
Dividendo: 6
Divisor: 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ejemplos.T12.Main.main(Main.java:16)
La aplicación muestra un error, ya que estamos intentando dividir entre 0. Lo importante es el mensaje del error: //**Exception** in thread...//. Una **excepción** es una situación que ocurre durante la ejecución del código y finaliza la ejecución de forma anormal, por ejemplo, al tratar de dividir un número entero entre cero.
===== Errores y excepciones =====
La diferencia entre una excepción y un error es que las primeras las podemos (la mayoría las debemos) detectar y tratar en nuestro código, mientras un error no se pueden manejar en tiempo de ejecución (por ejemplo, el programa requiere más memoria de la disponible).
Todos los tipos de excepciones y errores son subclases de la clase //Throwable// , que es la clase base de la jerarquía. Una rama está encabezada por //Exception//. Esta clase se utiliza para condiciones excepcionales que los programas de usuario deberían detectar. Otra rama, //Error//, es utilizada por el sistema de tiempo de ejecución de Java (JVM) para indicar errores que tienen que ver con el entorno de tiempo de ejecución en sí (JRE).
{{ :clase:daw:prog:3eval:excepciones01.gif?400 |}}
En Java, dentro de las excepciones tenemos dos clases:
* **Excepciones marcadas** (**checked**): Son las excepciones que ocurren en tiempo de compilación. Son todas aquellas clases que heredan de la clase //Exception//, excepto //RuntimeException// y sus derivadas.
* **Excepciones no marcadas** (**unchecked**): Son las excepciones que ocurren en tiempo de ejecución. Son aquellas clases que heredan de la clase //RuntimeException//.
===== Capturar excepciones =====
Casi todos los lenguajes de programación ofrecen herramientas para capturar y tratar excepciones. Una de las más comunes es el bloque **try...catch**. Lo que se hace es encerrar el código susceptible de lanzar una excepción dentro del bloque //try// y, si ocurre una excepción, capturarla en el bloque //catch//.
Por ejemplo, vamos a modificar nuestro código anterior usando un bloque //try...catch//:
try {
System.out.println("Resultado: " + (dividendo / divisor));
} catch (Exception e) {
System.out.println("Error en la división");
}
Dividendo: 6
Divisor: 0
Error en la división
Como ves, el programa intenta ejecutar el bloque de código encerrado en el //try// y, si salta alguna excepción, se ejecuta el código encerrado en el bloque //catch//.
==== bloque finally ====
Existe otro bloque opcional que podemos añadir al //try...catch//: el bloque **finally**. Este bloque se ejecutará siempre pase lo que pase. Es decir, si el código encerrado en el bloque //try// lanza una excepción o no. Por ejemplo, fíjate como siempre se muestra por pantalla la frase "Este código se ejecutará siempre" del siguiente código:
try {
System.out.println("Resultado: " + (dividendo / divisor));
} catch (Exception e) {
System.out.println("Error en la división");
} finally {
System.out.println("Este código se ejecutará siempre");
}
Dividendo: 6
Divisor: 0
Error en la división
Este código se ejecutará siempre
Dividendo: 6
Divisor: 4
Resultado: 1
Este código se ejecutará siempre
El bloque //finally// es, en ciertas ocasiones, muy útil. Por ejemplo, si abrimos un fichero para operar con él, podemos poner la orden de cerrar el fichero dentro del bloque //finally// para asegurarnos que, pase lo que pase, el fichero se cerrará (o desconectar con un bbdd, por ejemplo).
==== Excepciones checked y unchecked ====
Una de las diferencias fundamentales entre las excepciones //checked// y las //unchecked// es que, si ejecutamos algún código que pueda lanzar una excepción de tipo //checked//, Java nos obliga a encerrarlo entre un bloque //try...catch//, mientras que con las segundas es opcional.
Esto es importante a la hora de lanzar excepciones en nuestros métodos, como veremos más adelante.
===== Tipos de excepción =====
Vamos a volver a nuestro primer ejemplo y ejecutar la aplicación con los siguientes datos:
Dividendo: 6
Divisor: 1.5
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at ejemplos.T12.Main.main(Main.java:14)
En este caso, hemos intentado almacenar en una variable de tipo entero el número 1.5, con lo que Java nos muestra la excepción correspondiente. Si encerramos todo nuestro código en el bloque //try..catch//, veremos como nos muestra la frase "Error en la división" siempre que se capture una excepción (sea del tipo que sea):
try {
System.out.print("Dividendo: ");
dividendo = reader.nextInt();
System.out.print("Divisor: ");
divisor = reader.nextInt();
System.out.println("Resultado: " + (dividendo / divisor));
} catch (Exception e) {
System.out.println("Error en la división");
} finally {
System.out.println("Este código se ejecutará siempre");
}
Dividendo: 6
Divisor: 0
Error en la división
Este código se ejecutará siempre
Dividendo: 6
Divisor: 1.5
Error en la división
Este código se ejecutará siempre
A menudo, nos interesa hacer diferentes cosas según el tipo de excepción que salte. Si te fijas en el bloque //catch//, hay una objeto (//e//) de tipo //Exception// que se le pasa al bloque:
try {
...
} catch (Exception e) {
...
}
La clase //Exception// tiene varios métodos muy útiles (derivados de sus clases antecesoras //Trhowable// y //Object//) que nos pueden servir para tratar las excepciones, entre ellos:
* **getMessage()**: Obtenemos el mensaje asociado a la excepción.
* **getClass()**: Obtenemos la clase de la excepción. Si lo usamos tal cual, la clase será del tipo "class java.lang...". Para obtener el nombre simple de la clase podemos usar el método //getCanonicalName()//.
* **getStackTrace()**: Devuelve la traza de la excepción.
Por ejemplo, vamos a mostrar el mensaje y la clase de nuestro excepción:
try {
System.out.print("Dividendo: ");
dividendo = reader.nextInt();
System.out.print("Divisor: ");
divisor = reader.nextInt();
System.out.println("Resultado: " + (dividendo / divisor));
} catch (Exception e) {
System.out.println(e.getClass().getCanonicalName());
System.out.println(e.getMessage());
System.out.println("Error en la división");
} finally {
System.out.println("Este código se ejecutará siempre");
}
Dividendo: 6
Divisor: 0
java.lang.ArithmeticException
Error en la división
Este código se ejecutará siempre
Dividendo: 6
Divisor: 1.5
java.util.InputMismatchException
null
Error en la división
Este código se ejecutará siempre
De esta forma, podríamos personalizar las acciones a tomar según el tipo de excepción con un simple //if//:
try {
System.out.print("Dividendo: ");
dividendo = reader.nextInt();
System.out.print("Divisor: ");
divisor = reader.nextInt();
System.out.println("Resultado: " + (dividendo / divisor));
} catch (Exception e) {
if (e.getMessage()!= null && e.getMessage().equals("/ by zero")) {
System.out.println("El divisor no puede ser 0");
} else if(e.getClass().getCanonicalName().equals("java.util.InputMismatchException")) {
System.out.println("Ha introducido algún dato erróneo");
} else {
System.out.println("Error desconocido");
}
}
Dividendo: 6
Divisor: 0
El divisor no puede ser 0
Dividendo: 6
Divisor: 1.5
Ha introducido algún dato erróneo
En cualquier caso, existe un método mejor para personalizar nuestras acciones según el tipo de excepción que capturemos: anidar bloques //catch//:
try {
System.out.print("Dividendo: ");
dividendo = reader.nextInt();
System.out.print("Divisor: ");
divisor = reader.nextInt();
System.out.println("Resultado: " + (dividendo / divisor));
} catch (InputMismatchException e) {
System.out.println("Entrada incorrecta");
} catch (ArithmeticException e) {
System.out.println("División por 0");
} catch (Exception e) {
System.out.println("Error en la división");
}
Nuestra aplicación nunca debería hacer un volcado de pila con los datos de la excepción, ya que estamos dando información que puede ser utilizada por un usuario malintencionado. Si te fijas, la última excepción capturada en el bloque anterior es de tipo //Exception// (de cualquier tipo), con lo que nos aseguramos de capturar todas las excepciones posibles y actuar en consecuencia.
El tratamiento de excepciones puede ser muy tedioso. Por suerte, frameworks como //Spring// nos dan herramientas para simplificar la tarea, como veremos en segundo.
===== Lanzar excepciones =====
Además de poder capturar excepciones, también podemos lanzarlas para que sean capturadas en otra parte del programa. Para lanzar una nueva excepción, tenemos que crear un nuevo objeto del tipo de excepción que queramos y lanzarla mediante la palabra reservada **trhow**. Además, podemos añadir un mensaje como descripción de la excepción:
throw new Exception("Mensaje de la excepción");
Por ejemplo, vamos a simular un error y lanzar una excepción dentro de un bloque //try...catch// para poder capturarla:
public class Main {
public static void main(String[] args) {
try {
System.out.println("Ocurre un error");
throw new Exception("Ha ocurrido un error en la aplicación");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
Ocurre un error
Ha ocurrido un error en la aplicación
En este caso, hemos lanzado una excepción de la clase general //Exception//, pero podríamos haber lanzado cualquier tipo de excepción. [[https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Exception.html|Aquí]] puedes ver los tipos de excepciones que tiene Java por defecto.
==== Lanzar excepciones dentro de métodos ====
A menudo, nos es muy útil que algunos métodos de nuestras clases lancen excepciones si ocurre algún error. El procedimiento es el mismo que el anterior, con una pequeña modificación en la cabecera del método si la excepción no es de tipo //unchecked//. Por ejemplo, vamos a crear un método que lo único que hará será mostrar por pantalla un mensaje y lanzar una excepción:
public class Main {
public static void metodoExcepcion(){
System.out.println("Método que lanza una excepción");
throw new Exception("Mensaje de la excepción");
}
public static void main(String[] args) {
metodoExcepcion();
}
}
Si dejamos el código como está, verás que el compilador te muestra un error. Eso es debido a que debemos indicar en la cabecera del método que éste puede lanzar una excepción si ocurre algún tipo de error. Ten en cuenta que estamos lanzando una excepción de tipo genérica, con lo que Java nos obliga a tratarla como si fuera de tipo //checked//, ya que de ella dependen tanto las excepciones //checked// como las //unchecked//. Para eso, solo debemos **trhows tipoExcepcion** en la cabecera del método:
public class Main {
public static void metodoExcepcion() throws Exception{
System.out.println("Método que lanza una excepción");
throw new Exception("Mensaje de la excepción");
}
public static void main(String[] args) {
metodoExcepcion();
}
}
Todavía nos falta algo para que la aplicación funcione. Como hemos dicho, al lanzar una excepción de tipo //checked// (o genérica), debemos tratarla, con lo que tenemos que encerrar el código donde llamamos al método en un bloque //try...catch//:
public class Main {
public static void metodoExcepcion() throws Exception{
System.out.println("Método que lanza una excepción");
throw new Exception("Mensaje de la excepción");
}
public static void main(String[] args) {
try {
metodoExcepcion();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
Si utilizamos excepciones de tipo //unchecked//, no hace falta añadir nada a la cabera del método (aunque podemos seguir haciéndolo) ni encerrar la llamada al método en un bloque //try...catch//. Por ejemplo, vamos a modificar el tipo de excepción a //RuntimeException//, que es de tipo //unchecked//:
public class Main {
public static void metodoExcepcion() {
System.out.println("Método que lanza una excepción");
throw new RuntimeException("Excepción en tiempo de ejecución");
}
public static void main(String[] args) {
metodoExcepcion();
}
}
Como ves, ahora podemos llamar al método de forma normal (sin encerrarlo en un bloque //try...catch//, aunque de esta forma, la salida de la aplicación será un error del compilador de Java, ya que no estamos capturando la posible excepción:
Método que lanza una excepción
Exception in thread "main" java.lang.RuntimeException: Excepción en tiempo de ejecución
at ejemplos.T12_2.Main.metodoExcepcion(Main.java:9)
at ejemplos.T12_2.Main.main(Main.java:27)
Un método puede lanzar diferentes tipos de excepciones. Para indicarlo, debemos añadir la lista de excepciones en la cabecera separada por comas:
public void miMetodo() throws ExceptionInInitializerError, ArithmeticException, RuntimeException ...{
...
}
===== Utilidad de las excepciones =====
Las excepciones son uno de los métodos más útiles para tratar los errores conocidos de nuestra aplicación. Por ejemplo, vamos a hacer una aplicación con una clase principal y otra llamada //Trabajador//. La clase //Trabajador// solo tendrá dos propiedades (nombre y edad), sus //setters// y el método //toString()//. En la clase principal, crearemos un nuevo trabajador, le asignaremos un nombre y edad y lo mostraremos por pantalla:
public class Trabajador {
private String nombre;
private int edad;
public void setNombre(String nombre) {
this.nombre = nombre;
}
public void setEdad(int edad) {
this.edad = edad;
}
@Override
public String toString(){
return "Nombre: " + this.nombre +
"\nEdad: " + this.edad;
}
}
public class Main {
public static void main(String[] args) {
Trabajador trabajador1 = new Trabajador();
trabajador1.setNombre("Pepe");
trabajador1.setEdad(54);
System.out.println(trabajador1.toString());
}
}
Nombre: Pepe
Edad: 54
Como ves, el código es muy sencillo y todo funciona como toca. Vamos a modificar la edad del trabajador:
public static void main(String[] args) {
Trabajador trabajador1 = new Trabajador();
trabajador1.setNombre("Pepe");
trabajador1.setEdad(999);
System.out.println(trabajador1.toString());
}
Nombre: Pepe
Edad: 999
La aplicación sigue funcionando como toca, aunque no parece que tenga mucho sentido tener un trabajador de 999 años (por lo menos, por ahora). Podríamos comprobar la edad (entre 18 y 65 años, por ejemplo) en el método //setEdad()// y devolver un valor que represente el error (por ejemplo, devolvemos -1). Este enfoque, nos obliga a cambiar la cabecera del método (para indicar que ahora devolveremos un entero) y devolver cualquier otro valor (0, por ejemplo) si todo ha ido bien:
public int setEdad(int edad) {
if (edad<18 || edad>65) {
return -1;
}
this.edad = edad;
return 0;
}
En nuestra clase principal, podríamos comprobar el valor devuelto por el método y actuar en consecuencia:
if(trabajador1.setEdad(99) == -1) {
System.out.println("La edad debe estar comprendida entre 18 y 65 años");
}
Aparte del hecho de no tener mucho sentido el que un método //setter// devuelva algo, también tenemos otro problema. ¿Y si tenemos otro método que puede devolver diferentes tipos de errores? En ese caso, nos obligaría a ir añadiendo códigos diferentes de error, con la complejidad que eso añade a nuestro código.
Si usamos excepciones, el código se simplifica, ya que podemos lanzar diferentes tipos en nuestros métodos según el error que queramos capturar:
public void setEdad(int edad) throws Exception {
if(edad < 18) {
throw new Exception("La edad debe ser superior a 18 años");
} else if (edad > 65) {
throw new Exception("La edad no puede ser mayor que 65 años");
}
this.edad = edad;
}
===== Crear excepciones propias =====
Podemos crear nuestras propias excepciones con el código que queramos. Para eso, lo normal es crear una clase que herede de la clase //Exception//:
public class ExceptionEdad extends Exception{
private int code;
public ExceptionEdad(String message, int code) {
super(message);
this.code = code;
}
public int getCode(int code) {
return this.code;
}
}
En este caso, hemos creado una clase //ExceptionEdad// para comprobar la edad de los trabajadores. Además del mensaje de la excepción, añadimos otra propiedad //code// para asignarle un código diferente a cada excepción (fíjate que en el constructor de la clase, usamos //super(message)// para llamar al constructor de la clase //Exception//). Ahora, ya podemos usar la nueva clase para lanzar excepciones de ese tipo:
public void setEdad(int edad) throws ExceptionEdad {
if(edad < 18) {
throw new ExceptionEdad("La edad debe ser superior a 18 años", 1);
} else if (edad > 65) {
throw new ExceptionEdad("La edad no puede ser mayor que 65 años", 2);
}
this.edad = edad;
}
Aunque la excepción que hemos creado es sencilla (sólo añadimos el mensaje de error), podríamos añadir toda la complejidad que queramos. Por ejemplo, podríamos tener sólo el código de la excepción (sin el mensaje) y almacenar los mensajes en un fichero aparte en diferentes idiomas. A la hora de mostrar el mensaje de la excepción, podríamos mirar el código y mostrarlo en el idioma en el que esté configurada nuestra aplicación.
===== Dónde tratar las excepciones =====
Aunque podemos tratar las excepciones en cualquier lugar, lo mejor es tratarlas en las clases más generales (en nuestro caso, en la clase //Main//). El resto de métodos de las demás clases, solo se deberían dedicar a lanzar excepciones si ocurre algún error controlable.
¿Qué pasa con las excepciones //checked//? Como hemos dicho, Java nos obliga a tratar esas excepciones cuando utilizamos algún método que pueda lanzar una excepción de ese tipo. Por ejemplo, a la hora de leer un fichero con algunas clases de Java (//FileReader//, //BufferReader//...) debemos encerrar ese código en un bloque //try...catch//. Si hacemos, por ejemplo, una clase para leer ficheros y usamos esas clases, en sus métodos estaremos capturando las excepciones de forma obligatoria.
Una forma común de solucionar ese problema para poder tratar las excepciones en las clases //superiores//, es propagar esa excepción hacia arriba:
public class Fichero {
public void read(String path) throws Exception {
try {
File file = new File(path);
FileReader fileReader = new FileReader(file);
} catch (Exception e) {
throw new Exception("No se puede leer el fichero", e);
}
}
}
Fíjate que a la hora de crear la nueva excepción, usamos un constructor con un mensaje y la excepción que hemos capturado (recuerda que una clase puede tener varios constructores con diferentes parámetros). Ésto lo hacemos para no perder la traza de la excepción original.
Uno de los problemas de las excepciones en Java es que muchas //API// lanzan excepciones //checked// cuando deberían ser //unchecked//. Una posible solución es transformar esa excepción //checked// en un //checked// cuando la propagamos hacia los métodos superiores:
public class Fichero {
public void read(String path) throws RuntimeException {
try {
File file = new File(path);
FileReader fileReader = new FileReader(file);
} catch (Exception e) {
throw new RuntimeException("No se puede leer el fichero", e);
}
}
}
El código anterior es a modo de ejemplo. Ten en cuenta que las excepciones //checked// deberían usarse cuando el programa no tiene ningún tipo de error, pero ocurre una situación excepcional. En el ejemplo anterior, si, por ejemplo, no existe el fichero, no quiere decir que el programa tenga errores (se puede haber borrado fuera de la aplicación, con lo que no es responsabilidad del programa), con lo que sí que deberíamos lanzar una excepción //checked//.
Si quieres saber más sobre el uso correcto de las excepciones en Java, puedes mirar este [[http://www.cursohibernate.es/doku.php?id=patrones:excepciones|enlace]] a una web de un curso de //Hibernate// de Lorenzo González donde está mucho más detallado el tema (el curso es muy completo, pero el nivel es bastante avanzado, con lo que deberías echarle un vistazo cuando tengas más conocimiento sobre Java).
===== try-with-resources =====
Antes hemos visto como podemos usar el bloque //finally// para, por ejemplo, cerrar un recurso como //BufferedReader// o //FileReader//. A partir de Java 7, se añadió la sentencia //try-with-resources// para cerrar automáticamente los recursos.
Por ejemplo, si queremos leer las líneas de un archivo (el tema de ficheros lo veremos en el punto siguiente), una de las forma convencional de hacerlo antes de Java 7 era:
public static void main(String[] args) {
String path = "path_archivo";
String line;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
while((line = br.readLine()) != null){
System.out.println(line);
}
} catch (Exception e) {
e.getStackTrace();
} finally {
if (br != null)
try {
br.close();
} catch (Exception e) {
e.getStackTrace();
}
}
}
Tenemos que añadir el bloque //finally// para asegurarnos de cerrar el recurso (lo que puede dar otra excepción, de ahí el nuevo bloe //try//).
A partir de Java 7, podemos aprovechar la nueva construcción para simplificar el código:
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
while((line = br.readLine()) != null){
System.out.println(line);
}
} catch (Exception e) {
e.getStackTrace();
}
Como ves, el código queda mucho más limpio, ya que el bloque //try-with-resources// ya se encarga de cerrar automáticamente los recursos.
===== Ejercicios =====
** Ejercicio 1a **
Haz una aplicación que pida un número por pantalla y muestre el resultado de multiplicarlo por 2. Si ocurre alguna excepción (el número introducido no tiene el formato correcto, por ejemplo) la aplicación deberá mostrar el mensaje de error "Algo ha salido mal".
** Ejercicio 1b **
Modifica la aplicación anterior para que se cierre siempre el objeto de tipo //Scanner// pase lo que pase.
** Ejercicio 2a **
Escribe una aplicación que pida al usuario su edad por pantalla. Crea un método que devuelva //true// o //false// según la edad sea mayor de 18 o menor. Muestra por pantalla la frase "Puedes pasar" o "Tienes que ser mayor de edad para pasar" según el resultado del método (sin utilizar excepciones). Si ocurre alguna excepción, la aplicación mostrará el mensaje "Algo ha salido mal".
** Ejercicio 2b **
Modifica la aplicación anterior para que el método que comprueba la edad lance una excepción de tipo //Exception// si la edad es menor de 18 años. En el método //main()//, Si ocurre alguna excepción (del tipo que sea), la aplicación mostrará el mensaje "Tienes que ser mayor de edad para pasar".
** Ejercicio 2c **
Elimina el bloque //try..catch// del método //main()//. ¿Por qué el compilador muestra un error? ¿Cómo lo puedes solucionar?
** Ejercicio 2d **
Cambia el tipo de excepción que devuelve el método que comprueba la edad a //RuntimeException// y vuelve a eliminar el bloque //try..catch// del método //main()//. ¿Por qué ahora el compilador sí que te deja ejecutar el programa?
** Ejercicio 2e **
Vuelve a capturar las excepciones en el //main()// (deja la excepción del método que comprueba la edad como //RuntimeException//) y muestra la frase "Puedes pasar" si la edad es mayor que 18 años, "Tienes que ser mayor de edad para pasar" si la edad es inferior a 18 años o "La edad tiene que ser un entero" si se introduce algo diferente a un entero.
** Ejercicio 3a **
Modifica la aplicación anterior para que se muestre la frase "No puedes pasar" si la edad introducida es menor de 18 años o mayor que 65. Para eso, lanza una excepción de tipo //RuntimeException// en el método que comprueba la edad si se cumple alguna de esas condiciones.
** Ejercicio 3b **
Modifica la aplicación anterior para que el mensaje de la excepción sea "Tienes que ser mayor de edad para pasar" o "Tienes que tener menos de 65 años para pasar" según el tipo de error. Utiliza el método correspondiente de la clase //Exception// para mostrar ese mensaje de error.
** Ejercicio 3c **
Crea dos excepciones de tipo //Exception// dentro de un nuevo //package// llamado //exception//. La primera será //AgeLowerException// y su mensaje será "Tienes que ser mayor de edad para pasar". La segunda será //AgeTopException// y su mensaje "Tienes que tener menos de 65 años para pasar".
Haz que el método que comprueba la edad en la aplicación principal lance esos tipos de excepciones según el caso.
Captura en la aplicación principal ambos tipos de excepciones y si el usuario introduce un dato incorrecto.
** Ejercicio 3d **
Modifica la aplicación anterior para que si ocurre alguna excepción no prevista se muestre el mensaje "Ha ocurrido un error inesperado".
** Ejercicio 3e **
Cambia el tipo de excepciones creadas anteriormente a //RuntimeException//. ¿Puedes cambiar algo en la clase principal?
** Ejercicio 4 **
Crea una aplicación mediante la arquitectura por capas que estamos viendo en este curso. La aplicación operará con dos recursos: libros y autores.
Listado de libros:
^ id ^ Título ^ id del autor ^
| 1 | El nombre de la rosa | 1 |
| 2 | El señor de los anillos | 2 |
| 3 | El hobbit | 2 |
| 4 | La fundación | 3 |
Listado de autores:
^ id ^ Nombre ^
| 1 | Umberto Eco |
| 2 | J. R. R. Tolkien |
| 3 | Isaac Asimov |
| 4 | Alejandro Dumas |
Desde nuestra clase principal (//App//) se llamarán a los controladores correspondientes para ejecutar las siguientes acciones:
* Mostrar el listado de libros
* Mostrar un libro por id
* Añadir un libro
* Mostrar listado de libros de un autor según su id
* Mostrar el listado de autores
* Borrar un autor
A la hora de ejecutar las acciones tienes que tener en cuenta las siguientes restricciones:
* Si no se encuentra el recurso (libro o autor) deberá mostrar el correspondiente mensaje de error
* Al añadir un libro, si el id del autor pasado no es válido (no existe), deberá mostrar el mismo mensaje de error anterior ("Autor no encontrado" o similar)
* Al mostrar el listado de libros, deberá comprobar que el id del autor existe (y, si no es así, mostrará el mensaje de error correspondiente)
* Cuando se borre un autor deberá comprobar dos cosas: que el id del autor existe y que no tenga libros asociados. Si tiene algún libro asociado, deberá mostrar un error indicando primero que elimine sus libros
Haz toda la gestión de errores mediante excepciones en los métodos que creas convenientes.