Manejo de Excepciones en Java

Introducción

Una excepción es un evento de error que puede ocurrir durante la ejecución de un programa y perturbar su flujo normal. Java proporciona una forma robusta y orientada a objetos de manejar escenarios de excepción conocida como Manejo de Excepciones en Java.

Las excepciones en Java pueden surgir de diferentes tipos de situaciones, como datos incorrectos ingresados por el usuario, falla de hardware, falla de conexión de red o un servidor de base de datos que está caído. El código que especifica qué hacer en escenarios de excepción específicos se llama manejo de excepciones.

Lanzamiento y Captura de Excepciones

Java crea un objeto de excepción cuando ocurre un error durante la ejecución de una declaración. El objeto de excepción contiene mucha información de depuración, como la jerarquía de métodos, el número de línea donde ocurrió la excepción y el tipo de excepción.

Si ocurre una excepción en un método, el proceso de crear el objeto de excepción y entregarlo al entorno de ejecución se llama “lanzar la excepción”. El flujo normal del programa se detiene y el Entorno de Ejecución de Java (JRE) intenta encontrar el controlador para la excepción. El controlador de excepciones es el bloque de código que puede procesar el objeto de excepción.

  • La lógica para encontrar el controlador de excepciones comienza con la búsqueda en el método donde ocurrió el error.
  • Si no se encuentra un controlador apropiado, se moverá al método llamador.
  • Y así sucesivamente.

Entonces, si la pila de llamadas del método es A->B->C y se produce una excepción en el método C, la búsqueda del controlador apropiado se moverá de C->B->A.

Si se encuentra un controlador de excepciones adecuado, se pasa el objeto de excepción al controlador para procesarlo. Se dice que el controlador está “capturando la excepción”. Si no hay un controlador de excepciones adecuado, el programa termina e imprime información sobre la excepción en la consola.

El marco de manejo de excepciones de Java se utiliza para manejar solo errores en tiempo de ejecución. Los errores en tiempo de compilación deben corregirse por el desarrollador que escribe el código, de lo contrario, el programa no se ejecutará.

Palabras clave de manejo de excepciones en Java

Java proporciona palabras clave específicas para fines de manejo de excepciones.

  1. throw – Sabemos que si ocurre un error, se crea un objeto de excepción y luego Java inicia el procesamiento para manejarlo. A veces, es posible que deseemos generar excepciones explícitamente en nuestro código. Por ejemplo, en un programa de autenticación de usuario, deberíamos lanzar excepciones a los clientes si la contraseña es null. La palabra clave throw se utiliza para lanzar excepciones al tiempo de ejecución de Java para manejarlas.
  2. throws – Cuando estamos lanzando una excepción en un método y no la estamos manejando, entonces tenemos que usar la palabra clave throws en la firma del método para que el programa que llama sepa las excepciones que podrían ser lanzadas por el método. El método que llama podría manejar estas excepciones o propagarlas a su método llamador utilizando la palabra clave throws. Podemos proporcionar múltiples excepciones en la cláusula throws, y también se puede usar con el método main().
  3. try-catch – Usamos el bloque try-catch para el manejo de excepciones en nuestro código. try es el inicio del bloque y catch está al final del bloque try para manejar las excepciones. Podemos tener múltiples bloques catch con un bloque try. El bloque try-catch también puede estar anidado. El bloque catch requiere un parámetro que debe ser del tipo Exception.
  4. finalmente – el bloque finally es opcional y se puede utilizar solo con un bloque try-catch. Dado que una excepción detiene el proceso de ejecución, es posible que tengamos algunos recursos abiertos que no se cerrarán, por lo que podemos usar el bloque finally. El bloque finally siempre se ejecuta, ya sea que haya ocurrido una excepción o no.

Ejemplo de Manejo de Excepciones

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • El método testException() está lanzando excepciones usando la palabra clave throw. La firma del método utiliza la palabra clave throws para informar al llamador sobre el tipo de excepciones que podría lanzar.
  • En el método main(), estoy manejando excepciones usando el bloque try-catch en el método main(). Cuando no las estoy manejando, las propago a tiempo de ejecución con la cláusula throws en el método main().
  • El método testException(-10) nunca se ejecuta debido a la excepción y luego se ejecuta el bloque finally.

El método printStackTrace() es uno de los métodos útiles en la clase Exception con fines de depuración.

Este código producirá la siguiente salida:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

Algunos puntos importantes a tener en cuenta:

  • No podemos tener una cláusula catch o finally sin una instrucción try.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • No podemos escribir ningún código entre bloques try-catch-finally.
  • Podemos tener varios bloques catch con una única instrucción try.
  • Los bloques try-catch pueden estar anidados de manera similar a las declaraciones if-else.
  • Puede haber solo un bloque finally con una instrucción try-catch.

Jerarquía de Excepciones en Java

Como se mencionó anteriormente, cuando se genera una excepción, se crea un objeto de excepción. Las excepciones de Java son jerárquicas y se utiliza la herencia para categorizar diferentes tipos de excepciones. Throwable es la clase principal de la jerarquía de excepciones de Java y tiene dos objetos secundarios: Error y Exception. Las Exceptions se dividen además en Exceptions Verificadas y Exceptions en Tiempo de Ejecución.

  1. Errores: Los Errors son escenarios excepcionales que están fuera del alcance de la aplicación y no es posible anticipar ni recuperarse de ellos. Por ejemplo, falla de hardware, bloqueo de la máquina virtual de Java (JVM) o error de falta de memoria. Es por eso que tenemos una jerarquía separada de Errors y no deberíamos intentar manejar estas situaciones. Algunos de los Errors comunes son OutOfMemoryError y StackOverflowError.
  2. Excepciones Verificadas: Las Exceptions verificadas son escenarios excepcionales que podemos anticipar en un programa e intentar recuperarnos de ello. Por ejemplo, FileNotFoundException. Deberíamos capturar esta excepción y proporcionar un mensaje útil al usuario y registrarla adecuadamente para propósitos de depuración. La clase Exception es la clase principal de todas las excepciones verificadas. Si estamos lanzando una excepción verificada, debemos catchearla en el mismo método, o debemos propagarla al llamador usando la palabra clave throws.
  3. Excepción en Tiempo de Ejecución: Las excepciones en tiempo de ejecución son causadas por una mala programación. Por ejemplo, intentar recuperar un elemento de un array. Deberíamos verificar primero la longitud del array antes de intentar recuperar el elemento, de lo contrario podría lanzar ArrayIndexOutOfBoundException en tiempo de ejecución. RuntimeException es la clase principal de todas las excepciones en tiempo de ejecución. Si estamos throweando alguna excepción en tiempo de ejecución en un método, no es necesario especificarlas en la cláusula de firma del método throws. Las excepciones en tiempo de ejecución pueden evitarse con una mejor programación.

Algunos métodos útiles de las Clases de Excepción

Java \code{Exception} y todas sus subclases no proporcionan ningún método específico, y todos los métodos están definidos en la clase base – \code{Throwable}. Las clases \code{Exception} se crean para especificar diferentes tipos de escenarios de \code{Exception} para que podamos identificar fácilmente la causa raíz y manejar la \code{Exception} según su tipo. La clase \code{Throwable} implementa la interfaz \code{Serializable} para interoperabilidad.

Algunos de los métodos útiles de la clase \code{Throwable} son:

  1. public String getMessage() – Este método devuelve el mensaje \code{String} de \code{Throwable} y el mensaje puede proporcionarse mientras se crea la excepción a través de su constructor.
  2. public String getLocalizedMessage() – Este método se proporciona para que las subclases puedan anularlo para proporcionar un mensaje específico de la ubicación al programa que llama. La implementación de la clase \code{Throwable} de este método utiliza el método \code{getMessage()} para devolver el mensaje de excepción.
  3. public synchronized Throwable getCause() – Este método devuelve la causa de la excepción o \code{null} si la causa es desconocida.
  4. public String toString() – Este método devuelve la información sobre \code{Throwable} en formato \code{String}, el \code{String} devuelto contiene el nombre de la clase \code{Throwable} y el mensaje localizado.
  5. public void printStackTrace() – Este método imprime la información de la traza de la pila en el flujo de error estándar, este método está sobrecargado, y podemos pasar PrintStream o PrintWriter como argumento para escribir la información de la traza de la pila en el archivo o flujo.

Mejoras en la Administración Automática de Recursos y en el Bloque Catch de Java 7

Si estás catcheando muchas excepciones en un solo bloque try, notarás que el código del bloque catch consiste principalmente en código redundante para registrar el error. En Java 7, una de las características fue un bloque catch mejorado donde podemos capturar múltiples excepciones en un solo bloque catch. Aquí tienes un ejemplo del bloque catch con esta característica:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

Existen algunas limitaciones, como que el objeto de excepción es final y no podemos modificarlo dentro del bloque catch, lee el análisis completo en Mejoras en el Bloque Catch de Java 7.

La mayor parte del tiempo, utilizamos el bloque finally solo para cerrar los recursos. A veces olvidamos cerrarlos y obtenemos excepciones en tiempo de ejecución cuando los recursos se agotan. Estas excepciones son difíciles de depurar, y puede ser necesario revisar cada lugar donde estamos utilizando ese recurso para asegurarnos de cerrarlo. En Java 7, una de las mejoras fue try-with-resources, donde podemos crear un recurso en la propia declaración try y usarlo dentro del bloque try-catch. Cuando la ejecución sale del bloque try-catch, el entorno de ejecución cierra automáticamente estos recursos. Aquí tienes un ejemplo del bloque try-catch con esta mejora:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

Java proporciona muchas clases de excepciones para que las utilicemos, pero a veces es necesario crear nuestras propias clases de excepciones personalizadas. Por ejemplo, para notificar al llamador acerca de un tipo específico de excepción con el mensaje adecuado. Podemos tener campos personalizados para realizar un seguimiento, como códigos de error. Por ejemplo, supongamos que escribimos un método para procesar solo archivos de texto, de manera que podamos proporcionar al llamador el código de error apropiado cuando se envía otro tipo de archivo como entrada.

Primero, creamos MyException:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

Luego, creamos un CustomExceptionExample:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

Podemos tener un método separado para procesar diferentes tipos de códigos de error que obtenemos de diferentes métodos. Algunos de ellos se consumen porque es posible que no queramos notificar al usuario, o algunos de ellos los lanzaremos para notificar al usuario del problema.

Aquí estoy extendiendo Exception para que cada vez que esta excepción se produzca, tenga que ser manejada en el método o devuelta al programa llamador. Si extendemos RuntimeException, no es necesario especificarlo en la cláusula throws.

Esta fue una decisión de diseño. El uso de Excepciones Verificadas tiene la ventaja de ayudar a los desarrolladores a comprender qué excepciones pueden esperar y tomar medidas apropiadas para manejarlas.

Mejores Prácticas para el Manejo de Excepciones en Java

  • Usar Excepciones Específicas – Las clases base de la jerarquía de Exception no proporcionan información útil, por eso Java tiene tantas clases de excepción, como IOException con subclases adicionales como FileNotFoundException, EOFException, etc. Siempre debemos lanzar y capturar clases de excepción específicas para que el llamador pueda conocer fácilmente la causa raíz de la excepción y procesarlas. Esto hace que la depuración sea más fácil y ayuda a las aplicaciones cliente a manejar excepciones adecuadamente.
  • Lanzar Temprano o Fallar Rápido – Deberíamos intentar lanzar excepciones lo más temprano posible. Considera el método processFile() anterior, si pasamos el argumento null a este método, obtendremos la siguiente excepción:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

Mientras depuramos, tendremos que observar cuidadosamente el rastro de la pila para identificar la ubicación real de la excepción. Si cambiamos nuestra lógica de implementación para verificar estas excepciones temprano como se muestra a continuación:

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... procesamiento adicional
}

Entonces, el rastro de la pila de la excepción indicará dónde ha ocurrido la excepción con un mensaje claro:

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • Capturar Tarde – Dado que Java obliga a manejar la excepción comprobada o declararla en la firma del método, a veces los desarrolladores tienden a capturar la excepción y registrar el error. Pero esta práctica es perjudicial porque el programa llamador no recibe ninguna notificación de la excepción. Deberíamos capturar excepciones solo cuando podamos manejarlas adecuadamente. Por ejemplo, en el método anterior, estoy lanzando excepciones de vuelta al método llamador para manejarlas. El mismo método podría ser utilizado por otras aplicaciones que podrían querer procesar la excepción de manera diferente. Mientras implementamos cualquier característica, siempre debemos lanzar excepciones de vuelta al llamador y dejar que ellos decidan cómo manejarla.
  • Cierre de Recursos – Dado que las excepciones detienen el procesamiento del programa, deberíamos cerrar todos los recursos en el bloque finally o usar la mejora de Java 7 try-with-resources para permitir que Java cierre los recursos por ti.
  • Registro de Excepciones – Siempre debemos registrar mensajes de excepción y al lanzar excepciones proporcionar un mensaje claro para que el llamante sepa fácilmente por qué ocurrió la excepción. Siempre debemos evitar un bloque catch vacío que simplemente consuma la excepción y no proporcione ningún detalle significativo de la excepción para la depuración.
  • Bloque único de captura para múltiples excepciones – La mayoría de las veces registramos los detalles de la excepción y proporcionamos un mensaje al usuario, en este caso, deberíamos usar la característica de Java 7 para manejar múltiples excepciones en un solo bloque catch. Este enfoque reducirá el tamaño de nuestro código y también se verá más limpio.
  • Uso de Excepciones Personalizadas – Siempre es mejor definir una estrategia de manejo de excepciones en el diseño y en lugar de lanzar y capturar múltiples excepciones, podemos crear una excepción personalizada con un código de error, y el programa llamante puede manejar estos códigos de error. También es una buena idea crear un método de utilidad para procesar diferentes códigos de error y usarlos.
  • Convenciones de Nomenclatura y Empaquetado – Cuando crees tu excepción personalizada, asegúrate de que termine con Exception para que quede claro desde el nombre mismo que es una clase de excepción. Además, asegúrate de empaquetarlas como se hace en el Kit de Desarrollo de Java (JDK). Por ejemplo, IOException es la excepción base para todas las operaciones de IO.
  • Usa las Excepciones con Moderación – Las excepciones son costosas, y a veces no es necesario lanzar excepciones en absoluto, podemos devolver una variable booleana al programa que llama para indicar si una operación fue exitosa o no. Esto es útil cuando la operación es opcional y no quieres que tu programa se bloquee porque falla. Por ejemplo, al actualizar las cotizaciones de acciones en la base de datos desde un servicio web de terceros, podemos querer evitar lanzar excepciones si la conexión falla.
  • Documenta las Excepciones Lanzadas – Usa Javadoc @throws para especificar claramente las excepciones lanzadas por el método. Es muy útil cuando estás proporcionando una interfaz para que otras aplicaciones la utilicen.

Conclusión

En este artículo, aprendiste sobre el manejo de excepciones en Java. Aprendiste sobre throw y throws. También aprendiste sobre bloques try (y try-with-resources), catch, y finally.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java