Excepciones

En Java las instrucciones se ejecutan casi siempre en secuencia. Es decir que después de ejecutar una instrucción, la próxima será la que se encuentra inmediatamente abajo (o al lado). Existen tres excepciones a esta regla: la instrucción break, la instrucción return y la instrucción throw.

Cuando se ejecuta la instrucción break, la instrucción siguiente será la que sigue al ciclo que contenía ese break:

    ...
    while ( ... ) {
      ...
      if ( ... )
        break;
      ... instrucción A ...
    }
    ... instrucción B ...
Cuando se llega a ejecutar el break, la ejecución continúa con la instrucción B y no con A. De la misma forma, cuando se ejecuta un return, la ejecución prosigue en el método que llamó al actual.

La excepciones pueden ser vistas como una forma más avanzada de break, en donde se puede incluso retomar la ejecución en otro método. Por ejemplo, el mismo ejemplo anterior se puede reprogramar de la siguiente manera:

    ...
    try {
      while ( ... ) {
        ...
        if ( ... )
          throw new MiExcp();
        ... instrucción A ...
      }
    }
    catch(MiExcp e) {
    }
    ... instrucción B ...
MiExcp debe ser una subclase de Exception. La mayoría de las veces no contiene ni variables de instancia ni métodos útiles. En el ejemplo se puede definir simplemente como:
    class MiExcp extends Exception {
    }
Por supuesto, cuando se desea terminar un ciclo, la instrucción break es la más adecuada. Las excepciones se usan cuando se desea alterar el flujo normal de las instrucciones y por lo tanto, como su nombre lo indica, se emplean para casos excepcionales, como por ejemplo para indicar errores.

Para explicar su funcionamiento, supongamos que se desea ejecutar la instrucción C si el ciclo termina porque se cumple la condición del if, pero no la instrucción B. Y si el ciclo termina porque no se cumple la condición del while, se debe ejecutar B y no C. Entonces, esto se puede programar con throw:

    ...
    try {
      while ( ... ) {
        ...
        if ( ... )
          throw new MiExcp();
        ... instrucción A ...
      }
      ... instrucción B ...
    }
    catch(MiExcp e) {
      ... instrucción C ...
    }
Ahora supongamos que el ciclo tiene varias salidas posibles y en cada caso se deben ejecutar instrucciones distintas. Entonces se pueden usar varias excepciones diferentes como en el siguiente ejemplo:

    ...
    try {
      while ( ... ) {
        ...
        if ( ... )
          throw new MiExcpX();
        ...
        if ( ... )
          throw new MiExcpY();
        ...
      }
      ... instrucción B ...
    }
    catch(MiExcpX e) {
      ... instrucción X ...
    }
    catch(MiExcpY e) {
      ... instrucción Y ...
    }

Excepciones capturadas en otros métodos

Una excepción se puede lanzar en un método y capturar en el método que llamó a ese método (directa o indirectamente). El siguiente método busca la posición de un entero en un arreglo. Si no lo encuentra arroja la excepción NoEncontrado:

    int buscar(int x, int[] a) throws NoEncontrado {
      for (int i= 0; i<a.length; i++)
        if (x==a[i])
          return i;
      throw new NoEncontrado();
    }
Cuando dentro de un método es posible que se lanze una excepción, el encabezado del método debe especificar el nombre de la excepción mediante la cláusula throws.

Observe que si el entero no es encontrado el método no retorna -1. En ese caso lanza una excepción que puede ser capturada en el método que lo llamó:

    int[] a= ...;
    try {
      int pos= buscar(2001, a);
      System.out.println("Su posicion es: "+pos);
    }
    catch (NoEncontrado e) {
      System.out.println("no se encontro");
    }
A veces no es cómodo manejar la excepción en el método que llamó a buscar. En ese caso, este debe especificar la excepción con throws:

    void llamaABuscar() throws NoEncontrado {
      int[] a= ...;
      int pos= buscar(2001, a);
      System.out.println("Su posicion es: "+pos);
    }
Y así el que llama a llamaABuscar puede capturar esa excepción con un try ... catch o bien especificarla a su vez en el encabezado.

En el encabezado de un método es obligatorio especificar todas las excepciones que podrían ser lanzadas directamente por ese método o indirectamente desde otro método invocado por el primero. Esto se hace con:

    ... metodo( ... ) throws Excp1, Excp2, ... {
      ...
    }
Sin embargo, hay excepciones que no es necesario declarar en el encabezado de un método porque se supone que se relacionan más bien con errores de programación y no por causas ajenas al programa. Estas excepciones son todas las subclases de RuntimeException. Por ejemplo: NullPointerException, ClassCastException, ArrayIndexOutOfBoundsException, ArithmeticException, NumberFormatException (la que lanza Integer.parseInt). Y también las subclases de Error, como OutOfMemoryError (se acabó la memoria) y StackOverflowError (se acaba la memoria del stack debido a una recursividad infinita).

Las excepciones son objetos

El objeto que representa la excepción puede contener información útil. Por ejemplo, un método de nombre ``descartar'' podría verificar que un entero no se encuentre en un arreglo dado. En caso de que sí se encuentre, se lanza una excepción que incluye su posición.

    void descartar(int x, int[] a) throws Encontrado {
      for (int i= 0; i<a.length; i++)
        if (x==a[i])
          throw new Encontrado(i);
    }
Ahora el método retorna normalmente si se recorre todo el arreglo y no se encuentra x. La clase Encontrado debe declararse como:

    class Encontrado extends Exception {
      int pos;
      Encontrado(int pos) { this.pos= pos; }
    }
El siguiente código muestra cómo se usa el método descartar:

    int[] a= ...;
    try {
      descartar(2001, x);
      System.out.println("Correcto");
    }
    catch (Encontrado e) {
      System.out.println("Error, se encontro en la posición "+e.pos);
    }
Observe que el objeto que fue lanzado se captura en la variable que aparece en el catch. En realidad, lo que apararece entre paréntesis en el catch es como la declaración de un parámetro en el encabezado de un método. En tiempo de ejecución el parámetro es el objeto lanzado.

Lectura de archivos en Java

En Java, toda la entrada y salida (disco, la red, etc.) puede provocar excepciones debido a que un archivo no existe, el disco se echó a perder, porque alguien pateo el cable de la red, etc. Java fue diseñado para escribir software robusto, es decir que es capaz de manejar estos casos excepcionales. Por ejemplo, si el programa es un editor, para un usuario no es aceptable que si un archivo no es accesible por alguna razón, el programa termine con un mensaje de error y el usuario pierda todo lo que había ingresado. En este caso, se espera que el programa capture la excepción y le de al usuario la oportunidad de grabar el archivo en otro disco, por ejemplo.

El siguiente es un patrón de lectura de un archivo en disco:

    String nomArch= ...;
    try {
      BufferedReader lect= new BufferedReader(new FileReader(nomArch));
      String lin= lect.readLine();
      while(lin!=null) {
        System.out.println(lin);
        lin= lect.readLine();
      } 
    }
    catch (FileNotFoundException e) {
      System.err.println("Lo siento, no se encontro "+nomArch);
    }
    catch (IOException e) {
      System.err.println("Error de lectura en el archivo "+nomArch);
    }
En el código anterior, las expresiones marcadas en negritas pueden lanzar excepciones. Consulte la API de Java para averiguar que excepciones puede lanzar un método dado, o algún constructor.

Ejercicio: Averigue que excepción puede lanzar el constructor de la clase URL en el paquete java.net. Un URL significa Uniform Resource Locator y corresponde a las direcciones que Ud. escribe en la barra del navegador de Internet, como por ejemplo: http://www.dcc.uchile.cl/~lmateu.

Es importante notar que en Java la clase FileNotFoundException es una subclase de IOException. Cuando se lanza una excepción se busca en el método actual si hay algún try ... catch en ejecución. Si lo hay se revisan secuencialmente los catch para ver si el objecto lanzado es una instancia de la clase declarada en cada catch. Si no hay un try en ejecución o no se encuentra ninguna clase apropiada entonces se hace lo mismo en el método llamador. Y así hasta encontrar el catch que captura la excepción o hasta llegar al main. En este último caso se muestra en pantalla la lista de métodos invocados en el momento que se lanzó la excepción.

Entonces, es muy importante no alterar el orden de los catch. En el ejemplo, si se escribe:

    try {
      ...
    }
    catch (IOException e) {
      System.err.println("Error de lectura en el archivo "+nomArch);
    }
    catch (FileNotFoundException e) {
      System.err.println("Lo siento, no se encontro "+nomArch);
    }
El compilador va a reclamar porque el segundo catch nunca podrá capturar ninguna excepción. Si se lanzara un objeto de la clase FileNotFoundException, sería capturado por el catch de IOException, porque el objeto también es instancia de esa clase.

Por último, fijese bien en que las instrucciones que lanzan excepciones estén dentro del try. En el siguiente ejemplo, la IOException lanzada en (A) no es capturada por el try programado, porque se encuentra fuera:

    
    try {
      ...
    }
    catch (IOException e) {
      System.err.println("Error de lectura en el archivo "+nomArch);
    }
    lin= lect.readLine(); // (A)