Buscar..


Introducción

Varios usos indebidos del lenguaje de programación Java pueden llevar a cabo un programa para generar resultados incorrectos a pesar de haber sido compilados correctamente. El propósito principal de este tema es enumerar las dificultades comunes relacionadas con el manejo de excepciones y proponer la forma correcta de evitar tales dificultades.

Pitfall - Ignorar o aplastar excepciones

Este ejemplo trata sobre ignorar deliberadamente o "aplastar" las excepciones. O para ser más precisos, se trata de cómo capturar y manejar una excepción de una manera que la ignora. Sin embargo, antes de describir cómo hacer esto, primero debemos señalar que las excepciones de aplastamiento generalmente no son la forma correcta de tratarlas.

Las excepciones generalmente se lanzan (por algo) para notificar a otras partes del programa que ha ocurrido algún evento significativo (es decir, "excepcional"). En general (aunque no siempre) una excepción significa que algo ha salido mal. Si codifica su programa para eliminar la excepción, existe la posibilidad de que el problema vuelva a aparecer en otra forma. Para empeorar las cosas, cuando aplastas la excepción, estás desechando la información en el objeto de excepción y su seguimiento de pila asociado. Es probable que sea más difícil averiguar cuál fue la fuente original del problema.

En la práctica, el aplastamiento de excepciones ocurre con frecuencia cuando utiliza la función de corrección automática de un IDE para "arreglar" un error de compilación causado por una excepción no controlada. Por ejemplo, puede ver código como este:

try {
    inputStream = new FileInputStream("someFile");
} catch (IOException e) {
    /* add exception handling code here */
}

Claramente, el programador ha aceptado la sugerencia del IDE para que desaparezca el error de compilación, pero la sugerencia fue inapropiada. (Si el archivo ha fallado, lo más probable es que el programa haga algo al respecto. Con la "corrección" anterior, es probable que el programa falle más adelante; por ejemplo, con una NullPointerException porque inputStream ahora es null ).

Dicho esto, aquí hay un ejemplo de aplastar deliberadamente una excepción. (Para los fines del argumento, supongamos que hemos determinado que una interrupción al mostrar la autofoto es inofensiva). El comentario le dice al lector que eliminamos la excepción deliberadamente y por qué lo hicimos.

try {
    selfie.show();
} catch (InterruptedException e) {
    // It doesn't matter if showing the selfie is interrupted.
}

Otra forma convencional de resaltar que estamos aplastando deliberadamente una excepción sin decir por qué es indicar esto con el nombre de la variable de excepción, como este:

try { 
    selfie.show(); 
} catch (InterruptedException ignored) {  }

Algunos IDE (como IntelliJ IDEA) no mostrarán una advertencia sobre el bloque catch vacío si el nombre de la variable se establece en ignored .

Pitfall: captura de Throwable, Exception, Error o RuntimeException

Un patrón de pensamiento común que los programadores inexpertos Java es que las excepciones son "un problema" o "una carga" y la mejor manera de lidiar con esto es que coger todos 1 tan pronto como sea posible. Esto lleva a un código como este:

....
try {
    InputStream is = new FileInputStream(fileName);
    // process the input
} catch (Exception ex) {
    System.out.println("Could not open file " + fileName);
}

El código anterior tiene un defecto significativo. La catch realidad va a atrapar más excepciones de las que espera el programador. Supongamos que el valor del nombre de fileName es null , debido a un error en otra parte de la aplicación. Esto hará que el constructor FileInputStream lance una NullPointerException . El controlador detectará esto e informará al usuario:

    Could not open file null

que es inútil y confuso. Peor aún, supongamos que fue el código de "procesar la entrada" el que lanzó la excepción inesperada (activada o desactivada). Ahora el usuario recibirá el mensaje engañoso para un problema que no se produjo al abrir el archivo y es posible que no esté relacionado con la E / S en absoluto.

La raíz del problema es que el programador ha codificado un controlador para Exception . Esto es casi siempre un error:

  • La Exception captura capturará todas las excepciones marcadas, y la mayoría de las excepciones no marcadas también.
  • La captura de RuntimeException detectará la mayoría de las excepciones no verificadas.
  • El Error captura detectará las excepciones sin marcar que señalan errores internos de JVM. Estos errores generalmente no son recuperables y no deben ser detectados.
  • La captura de Throwable capturará todas las excepciones posibles.

El problema con la captura de un conjunto demasiado amplio de excepciones es que el controlador generalmente no puede manejarlas todas adecuadamente. En el caso de la Exception etc., es difícil para el programador predecir lo que podría detectarse; es decir, qué esperar.

En general, la solución correcta es lidiar con las excepciones que se lanzan. Por ejemplo, puedes atraparlos y manejarlos in situ:

try {
    InputStream is = new FileInputStream(fileName);
    // process the input
} catch (FileNotFoundException ex) {
    System.out.println("Could not open file " + fileName);
}

o puede declararlos como thrown por el método adjunto.


Hay muy pocas situaciones en las que la captura de Exception es apropiada. El único que surge comúnmente es algo como esto:

public static void main(String[] args) {
    try {
        // do stuff
    } catch (Exception ex) {
        System.err.println("Unfortunately an error has occurred. " +
                           "Please report this to X Y Z");
        // Write stacktrace to a log file.
        System.exit(1);
    }
}

Aquí realmente queremos lidiar con todas las excepciones, por lo que capturar Exception (o incluso Throwable ) es correcto.


1 - También conocido como Pokemon Exception Handling .

Pitfall - Lanzar Throwable, Exception, Error o RuntimeException

Si bien la captura de las Exception Throwable , Exception , Error y RuntimeException es mala, lanzarlas es aún peor.

El problema básico es que cuando su aplicación necesita manejar excepciones, la presencia de las excepciones de nivel superior hace que sea difícil discriminar entre diferentes condiciones de error. Por ejemplo

try {
    InputStream is = new FileInputStream(someFile);  // could throw IOException
    ...
    if (somethingBad) {
        throw new Exception();  // WRONG
    }
} catch (IOException ex) {
    System.err.println("cannot open ...");
} catch (Exception ex) {
    System.err.println("something bad happened");  // WRONG
}

El problema es que debido a que lanzamos una instancia de Exception , nos vemos obligados a atraparla. Sin embargo, como se describe en otro ejemplo, la captura de Exception es mala. En esta situación, se vuelve difícil discriminar entre el caso "esperado" de una Exception que se lanza si somethingBad es una true , y el caso inesperado en el que realmente detectamos una excepción no comprobada, como NullPointerException .

Si se permite que la excepción de nivel superior se propague, nos encontramos con otros problemas:

  • Ahora tenemos que recordar todas las diferentes razones por las que lanzamos el nivel superior y discriminarlos / manejarlos.
  • En el caso de Exception y Throwable , también necesitamos agregar estas excepciones a la cláusula de throws de métodos si queremos que la excepción se propague. Esto es problemático, como se describe a continuación.

En resumen, no tirar estas excepciones. Lanzar una excepción más específica que describa con más detalle el "evento excepcional" que ha ocurrido. Si lo necesita, defina y use una clase de excepción personalizada.

Declarar Throwable o Exception en los "lanzamientos" de un método es problemático.

Es tentador reemplazar una larga lista de excepciones lanzadas en la cláusula de throws un método con Exception o incluso `Throwable. Esta es una mala idea:

  1. Obliga al llamante a manejar (o propagar) la Exception .
  2. Ya no podemos confiar en el compilador para informarnos sobre excepciones comprobadas específicas que deben manejarse.
  3. Manejar la Exception adecuadamente es difícil. Es difícil saber qué excepciones reales pueden detectarse, y si no sabe qué podría detectarse, es difícil saber qué estrategia de recuperación es adecuada.
  4. El manejo de Throwable es aún más difícil, ya que ahora también tiene que hacer frente a fallas potenciales de las que nunca debería recuperarse.

Este consejo significa que se deben evitar ciertos otros patrones. Por ejemplo:

try {
    doSomething();
} catch (Exception ex) {
    report(ex);
    throw ex;
}

Lo anterior intenta registrar todas las excepciones a medida que pasan, sin manejarlas definitivamente. Desafortunadamente, antes de Java 7, el throw ex; La declaración hizo que el compilador pensara que se podía lanzar cualquier Exception . Eso podría forzarlo a declarar el método de cierre como throws Exception . Desde Java 7 en adelante, el compilador sabe que el conjunto de excepciones que podrían ser (relanzadas) es menor.

Pitfall - Catching InterruptedException

Como ya se señaló en otros escollos, capturar todas las excepciones usando

try {
    // Some code
} catch (Exception) {
    // Some error handling
}

Viene con muchos problemas diferentes. Pero un problema importante es que puede provocar interbloqueos ya que rompe el sistema de interrupción al escribir aplicaciones de subprocesos múltiples.

Si inicia un hilo, por lo general, también debe poder detenerlo bruscamente por varios motivos.

Thread t = new Thread(new Runnable() {
    public void run() {
         while (true) {
             //Do something indefinetely
         }
    }
}

t.start();

//Do something else

// The thread should be canceld if it is still active. 
// A Better way to solve this is with a shared variable that is tested 
// regularily by the thread for a clean exit, but for this example we try to 
// forcibly interrupt this thread.
if (t.isAlive()) {
   t.interrupt();
   t.join();
}

//Continue with program

El t.interrupt() generará una InterruptedException en ese hilo, que está destinado a cerrar el hilo. Pero, ¿qué pasa si el subproceso necesita limpiar algunos recursos antes de que se detenga por completo? Para esto puede atrapar la excepción interrumpida y hacer algo de limpieza.

 Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                //Do something indefinetely
            }
        } catch (InterruptedException ex) {
            //Do some quick cleanup

            // In this case a simple return would do. 
            // But if you are not 100% sure that the thread ends after 
            // catching the InterruptedException you will need to raise another 
            // one for the layers surrounding this code.                
            Thread.currentThread().interrupt(); 
        }
    }
}

Pero si tiene una expresión de captura en su código, la InterruptedException también será detectada por ella y la interrupción no continuará. Lo que en este caso podría provocar un punto muerto, ya que el subproceso principal espera indefinidamente a que este thead se detenga con t.join() .

 Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                try {
                    //Do something indefinetely
                }
                catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ex) {
            // Dead code as the interrupt exception was already caught in
            // the inner try-catch           
            Thread.currentThread().interrupt(); 
        }
    }
}

Por lo tanto, es mejor capturar Excepciones individualmente, pero si insistes en usar un catch-all, al menos debes capturar la InterruptedException individualmente de antemano.

Thread t = new Thread(new Runnable() {
    public void run() {
        try {
            while (true) {
                try {
                    //Do something indefinetely
                } catch (InterruptedException ex) {
                    throw ex; //Send it up in the chain
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ex) {
            // Some quick cleanup code 
    
            Thread.currentThread().interrupt(); 
        }
    }
}

Pitfall - Uso de excepciones para el control de flujo normal

Hay un mantra que algunos expertos de Java suelen recitar:

"Las excepciones solo deben usarse en casos excepcionales".

(Por ejemplo: http://programmers.stackexchange.com/questions/184654 )

La esencia de esto es que es una mala idea (en Java) usar excepciones y el manejo de excepciones para implementar el control de flujo normal. Por ejemplo, compare estas dos formas de tratar un parámetro que podría ser nulo.

public String truncateWordOrNull(String word, int maxLength) {
    if (word == null) {
        return "";
    } else {
        return word.substring(0, Math.min(word.length(), maxLength));
    }
}

public String truncateWordOrNull(String word, int maxLength) {
    try {
        return word.substring(0, Math.min(word.length(), maxLength));
    } catch (NullPointerException ex) {
        return "";
    }
}

En este ejemplo, estamos (por diseño) tratando el caso donde la word es null como si fuera una palabra vacía. Las dos versiones se ocupan de null ya sea utilizando convencional si ... más y / o intentan ... atrapar . ¿Cómo deberíamos decidir qué versión es mejor?

El primer criterio es la legibilidad. Si bien la legibilidad es difícil de cuantificar objetivamente, la mayoría de los programadores estarían de acuerdo en que el significado esencial de la primera versión es más fácil de discernir. De hecho, con el fin de entender realmente la segunda forma, es necesario entender que una NullPointerException no puede ser lanzada por los Math.min o String.substring métodos.

El segundo criterio es la eficiencia. En las versiones de Java anteriores a Java 8, la segunda versión es significativamente más lenta (órdenes de magnitud) que la primera versión. En particular, la construcción de un objeto de excepción implica la captura y el registro de los cuadros de pila, en caso de que se requiera el seguimiento de la pila.

Por otro lado, hay muchas situaciones donde el uso de excepciones es más legible, más eficiente y (a veces) más correcto que usar el código condicional para tratar eventos "excepcionales". De hecho, hay situaciones raras en las que es necesario usarlas para eventos "no excepcionales"; Es decir, eventos que ocurren con relativa frecuencia. Para este último, vale la pena buscar formas de reducir los gastos generales de creación de objetos de excepción.

Pitfall - Stacktraces excesivos o inapropiados

Una de las cosas más molestas que pueden hacer los programadores es dispersar las llamadas a printStackTrace() largo de su código.

El problema es que printStackTrace() escribirá el seguimiento de pila en la salida estándar.

  • Para una aplicación que está destinada a usuarios finales que no son programadores de Java, un seguimiento de pila no es informativo en el mejor de los casos, y alarmante en el peor.

  • Para una aplicación del lado del servidor, es probable que nadie vea la salida estándar.

Una idea mejor es no llamar directamente a printStackTrace , o si lo llama, hágalo de manera que el seguimiento de la pila se escriba en un archivo de registro o de error en lugar de hacerlo en la consola del usuario final.

Una forma de hacer esto es usar un marco de registro y pasar el objeto de excepción como un parámetro del evento de registro. Sin embargo, incluso el registro de la excepción puede ser perjudicial si se realiza de forma poco prudente. Considera lo siguiente:

public void method1() throws SomeException {
    try {
        method2();
        // Do something
    } catch (SomeException ex) {
        Logger.getLogger().warn("Something bad in method1", ex);
        throw ex;
    }
}

public void method2() throws SomeException {
    try {
        // Do something else
    } catch (SomeException ex) {
        Logger.getLogger().warn("Something bad in method2", ex);
        throw ex;
    }
}

Si la excepción se produce en el method2 , es probable que vea dos copias del mismo seguimiento de pila en el archivo de registro, que corresponde al mismo error.

En resumen, registre la excepción o vuelva a lanzarla más (posiblemente envuelta con otra excepción). No hagas ambas cosas.

Pitfall - Subclasificando directamente `Throwable`

Throwable tiene dos subclases directas, Exception y Error . Si bien es posible crear una nueva clase que extienda Throwable directamente, esto no es aconsejable ya que muchas aplicaciones suponen que solo existe Exception y Error .

Más aún, no hay ningún beneficio práctico en la subclasificación directa de Throwable , ya que la clase resultante es, en efecto, simplemente una excepción comprobada. La Exception subclases en cambio resultará en el mismo comportamiento, pero transmitirá más claramente su intención.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow