¿Las excepciones marcadas son buenas o malas?

Java admite excepciones comprobadas. Esta controvertida característica del lenguaje es amada por algunos y odiada por otros, hasta el punto en que la mayoría de los lenguajes de programación evitan las excepciones marcadas y solo admiten sus contrapartes no verificadas.

En esta publicación, examino la controversia en torno a las excepciones marcadas. Primero presento el concepto de excepciones y describo brevemente el soporte del lenguaje Java para las excepciones para ayudar a los principiantes a comprender mejor la controversia.

¿Qué son las excepciones?

En un mundo ideal, los programas de computadora nunca encontrarían ningún problema: los archivos existirían cuando se supone que existen, las conexiones de red nunca se cerrarían inesperadamente, nunca habría un intento de invocar un método a través de la referencia nula, entero-división-por -no ocurrirían cero intentos, y así sucesivamente. Sin embargo, nuestro mundo está lejos de ser ideal; estas y otras excepciones a la ejecución ideal del programa están muy extendidas.

Los primeros intentos de reconocer excepciones incluyeron la devolución de valores especiales que indican fallas. Por ejemplo, la fopen()función del lenguaje C regresa NULLcuando no puede abrir un archivo. Además, la mysql_query()función de PHP regresa FALSEcuando ocurre una falla de SQL. Debe buscar en otra parte el código de falla real. Aunque es fácil de implementar, hay dos problemas con este enfoque de "devolver valor especial" para reconocer excepciones:

  • Los valores especiales no describen la excepción. ¿Qué significa NULLo FALSErealmente significa? Todo depende del autor de la funcionalidad que devuelve el valor especial. Además, ¿cómo se relaciona un valor especial con el contexto del programa cuando ocurrió la excepción para que pueda presentar un mensaje significativo al usuario?
  • Es demasiado fácil ignorar un valor especial. Por ejemplo, int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);es problemático porque este fragmento de código C se ejecuta fgetc()para leer un carácter del archivo incluso cuando fopen()regresa NULL. En este caso, fgetc()no tendrá éxito: tenemos un error que puede ser difícil de encontrar.

El primer problema se resuelve usando clases para describir excepciones. El nombre de una clase identifica el tipo de excepción y sus campos agregan el contexto de programa apropiado para determinar (mediante llamadas a métodos) qué salió mal. El segundo problema se resuelve haciendo que el compilador obligue al programador a responder directamente a una excepción o indicar que la excepción se manejará en otro lugar.

Algunas excepciones son muy graves. Por ejemplo, un programa puede intentar asignar algo de memoria cuando no hay memoria libre disponible. La recursividad ilimitada que agota la pila es otro ejemplo. Estas excepciones se conocen como errores .

Excepciones y Java

Java usa clases para describir excepciones y errores. Estas clases están organizadas en una jerarquía que tiene sus raíces en la java.lang.Throwableclase. (La razón por la que Throwablese eligió nombrar esta clase especial se hará evidente en breve). Directamente debajo Throwableestán las clases java.lang.Exceptiony java.lang.Error, que describen excepciones y errores, respectivamente.

Por ejemplo, la biblioteca de Java incluye java.net.URISyntaxException, que se extiende Exceptione indica que una cadena no se pudo analizar como una referencia de Identificador de recursos uniforme. Tenga en cuenta que URISyntaxExceptionsigue una convención de nomenclatura en la que un nombre de clase de excepción termina con la palabra Exception. Se aplica una convención similar a los nombres de clases de error, como java.lang.OutOfMemoryError.

Exceptionestá subclasificado por java.lang.RuntimeException, que es la superclase de aquellas excepciones que se pueden lanzar durante el funcionamiento normal de la máquina virtual Java (JVM). Por ejemplo, java.lang.ArithmeticExceptiondescribe fallas aritméticas tales como intentos de dividir enteros por entero 0. Además, java.lang.NullPointerExceptiondescribe intentos de acceder a miembros de objeto a través de la referencia nula.

Otra forma de mirar RuntimeException

La sección 11.1.1 de la Especificación del lenguaje Java 8 establece: RuntimeExceptiones la superclase de todas las excepciones que se pueden lanzar por muchas razones durante la evaluación de la expresión, pero de las cuales aún es posible la recuperación.

Cuando se produce una excepción o error, un objeto de la adecuada Exceptiono Errorsubclase se crea y se pasa a la JVM. El acto de pasar el objeto se conoce como lanzar la excepción . Java proporciona la throwdeclaración para este propósito. Por ejemplo, throw new IOException("unable to read file");crea un nuevo java.io.IOExceptionobjeto que se inicializa con el texto especificado. Este objeto se lanza posteriormente a la JVM.

Java proporciona la trydeclaración para delimitar el código desde el cual se puede lanzar una excepción. Esta declaración consta de una palabra clave tryseguida de un bloque delimitado por llaves. El siguiente fragmento de código demuestra tryy throw:

try { method(); } // ... void method() { throw new NullPointerException("some text"); }

En este fragmento de código, la ejecución ingresa al trybloque e invoca method(), lo que arroja una instancia de NullPointerException.

La JVM recibe lo arrojable y busca en la pila de llamadas al método un manejador para manejar la excepción. RuntimeExceptionA menudo se manejan excepciones no derivadas de ; las excepciones y errores en tiempo de ejecución rara vez se manejan.

Por qué los errores rara vez se manejan

Los errores rara vez se manejan porque a menudo no hay nada que un programa Java pueda hacer para recuperarse del error. Por ejemplo, cuando se agota la memoria libre, un programa no puede asignar memoria adicional. Sin embargo, si el error de asignación se debe a retener una gran cantidad de memoria que debería liberarse, un administrador podría intentar liberar la memoria con la ayuda de la JVM. Aunque un controlador puede parecer útil en este contexto de error, es posible que el intento no tenga éxito.

Un manejador se describe mediante un catchbloque que sigue al trybloque. El catchbloque proporciona un encabezado que enumera los tipos de excepciones que está preparado para manejar. Si el tipo de lanzador está incluido en la lista, el lanzador se pasa al catchbloque cuyo código se ejecuta. El código responde a la causa de la falla de tal manera que hace que el programa continúe, o posiblemente termine:

try { method(); } catch (NullPointerException npe) { System.out.println("attempt to access object member via null reference"); } // ... void method() { throw new NullPointerException("some text"); }

En este fragmento de código, agregué un catchbloque al trybloque. Cuando NullPointerExceptionse lanza el objeto method(), la JVM localiza y pasa la ejecución al catchbloque, que genera un mensaje.

Finalmente bloquea

Un trybloque o su catchbloque final puede ir seguido de un finallybloque que se usa para realizar tareas de limpieza, como liberar recursos adquiridos. No tengo nada más que decir finallyporque no es relevante para la discusión.

Las excepciones descritas por Exceptiony sus subclases, excepto por RuntimeExceptiony sus subclases, se conocen como excepciones marcadas . Para cada throwdeclaración, el compilador examina el tipo de objeto de excepción. Si el tipo indica marcado, el compilador verifica el código fuente para asegurarse de que la excepción se maneja en el método donde se lanza o se declara que se maneja más arriba en la pila de llamadas al método. Todas las demás excepciones se conocen como excepciones no controladas .

Java le permite declarar que una excepción verificada se maneja más arriba en la pila de llamadas al método agregando una throwscláusula (palabra clave throwsseguida de una lista delimitada por comas de nombres de clases de excepciones verificadas) a un encabezado de método:

try { method(); } catch (IOException ioe) { System.out.println("I/O failure"); } // ... void method() throws IOException { throw new IOException("some text"); }

Debido a que IOExceptiones un tipo de excepción comprobado, las instancias lanzadas de esta excepción deben manejarse en el método donde se lanzan o declararse para ser manejadas más arriba en la pila de llamadas al método agregando una throwscláusula al encabezado de cada método afectado. En este caso, throws IOExceptionse adjunta una cláusula al method()encabezado de. El IOExceptionobjeto lanzado se pasa a la JVM, que localiza y transfiere la ejecución al catchcontrolador.

Argumentando a favor y en contra de excepciones comprobadas

Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.

Checked exceptions are good

James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.

The seriousness of not checking return values

Not checking return values might seem like no big deal, but this sloppiness can have life-or-death consequences. For example, think about such buggy software controlling missile guidance systems and driverless cars.

Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.

Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.

Checked exceptions are bad

Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException class.

Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:

  • Checked exceptions are easy to ignore by rethrowing them as RuntimeException instances, so what's the point of having them? I've lost count of the number of times I've written this block of code:
    try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }

    99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).

  • Checked exceptions can be ignored by swallowing them, so what's the point of having them? I've also lost count of the number of times I've seen this:
    try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }

    Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).

  • Checked exceptions result in multiple throws clause declarations. The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
  • Checked exceptions are not really exceptions. The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.

    The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.

Además, me he encontrado con el argumento de que las aplicaciones tienen que manejar una gran cantidad de excepciones comprobadas que se generan a partir de las múltiples bibliotecas a las que acceden. Sin embargo, este problema puede superarse mediante una fachada inteligentemente diseñada que aprovecha la función de excepción encadenada de Java y el relanzamiento de excepciones para reducir en gran medida el número de excepciones que deben manejarse conservando la excepción original que se lanzó.

Conclusión

¿Las excepciones marcadas son buenas o malas? En otras palabras, ¿se debería obligar a los programadores a manejar las excepciones comprobadas o darles la oportunidad de ignorarlas? Me gusta la idea de aplicar un software más robusto. Sin embargo, también creo que el mecanismo de manejo de excepciones de Java debe evolucionar para hacerlo más amigable para los programadores. Aquí hay un par de formas de mejorar este mecanismo: