El lenguaje de programación Java, como muchos otros, incluye herramientas integradas para trabajar con errores, es decir, situaciones excepcionales (excepciones) en las que una falla del programa se gestiona mediante un código especial separado del algoritmo principal.
Gracias a las excepciones, un desarrollador puede anticipar los puntos débiles del código y prevenir errores fatales en tiempo de ejecución.
Por lo tanto, manejar las excepciones en Java es una buena práctica que mejora la confiabilidad general del código.
El propósito de este artículo es explorar los principios de la captura y el manejo de excepciones, así como revisar las estructuras sintácticas correspondientes del lenguaje destinadas a este fin.
Todos los ejemplos de esta guía se ejecutaron en Ubuntu 22.04, instalado en un servidor en la nube de Hostman.
Los ejemplos mostrados en esta guía se ejecutaron usando OpenJDK. Su instalación es sencilla.
Primero, actualice la lista de repositorios disponibles:
sudo apt update
A continuación, solicite la lista de versiones de OpenJDK disponibles para descarga:
sudo apt search openjdk | grep -E 'openjdk-.*-jdk/'
Verá una lista corta en la terminal:
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
openjdk-11-jdk/jammy-updates,jammy-security 11.0.25+9-1ubuntu1~22.04 amd64
openjdk-17-jdk/jammy-updates,jammy-security 17.0.13+11-2ubuntu1~22.04 amd64
openjdk-18-jdk/jammy-updates,jammy-security 18.0.2+9-2~22.04 amd64
openjdk-19-jdk/jammy-updates,jammy-security 19.0.2+7-0ubuntu3~22.04 amd64
openjdk-21-jdk/jammy-updates,jammy-security,now 21.0.5+11-1ubuntu1~22.04 amd64 [installed]
openjdk-8-jdk/jammy-updates,jammy-security 8u432-ga~us1-0ubuntu2~22.04 amd64
Usaremos la versión openjdk-21-jdk:
sudo apt install openjdk-21-jdk
Luego puede verificar que Java se haya instalado correctamente consultando su versión:
java --version
La salida de la terminal será algo similar a esto:
openjdk 21.0.5 2024-10-15
OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu122.04)
OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu122.04, mixed mode, sharing)
Como se muestra, la versión exacta de OpenJDK es 21.0.5.
Todos los ejemplos de esta guía deben guardarse en un archivo separado con la extensión .java:
nano App.java
Luego, rellene el archivo creado con un código de ejemplo como este:
class App {
public static void main(String[] args) {
System.out.println("This text is printed to the console");
}
}
Tenga en cuenta que el nombre de la clase debe coincidir con el nombre del archivo.
A continuación, compile el archivo:
javac App.java
Y ejecútelo:
java App
La terminal mostrará lo siguiente:
This text is printed to the console
Todas las excepciones en Java tienen un tipo específico asociado con la razón por la cual ocurrió la excepción, es decir, el tipo particular de fallo del programa.
Existen dos tipos fundamentales de excepciones:
El tipo Error solo se considera parcialmente una excepción: es un error completo que inevitablemente provoca el fallo del programa.
Las excepciones que pueden manejarse mediante código personalizado y permiten que el programa continúe ejecutándose son las Checked Exceptions y las Unchecked Exceptions.
Por lo tanto, los errores y las excepciones en Java son entidades diferentes. Sin embargo, tanto los Errors como las Exceptions (Checked y Unchecked) son tipos con subtipos adicionales que aclaran la razón del fallo.
Aquí hay un ejemplo de código que genera una excepción en tiempo de compilación:
import java.io.File;
import java.util.Scanner;
public class App {
public static void main(String[] args) {
File someFile = new File("someFile.txt"); // create file reference
Scanner scanner = new Scanner(someFile); // parse file contents
}
}
La compilación se interrumpirá y verá el siguiente error en la terminal:
App.java:7: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
Scanner scanner = new Scanner(someFile);
^
1 error
Si captura y maneja esta excepción, el código se compilará y podrá ejecutarse correctamente.
Aquí hay otro ejemplo de código que genera una excepción solo en tiempo de ejecución:
class App {
public static void main(String[] args) {
int[] someArray = {1, 2, 3, 4, 5}; // create an array with 5 elements
System.out.println(someArray[10]); // attempt to access a non-existent element
}
}
No ocurrirá ninguna excepción durante la compilación, pero después de ejecutar el código compilado, verá este error en la terminal:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
at app.main(app.java:4)
Esto significa que dicha excepción puede manejarse mediante código definido por el usuario, permitiendo que el programa continúe su ejecución.
Finalmente, aquí hay un ejemplo de código que provoca un error en tiempo de ejecución:
public class App {
static int i = 0;
public static int showSomething(int x) {
i = i + 2;
return i + showSomething(i + 2);
}
public static void main(String[] args) {
App.showSomething(i); // trigger stack overflow
}
}
La compilación será exitosa, pero durante la ejecución, la terminal mostrará un StackOverflowError:
Exception in thread "main" java.lang.StackOverflowError
at java.base/java.io.BufferedOutputStream.implWrite(BufferedOutputStream.java:220)
at java.base/java.io.BufferedOutputStream.write(BufferedOutputStream.java:200)
at java.base/java.io.PrintStream.implWrite(PrintStream.java:643)
En este caso, el error no puede manejarse; solo puede corregirse en el código.
Internamente, todas las excepciones (y errores) en Java están representadas como un conjunto de clases, algunas de las cuales heredan de otras.
La clase base para todos los errores y excepciones es Throwable. De ella heredan dos clases más: Error y Exception, que sirven como clases base para una amplia gama de subclases asociadas con tipos específicos de excepciones.
La clase Error describe errores del tipo error, como se mencionó anteriormente, mientras que la clase Exception describe las excepciones verificadas.
Además, la clase RuntimeException hereda de Exception y describe las excepciones no verificadas.
Una jerarquía simplificada de clases de excepciones en Java puede representarse de la siguiente manera:
Cada clase de excepción incluye métodos para obtener información adicional sobre el fallo.
Puede encontrar la clasificación completa de excepciones de Java, incluidas las de paquetes adicionales, en una guía de referencia dedicada.
Todas las excepciones se manejan mediante bloques especiales try y catch, que son estándar en la mayoría de los lenguajes de programación, incluido Java.
Dentro del bloque try, se escribe el código que puede contener un error capaz de generar una excepción.
Dentro del bloque catch, se escribe el código que maneja la excepción que ocurrió en el bloque try definido previamente.
Por ejemplo, una estructura try-catch puede verse así:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
System.out.println("Who said you can’t divide by zero?");
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
}
}
}
La salida de este código en la consola será:
Actually, you can't divide by zero...
Este ejemplo se basa en una división ilegal entre cero, que está envuelta en un bloque try y lanza una ArithmeticException.
En el bloque catch, esta excepción se maneja imprimiendo un mensaje de error en la consola.
Gracias a esta estructura, el programa puede continuar ejecutándose incluso después del error durante la división por cero.
A diferencia de muchos otros lenguajes de programación, Java incluye un bloque especial finally como parte del mecanismo de manejo de excepciones. Este siempre se ejecuta, ocurra o no una excepción.
Podemos extender la estructura mostrada anteriormente:
public class App {
public static void main(String[] args) {
try {
// code that might throw an exception
int someVariable = 5 / 0;
} catch (ArithmeticException someException) {
// code that handles the exception
System.out.println("Actually, you can't divide by zero...");
} finally {
// code that always executes
System.out.println("Who cares if you can divide by zero or not? This message will appear anyway!");
}
}
}
Después de ejecutar este código, la consola mostrará:
Actually, you can't divide by zero...
Who cares if you can divide by zero or not? This message will appear anyway!
Para comprender la necesidad práctica del bloque finally, considere el siguiente ejemplo:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
}
// a function that hides the loading indicator
hideLoaderUI();
En un programa con esta estructura, la función hideLoaderUI() nunca se ejecutará si ocurre una excepción.
En este caso, podría intentar llamar hideLoaderUI() dentro del manejador de excepciones y también después si no ocurre ninguna excepción:
try {
parseJson(response.json);
} catch (JSONException someException) {
hideLoaderUI(); // duplicate
System.out.println("Looks like there’s something wrong with the JSON...");
}
hideLoaderUI(); // duplicate
Sin embargo, esto da lugar a una duplicación innecesaria de la llamada de la función. Además, en lugar de una función, podría ser un bloque completo de código, y duplicarlo se considera una mala práctica.
Por lo tanto, para garantizar la ejecución de hideLoaderUI() sin duplicar la llamada, puede usar un bloque finally:
try {
parseJson(response.json);
} catch (JSONException someException) {
System.out.println("Looks like there’s something wrong with the JSON...");
} finally {
// the loading indicator will be hidden in any case
hideLoaderUI();
}
Java permite crear (lanzar) excepciones manualmente mediante el operador especial throw:
public class App {
public static void main(String[] args) {
throw new Exception("Something strange seems to have happened...");
}
}
Incluso puede crear una variable para la excepción por adelantado y luego lanzarla:
public class App {
public static void main(String[] args) {
var someException = new Exception("Something strange seems to have happened...");
throw someException;
}
}
Otra palabra clave importante, throws (con “s” al final), permite declarar explícitamente los tipos de excepciones (en forma de nombres de clases) que un método puede lanzar.
Si dicho método lanza una excepción, esta se propagará al código que lo llamó, el cual deberá manejarla:
public class App {
public static void someMethod() throws ArithmeticException, NullPointerException, InterruptedException {
int someVariable = 5 / 0;
}
public static void main(String[] args) {
try {
App.someMethod();
} catch (Exception someException) {
System.out.println("Dividing by zero again? Do you even know what insanity is?");
}
}
}
La salida de la consola será:
Dividing by zero again? Do you even know what insanity is?
La estructura jerárquica de las excepciones permite naturalmente la creación de clases de excepciones personalizadas que heredan de las clases base.
Gracias a las excepciones personalizadas, Java permite implementar rutas de manejo de errores específicas de la aplicación.
Por lo tanto, además de las excepciones estándar de Java, puede agregar las suyas propias.
Cada excepción personalizada, como cualquier excepción predefinida, puede manejarse utilizando los bloques estándar try-catch-finally:
class MyOwnException extends Exception {
public MyOwnException(String message) {
super(message); // calls the parent class constructor
System.out.println("Warning! An exception is about to be thrown!");
}
}
public class App {
public static void main(String[] args) {
try {
throw new MyOwnException("Just an exception. No explanation. Anyone got a problem?");
} catch (MyOwnException someException) {
System.out.println(someException.getMessage());
}
}
}
Salida de consola:
Warning! An exception is about to be thrown!
Just an exception. No explanation. Anyone got a problem?
Este tutorial ha demostrado, mediante ejemplos, por qué se necesitan excepciones en Java, cómo surgen (incluyendo cómo lanzar una manualmente) y cómo manejarlas utilizando las herramientas del lenguaje.
Las excepciones que pueden capturarse y manejarse vienen en dos tipos:
Checked Exceptions: manejadas en tiempo de compilación.
Unchecked Exceptions: manejadas en tiempo de ejecución.
Además, existen errores fatales que solo pueden resolverse reescribiendo el código:
Errors: no pueden manejarse.
Hay varias estructuras sintácticas (bloques) utilizadas para manejar excepciones:
try: código que puede lanzar una excepción.
catch: código que maneja la posible excepción.
finally: código que se ejecuta sin importar si ocurre o no una excepción.
Así como palabras clave para controlar el proceso de lanzamiento de excepciones:
throw: lanza una excepción manualmente.
throws: enumera las posibles excepciones en un método declarado.
Puede encontrar la lista completa de métodos de la clase padre Exception en la documentación oficial de Oracle.