Cuando empezamos a programar en Java, nos encontramos con una curiosidad que puede parecer injusta: no todas las excepciones se tratan igual. Por ejemplo, imaginemos dos métodos. El primero lee el contenido de un archivo y lo muestra por pantalla. El segundo realiza una división entre dos números. En el primer caso, el compilador nos obliga a manejar una excepción llamada IOException, ya sea con un bloque try-catch o declarando que el método lanza esa excepción. Sin embargo, en el segundo caso, aunque sabemos que dividir por cero es un error, el compilador no nos exige ningún tratamiento especial.
Esta diferencia se debe a que Java distingue entre dos tipos de excepciones: las excepciones controladas (checked exceptions) y las no controladas (unchecked exceptions). Las primeras, como IOException, representan situaciones externas a nuestro código, como que un archivo no exista o que una conexión de red falle. Por eso, Java nos obliga a anticiparlas y manejarlas, para que nuestro programa pueda reaccionar adecuadamente a estas circunstancias inesperadas.
En cambio, las excepciones no controladas, como ArithmeticException que ocurre al dividir por cero, son indicativas de errores en nuestro propio código. Son excepciones que derivan de la clase RuntimeException y no requieren que las manejemos explícitamente. Esto no significa que no podamos hacerlo, pero el lenguaje no nos obliga. La idea es que estas excepciones reflejan bugs que deberíamos corregir en lugar de simplemente capturarlas.
Para entender mejor esta jerarquía, podemos visualizarla así:
classDiagram
Throwable <|-- Exception
Throwable <|-- Error
Exception <|-- RuntimeException
RuntimeException <|-- ArithmeticException
Exception <|-- IOException
Aquí, Throwable es la clase base para todo lo que puede ser lanzado como excepción o error. Se divide en Error y Exception. Los errores son situaciones graves, como quedarse sin memoria, que no podemos manejar y que detienen la ejecución. Las excepciones son situaciones excepcionales que sí podemos tratar. Dentro de las excepciones, las que derivan directamente de Exception son las controladas, y las que derivan de RuntimeException son las no controladas.
Volviendo a nuestro ejemplo, el método que lee un archivo debe manejar o declarar la IOException porque es una excepción controlada. Si no lo hacemos, el código no compilará. Por otro lado, el método que divide dos números puede lanzar una ArithmeticException si el denominador es cero, pero no estamos obligados a capturarla o declararla. Sin embargo, si queremos evitar que el programa falle en tiempo de ejecución, lo ideal es validar el denominador antes de hacer la división, por ejemplo:
public int dividir(int numerador, int denominador) {
if (denominador == 0) {
throw new IllegalArgumentException("El denominador no puede ser cero");
}
return numerador / denominador;
}
Así, prevenimos el error en lugar de capturarlo después.
En resumen, las excepciones controladas suelen reflejar problemas externos al programa, como recursos que no están disponibles o fallos en el entorno, y por eso Java nos obliga a tratarlas. Las excepciones no controladas indican errores en nuestro código que deberíamos corregir para evitar que ocurran. Esta distinción es fundamental para escribir código robusto y entender cómo manejar las excepciones en Java.