Visión general
Contenido
- 1 Visión general
- 2 ¿Qué es el manejo de excepciones?
- 3 ¿Por qué utilizar el manejo de excepciones?
- 4 Jerarquía de excepciones
- 5 Cómo manejar las excepciones
- 6 Cómo lanzar excepciones
- 7 Mejores y peores prácticas de manejo de excepciones
- 8 Mejores prácticas de manejo de excepciones
- 9 Peores prácticas de manejo de excepciones
- 10 Conclusión
Manejar excepciones en Java es una de las cosas más básicas y fundamentales que un desarrollador debe saber de memoria. Lamentablemente, esto a menudo se pasa por alto y se subestima la importancia del manejo de excepciones; es tan importante como el resto del código.
En este artículo, repasemos todo lo que necesita saber sobre el manejo de excepciones en Java, así como las buenas y malas prácticas.
¿Qué es el manejo de excepciones?
Estamos rodeados de manejo de excepciones en la vida real a diario.
Al pedir un producto en una tienda en línea, es posible que el producto no esté disponible en stock o que se produzca una falla en la entrega. Estas condiciones excepcionales se pueden contrarrestar fabricando otro producto o enviando uno nuevo después de que la entrega fallara.
Al crear aplicaciones, pueden encontrarse con todo tipo de condiciones excepcionales. Afortunadamente, al ser competente en el manejo de excepciones, estas condiciones se pueden contrarrestar alterando el flujo del código.
¿Por qué utilizar el manejo de excepciones?
Al crear aplicaciones, normalmente trabajamos en un entorno ideal: el sistema de archivos puede proporcionarnos todos los archivos que solicitamos, nuestra conexión a Internet es estable y la JVM siempre puede proporcionar suficiente memoria para nuestras necesidades.
Lamentablemente, en realidad, el entorno está lejos de ser ideal: el archivo no se puede encontrar, la conexión a Internet se interrumpe de vez en cuando y la JVM no puede proporcionar suficiente memoria y nos quedamos con un problema desalentador StackOverflowError
.
Si no manejamos tales condiciones, toda la aplicación terminará en ruinas y el resto del código quedará obsoleto. Por tanto, debemos ser capaces de escribir código que se adapte a tales situaciones.
Imagine que una empresa no puede resolver un problema simple que surgió después de ordenar un producto; no desea que su aplicación funcione de esa manera.
Jerarquía de excepciones
Todo esto simplemente plantea la pregunta: ¿cuáles son estas excepciones a los ojos de Java y la JVM?
Después de todo, las excepciones son simplemente objetos Java que amplían la Throwable
interfaz:
---> Throwable <---
| (checked) |
| |
| |
---> Exception Error
| (checked) (unchecked)
|
RuntimeException
(unchecked)
Cuando hablamos de condiciones excepcionales, normalmente nos referimos a una de las tres:
- Excepciones marcadas
- Excepciones sin marcar / Excepciones en tiempo de ejecución
- Errores
Nota : Los términos «Runtime» y «Unchecked» se utilizan a menudo indistintamente y se refieren al mismo tipo de excepciones.
Te puede interesar:Introducción a las secuencias de Java 8Excepciones marcadas
Las excepciones marcadas son las excepciones que normalmente podemos prever y planificar con anticipación en nuestra aplicación. Estas también son excepciones que el compilador de Java requiere que manejemos o declaremos al escribir código.
La regla manejar o declarar se refiere a nuestra responsabilidad de declarar que un método arroja una excepción en la pila de llamadas, sin hacer mucho para evitarlo, o manejar la excepción con nuestro propio código, lo que generalmente conduce a la recuperación del programa de la condición excepcional.
Esta es la razón por la que se denominan excepciones marcadas. El compilador puede detectarlos antes del tiempo de ejecución y usted es consciente de su posible existencia mientras escribe código.
Excepciones sin marcar
Las excepciones no marcadas son las excepciones que suelen ocurrir debido a errores humanos, en lugar de ambientales. Estas excepciones no se comprueban durante la compilación, sino en el tiempo de ejecución, razón por la cual también se denominan Excepciones en tiempo de ejecución.
A menudo se pueden contrarrestar implementando comprobaciones simples antes de un segmento de código que podría potencialmente usarse de una manera que forme una excepción de tiempo de ejecución, pero más sobre eso más adelante.
Errores
Los errores son las condiciones excepcionales más graves con las que puede encontrarse. A menudo son irrecuperables y no hay una forma real de manejarlos. Lo único que nosotros, como desarrolladores, podemos hacer es optimizar el código con la esperanza de que los errores nunca ocurran.
Los errores pueden ocurrir debido a errores humanos y ambientales. La creación de un método infinitamente recurrente puede llevar a un StackOverflowError
, o una pérdida de memoria puede llevar a un OutOfMemoryError
.
Cómo manejar las excepciones
tirar y lanzar
La forma más sencilla de solucionar un error del compilador cuando se trata de una excepción marcada es simplemente lanzarla.
public File getFile(String url) throws FileNotFoundException {
// some code
throw new FileNotFoundException();
}
Estamos obligados a marcar la firma de nuestro método con una throws
cláusula. Un método puede agregar tantas excepciones como sea necesario en su throws
cláusula y puede lanzarlas más adelante en el código, pero no es necesario. Este método no requiere una return
declaración, aunque define un tipo de retorno. Esto se debe a que genera una excepción de forma predeterminada, que finaliza el flujo del método de forma abrupta. Por lo return
tanto, la declaración sería inalcanzable y provocaría un error de compilación.
Tenga en cuenta que cualquiera que llame a este método también debe seguir la regla de manejar o declarar.
Al lanzar una excepción, podemos lanzar una nueva excepción, como en el ejemplo anterior, o una excepción detectada.
Try-catch Blocks
Un enfoque más común sería el uso de un try
– catch
bloque para capturar y manejar la excepción que se genera:
public String readFirstLine(String url) throws FileNotFoundException {
try {
Scanner scanner = new Scanner(new File(url));
return scanner.nextLine();
} catch(FileNotFoundException ex) {
throw ex;
}
}
En este ejemplo, «marcamos» un segmento de código de riesgo encerrándolo dentro de un try
bloque. Esto le dice al compilador que estamos al tanto de una posible excepción y que tenemos la intención de manejarla si surge.
Este código intenta leer el contenido del archivo y, si no se encuentra, FileNotFoundException
se captura y se vuelve a lanzar. Más sobre este tema más adelante.
Ejecutar este fragmento de código sin una URL válida resultará en una excepción lanzada:
Exception in thread "main" java.io.FileNotFoundException: some_file (The system cannot find the file specified) <-- some_file doesn't exist
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.util.Scanner.<init>(Scanner.java:611)
at Exceptions.ExceptionHandling.readFirstLine(ExceptionHandling.java:15) <-- Exception arises on the the readFirstLine() method, on line 15
at Exceptions.ExceptionHandling.main(ExceptionHandling.java:10) <-- readFirstLine() is called by main() on line 10
...
Alternativamente, podemos intentar recuperarnos de esta condición en lugar de volver a lanzar:
public static String readFirstLine(String url) {
try {
Scanner scanner = new Scanner(new File(url));
return scanner.nextLine();
} catch(FileNotFoundException ex) {
System.out.println("File not found.");
return null;
}
}
Ejecutar este fragmento de código sin una URL válida resultará en:
File not found.
finalmente bloquea
Al introducir un nuevo tipo de bloque, el finally
bloque se ejecuta independientemente de lo que suceda en el bloque try. Incluso si termina abruptamente lanzando una excepción, el finally
bloque se ejecutará.
Esto se usó a menudo para cerrar los recursos que se abrieron en el try
bloque, ya que una excepción que surgiera omitiría el código cerrándolos:
public String readFirstLine(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if(br != null) br.close();
}
}
Sin embargo, este enfoque ha sido mal visto después del lanzamiento de Java 7, que introdujo una forma mejor y más limpia de cerrar recursos, y actualmente se considera una mala práctica.
Declaración de prueba con recursos
El bloque anteriormente complejo y detallado se puede sustituir por:
static String readFirstLineFromFile(String path) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
Es mucho más limpio y obviamente se simplifica al incluir la declaración entre paréntesis del try
bloque.
Además, puede incluir varios recursos en este bloque, uno tras otro:
static String multipleResources(String path) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader(path));
BufferedWriter writer = new BufferedWriter(path, charset)) {
// some code
}
}
De esta manera, no tiene que preocuparse por cerrar los recursos usted mismo, ya que el bloque try-with-resources asegura que los recursos se cerrarán al final de la declaración.
Múltiples bloques de captura
Cuando el código que estamos escribiendo puede lanzar más de una excepción, podemos emplear varios bloques de captura para manejarlos individualmente:
public void parseFile(String filePath) {
try {
// some code
} catch (IOException ex) {
// handle
} catch (NumberFormatException ex) {
// handle
}
}
Cuando el try
bloque incurre en una excepción, la JVM verifica si la primera excepción detectada es apropiada y, de no ser así, continúa hasta que encuentra una.
Nota : La captura de una excepción genérica capturará todas sus subclases, por lo que no es necesario capturarlas por separado.
FileNotFound
No es necesario IOException
capturar una excepción en este ejemplo, porque se extiende desde , pero si surge la necesidad, podemos detectarla antes de IOException
:
public void parseFile(String filePath) {
try {
// some code
} catch(FileNotFoundException ex) {
// handle
} catch (IOException ex) {
// handle
} catch (NumberFormatException ex) {
// handle
}
}
De esta manera, podemos manejar la excepción más específica de una manera diferente a una más genérica.
Nota : Cuando se detectan múltiples excepciones, el compilador de Java requiere que coloquemos las más específicas antes que las más generales, de lo contrario, serían inalcanzables y generarían un error del compilador.
Bloques de captura de unión
Para reducir el código repetitivo, Java 7 también introdujo bloques de captura de unión. Nos permiten tratar múltiples excepciones de la misma manera y manejar sus excepciones en un solo bloque:
public void parseFile(String filePath) {
try {
// some code
} catch (IOException | NumberFormatException ex) {
// handle
}
}
Cómo lanzar excepciones
A veces, no queremos manejar excepciones. En tales casos, solo debemos preocuparnos por generarlos cuando sea necesario y permitir que otra persona, llamando a nuestro método, los maneje adecuadamente.
Lanzar una excepción marcada
Cuando algo sale mal, como la cantidad de usuarios que se conectan actualmente a nuestro servicio y excede la cantidad máxima que el servidor puede manejar sin problemas, queremos throw
una excepción para indicar una situación excepcional:
public void countUsers() throws TooManyUsersException {
int numberOfUsers = 0;
while(numberOfUsers < 500) {
// some code
numberOfUsers++;
}
throw new TooManyUsersException("The number of users exceeds our maximum
recommended amount.");
}
}
Este código aumentará numberOfUsers
hasta que supere la cantidad máxima recomendada, después de lo cual lanzará una excepción. Dado que esta es una excepción marcada, tenemos que agregar la throws
cláusula en la firma del método.
Definir una excepción como esta es tan fácil como escribir lo siguiente:
public class TooManyUsersException extends Exception {
public TooManyUsersException(String message) {
super(message);
}
}
Lanzar una excepción no marcada
Lanzar excepciones de tiempo de ejecución por lo general se reduce a la validación de la entrada, ya que más a menudo se producen debido a la entrada defectuoso – ya sea en la forma de un IllegalArgumentException
, NumberFormatException
, ArrayIndexOutOfBoundsException
, o una NullPointerException
:
public void authenticateUser(String username) throws UserNotAuthenticatedException {
if(!isAuthenticated(username)) {
throw new UserNotAuthenticatedException("User is not authenticated!");
}
}
Dado que estamos lanzando una excepción de tiempo de ejecución, no es necesario incluirla en la firma del método, como en el ejemplo anterior, pero a menudo se considera una buena práctica hacerlo, al menos por el bien de la documentación.
Nuevamente, definir una excepción de tiempo de ejecución personalizada como esta es tan fácil como:
public class UserNotAuthenticatedException extends RuntimeException {
public UserNotAuthenticatedException(String message) {
super(message);
}
}
Relanzar
Antes se mencionó volver a lanzar una excepción, así que aquí hay una breve sección para aclarar:
Te puede interesar:Cómo usar hilos en Java Swingpublic String readFirstLine(String url) throws FileNotFoundException {
try {
Scanner scanner = new Scanner(new File(url));
return scanner.nextLine();
} catch(FileNotFoundException ex) {
throw ex;
}
}
Relanzar se refiere al proceso de lanzar una excepción ya detectada, en lugar de lanzar una nueva.
Envase
Envolver, por otro lado, se refiere al proceso de envolver una excepción ya detectada, dentro de otra excepción:
public String readFirstLine(String url) throws FileNotFoundException {
try {
Scanner scanner = new Scanner(new File(url));
return scanner.nextLine();
} catch(FileNotFoundException ex) {
throw new SomeOtherException(ex);
}
}
¿Relanzar Throwable o _Exception *?
Estas clases de nivel superior se pueden capturar y volver a lanzar, pero la forma de hacerlo puede variar:
public void parseFile(String filePath) {
try {
throw new NumberFormatException();
} catch (Throwable t) {
throw t;
}
}
En este caso, el método lanza una NumberFormatException
excepción de tiempo de ejecución. Debido a esto, no tenemos que marcar la firma del método con NumberFormatException
o Throwable
.
Sin embargo, si lanzamos una excepción marcada dentro del método:
public void parseFile(String filePath) throws Throwable {
try {
throw new IOException();
} catch (Throwable t) {
throw t;
}
}
Ahora tenemos que declarar que el método arroja un Throwable
. Por qué esto puede ser útil es un tema amplio que está fuera del alcance de este blog, pero hay usos para este caso específico.
Herencia de excepción
Las subclases que heredan un método solo pueden generar menos excepciones marcadas que su superclase:
public class SomeClass {
public void doSomething() throws SomeException {
// some code
}
}
Con esta definición, el siguiente método provocará un error del compilador:
public class OtherClass extends SomeClass {
@Override
public void doSomething() throws OtherException {
// some code
}
}
Mejores y peores prácticas de manejo de excepciones
Con todo eso cubierto, debería estar bastante familiarizado con cómo funcionan las excepciones y cómo usarlas. Ahora, cubramos las mejores y peores prácticas cuando se trata de manejar excepciones que esperamos que comprendamos completamente ahora.
Mejores prácticas de manejo de excepciones
Evite condiciones excepcionales
A veces, al usar comprobaciones simples, podemos evitar que se forme una excepción por completo:
public Employee getEmployee(int i) {
Employee[] employeeArray = {new Employee("David"), new Employee("Rhett"), new
Employee("Scott")};
if(i >= employeeArray.length) {
System.out.println("Index is too high!");
return null;
} else {
System.out.println("Employee found: " + employeeArray[i].name);
return employeeArray[i];
}
}
}
Llamar a este método con un índice válido daría como resultado:
Employee found: Scott
Pero llamar a este método con un índice que está fuera de los límites daría como resultado:
Index is too high!
En cualquier caso, aunque el índice sea demasiado alto, la línea de código infractora no se ejecutará y no surgirá ninguna excepción.
Te puede interesar:Ejemplo: carga de una clase Java en tiempo de ejecuciónUsa recursos de prueba
Como ya se mencionó anteriormente, siempre es mejor utilizar el enfoque más nuevo, más conciso y más limpio cuando se trabaja con recursos.
Cerrar recursos en try-catch-finalmente
Si no está utilizando los consejos anteriores por algún motivo, al menos asegúrese de cerrar los recursos manualmente en el bloque finalmente.
No incluiré un ejemplo de código para esto ya que ambos ya se han proporcionado, por brevedad.
Peores prácticas de manejo de excepciones
Excepciones al tragar
Si su intención es simplemente satisfacer al compilador, puede hacerlo fácilmente tragándose la excepción :
public void parseFile(String filePath) {
try {
// some code that forms an exception
} catch (Exception ex) {}
}
Tragar una excepción se refiere al acto de detectar una excepción y no solucionar el problema.
De esta forma, el compilador queda satisfecho ya que se detecta la excepción, pero se pierde toda la información útil relevante que pudimos extraer de la excepción para la depuración, y no hicimos nada para recuperarnos de esta condición excepcional.
Otra práctica muy común es simplemente imprimir el seguimiento de la pila de la excepción:
public void parseFile(String filePath) {
try {
// some code that forms an exception
} catch(Exception ex) {
ex.printStackTrace();
}
}
Este enfoque forma una ilusión de manejo. Sí, aunque es mejor que simplemente ignorar la excepción, al imprimir la información relevante, esto no maneja la condición excepcional más que ignorarla.
Regresa en un bloque finalmente
Según la JLS ( especificación del lenguaje Java ):
Si la ejecución del bloque try se completa abruptamente por cualquier otro motivo R, entonces el finally
bloque se ejecuta y luego hay una opción.
Entonces, en la terminología de la documentación, si el finally
bloque se completa normalmente, entonces la try
declaración se completa abruptamente por la razón R.
Si el finally
bloque se completa abruptamente por la razón S, entonces la try
declaración se completa abruptamente por la razón S (y la razón R se descarta).
En esencia, al regresar abruptamente de un finally
bloque, la JVM eliminará la excepción del try
bloque y se perderán todos los datos valiosos del mismo:
public String doSomething() {
String name = "David";
try {
throw new IOException();
} finally {
return name;
}
}
En este caso, aunque el try
bloque arroje un nuevo IOException
, usamos return
en el finally
bloque, finalizándolo abruptamente. Esto hace que el try
bloque finalice abruptamente debido a la declaración de retorno, y no al IOException
, esencialmente eliminando la excepción en el proceso.
Lanzando un bloque finalmente
Muy similar al ejemplo anterior, usar throw
en un finally
bloque eliminará la excepción del bloque try-catch:
public static String doSomething() {
try {
// some code that forms an exception
} catch(IOException io) {
throw io;
} finally {
throw new MyException();
}
}
En este ejemplo, lo MyException
arrojado dentro del finally
bloque eclipsará la excepción lanzada por el catch
bloque y se eliminará toda la información valiosa.
Simular una declaración goto
El pensamiento crítico y las formas creativas de encontrar una solución a un problema es un buen rasgo, pero algunas soluciones, por creativas que sean, son ineficaces y redundantes.
Java no tiene una declaración goto como otros lenguajes, sino que usa etiquetas para saltar el código:
public void jumpForward() {
label: {
someMethod();
if (condition) break label;
otherMethod();
}
}
Sin embargo, algunas personas usan excepciones para simularlas:
public void jumpForward() {
try {
// some code 1
throw new MyException();
// some code 2
} catch(MyException ex) {
// some code 3
}
}
Usar excepciones para este propósito es ineficaz y lento. Las excepciones están diseñadas para código excepcional y deben usarse para código excepcional.
Tala y lanzamiento
Cuando intente depurar un fragmento de código y descubra lo que está sucediendo, no registre y lance la excepción:
public static String readFirstLine(String url) throws FileNotFoundException {
try {
Scanner scanner = new Scanner(new File(url));
return scanner.nextLine();
} catch(FileNotFoundException ex) {
LOGGER.error("FileNotFoundException: ", ex);
throw ex;
}
}
Hacer esto es redundante y simplemente resultará en un montón de mensajes de registro que no son realmente necesarios. La cantidad de texto reducirá la visibilidad de los registros.
Atrapar la excepción o lanzar
¿Por qué no simplemente capturamos Exception o Throwable, si captura todas las subclases?
A menos que haya una buena razón específica para detectar cualquiera de estos dos, generalmente no se recomienda hacerlo.
La captura Exception
detectará las excepciones marcadas y en tiempo de ejecución. Las excepciones en tiempo de ejecución representan problemas que son un resultado directo de un problema de programación y, como tales, no deben detectarse, ya que no se puede esperar razonablemente que se recuperen o manejen.
La captura Throwable
atrapará todo . Esto incluye todos los errores, que en realidad no deben detectarse de ninguna manera.
Conclusión
En este artículo, hemos cubierto las excepciones y el manejo de excepciones desde cero. Luego, cubrimos las mejores y peores prácticas de manejo de excepciones en Java.
Espero que hayas encontrado este blog informativo y educativo, ¡feliz codificación!