Manejo de Excepciones en Java: Una gu铆a completa con las mejores y peores pr谩cticas

    Visi贸n general

    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 Throwableinterfaz:

                                            ---> 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.

    Excepciones 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 throwscl谩usula. Un m茅todo puede agregar tantas excepciones como sea necesario en su throwscl谩usula y puede lanzarlas m谩s adelante en el c贸digo, pero no es necesario. Este m茅todo no requiere una returndeclaraci贸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 returntanto, 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 trycatchbloque 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 trybloque. 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, FileNotFoundExceptionse 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 finallybloque se ejecuta independientemente de lo que suceda en el bloque try. Incluso si termina abruptamente lanzando una excepci贸n, el finallybloque se ejecutar谩.

    Esto se us贸 a menudo para cerrar los recursos que se abrieron en el trybloque, 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 trybloque.

    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 trybloque 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.

    FileNotFoundNo es necesario IOExceptioncapturar 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 throwuna 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谩 numberOfUsershasta 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 throwscl谩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:

    public 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 NumberFormatExceptionexcepci贸n de tiempo de ejecuci贸n. Debido a esto, no tenemos que marcar la firma del m茅todo con NumberFormatExceptiono 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.

    Usa 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 finallybloque se ejecuta y luego hay una opci贸n.

    Entonces, en la terminolog铆a de la documentaci贸n, si el finallybloque se completa normalmente, entonces la trydeclaraci贸n se completa abruptamente por la raz贸n R.

    Si el finallybloque se completa abruptamente por la raz贸n S, entonces la trydeclaraci贸n se completa abruptamente por la raz贸n S (y la raz贸n R se descarta).

    En esencia, al regresar abruptamente de un finallybloque, la JVM eliminar谩 la excepci贸n del trybloque 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 trybloque arroje un nuevo IOException, usamos returnen el finallybloque, finaliz谩ndolo abruptamente. Esto hace que el trybloque 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 throwen un finallybloque 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 MyExceptionarrojado dentro del finallybloque eclipsar谩 la excepci贸n lanzada por el catchbloque 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 Exceptiondetectar谩 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 Throwableatrapar谩 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!

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *