jueves, 9 de mayo de 2013

Excepto por la Excepción....

¿Por qué excepciones?


Resulta que las excepciones son mecanismos que emplean los lenguajes de programación para manejar los posibles errores que puede tener una aplicación que estamos construyendo. Muchas veces, de manera intuitiva definimos ciertas convenciones para expresar que un procedimiento no cumplió su objetivo. Consideremos el siguiente algoritmo de búsqueda:
 

 public static int buscarPersona(int id, Persona[] conjuntoPersonas)
 {
  for (int i = 0; i < conjuntoPersonas.length; i++) 
  {
   if(id == conjuntoPersonas[i].getId())
   {
    return i;
   }
  }
  return -1;
 }

La anterior búsqueda retornaría la posición del arreglo donde se encuentra la persona que estamos buscando, sin embargo retornará -1 si esa persona no es encontrada en el arreglo. En este caso estamos utilizando el valor -1 como una convención que significa "No pude encontrar la persona en cuestión", lo cual es un comportamiento no esperado en la ejecución del método que estamos diseñando.

Así como el ejemplo anterior, los programadores nos valemos de un montón de convenciones para decir si una ejecución de un procedimiento fué exitosa o no; sin embargo manejar los casos de error de esta manera puede ser confuso cuando la complejidad de nuestros procedimientos es mayor. Además, las convenciones que utilizamos solo tienen significado para quien programó el procedimiento, razón por la cual quien llame nuestro método de búsqueda podría interpretar ese -1 como un resultado válido, lo cual puede desencadenar efectos inesperados (por ejemplo que el usuario de este método acceda a la posición -1 de ese arreglo).

La excepción salvando el día

Bueno, vamos a modificar el código de nuestra búsqueda para que avise que hubo un error de manera explícita, las excepciones son precisamente eso. 

 
public static int buscarPersona(int id, Persona[] conjuntoPersonas)throws Exception
{
 for (int i = 0; i < conjuntoPersonas.length; i++) 
 {
  if(id == conjuntoPersonas[i].getId())
  {
   return i;
  }
 }
 throw new Exception("Persona no encontrada");
}

Ahora nuestro método persona tiene dos cambios, en su encabezado (firma del método) aparece la palabra throws que indica que nuestro método podría lanzar una excepción, seguido de esta palabra se escribe el tipo de excepción que puede lanzar, en este caso la excepción más básica representada en la clase Exception. También se modifica el lugar donde retornamos -1 y en vez de ello creamos y lanzamos la excepción que anunciamos, en este caso una que lleva como mensaje : "Persona no encontrada". Para ello usamos la palabra reservada throw e instanciamos un objeto de tipo Exception. La palabra throw interrumpe el flujo del programa, razón por la cual cualquier línea después no será ejecutada.

El lado del cliente (versión sin excepción)

Ahora supongamos que usamos la primera versión de nuestro método de búsqueda (la que no usa excepciones). Nuestro código se vería más o menos así:

public static void modificarPersona ( int id , String nuevoNombre, Persona[] conjuntoPersonas )
{
 int posicion = buscarPersona(id, conjuntoPersonas);
 if(posicion != -1)
 {
  Persona p = conjuntoPersonas[posicion];
  p.setNombre(nuevoNombre);
 }
}

Este método le cambia el nombre a una persona, previamente la busca usando nuestro método del ejemplo anterior. Nótese el if horrible que hace que no se pueda modificar la persona en caso que la búsqueda devuelva -1. Quien lee este código está obligado a saber que ese -1 significa "no lo encontró", lo cual empeora si no es la misma persona quien escribe los dos métodos, no hay documentación o peor aún el método buscarPersona está en otra clase de la cual no se dispone del código fuente.

Sin embargo, podría ser peor...

public static void modificarPersona ( int id , String nuevoNombre, Persona[] conjuntoPersonas )
{
 int posicion = buscarPersona(id, conjuntoPersonas);

 Persona p = conjuntoPersonas[posicion];
 p.setNombre(nuevoNombre);
 
}

En este caso ni siquiera se toma en cuenta que el método podría fallar, por ende podríamos estar accediendo a la posición -1 del arreglo y generando un error grave en nuestra aplicación. ¿Quién tiene la culpa?

El lado del cliente (con excepción) y las cláusulas try, catch y finally

Bueno, ahora usemos la versión decente (con excepción) del método de búsqueda. Una de las ventajas de usar excepciones es que el compilador de java nos obliga a tratarlas, evitando que se pase por alto el caso de error. Revisemos una versión de nuestro método cliente usando excepciones.

public static void modificarPersona ( int id , String nuevoNombre, Persona[] conjuntoPersonas )
{
 int posicion;
 try 
 {
  posicion = buscarPersona(id, conjuntoPersonas);
  Persona p = conjuntoPersonas[posicion];
  p.setNombre(nuevoNombre);
  
 } 
 catch (Exception e) 
 {
  e.printStackTrace();
  //esta linea imprime en la consola la
  //traza de llamados que llevaron al error
 }
}

La cláusula try encierra las instrucciones que pueden lanzar excepciones, de hecho si un método es llamado sin la mediación de esta palabra reservada, no podrá ser ejecutado (el compilador nos dará un error a menos que el método cliente también lance excepción mediante la palabra throws ). La cláusula catch nos sirve para ejecutar algunas instrucciones que sucederán si alguna de las lineas dentro del try lanza una expcepción. La idea es escribir dentro del catch las instrucciones que manejarán el error, en el ejemplo anterior imprimiremos la traza de llamados en la consola, también podríamos escribir un archivo de registro de errores o lanzar una nueva excepción. Con un nuevo lanzamiento nuestro código podría quedar así...

public static void modificarPersona ( int id , String nuevoNombre, Persona[] conjuntoPersonas )throws Exception
{
 int posicion;
 try 
 {
  posicion = buscarPersona(id, conjuntoPersonas);
  Persona p = conjuntoPersonas[posicion];
  p.setNombre(nuevoNombre);
 } 
 catch (Exception e) 
 {
   throw new Exception("Hubo un error en la modificacion de la persona: "+e.getMessage());
 }
}

En esta versión, además de capturar la excepción lanzamos una nueva que dice un mensaje más explícito. Teniendo en cuenta que nuestro método de búsqueda podría ser usado por más de un cliente, el mensaje original podría no ser suficiente, en este caso le damos más contexto diciendo que no solamente la búsqueda falló, sino que además fué invocada con la intención de modificar una persona.

Existe la posibilidad de ejecutar algunas líneas de código independientes de la existencia de una excepción o no dentro de una cláusula try. La cláusula finally ejecuta su contenido siempre, inclusive si no se usa un try , lo cual podría ser una buena práctica para aquellas cosas que no se deben dejar de hacer en ningun caso (por ejemplo cerrar un archivo o una conexión a la red). Aquí un ejemplo de su uso leyendo un archivo:


public void openFile()
{
 FileReader reader = null;
 try 
 {
  reader = new FileReader("someFile");
  int i=0;
  while(i != -1){
   i = reader.read();
   System.out.println((char) i );
  }
 } 
 catch (IOException e) 
 {
  e.printStackTrace();
 } 
 finally 
 {
  if(reader != null)
  {
   try 
   {
    reader.close();
   } 
   catch (IOException e) 
   {
    e.printStackTrace();
   }
  }
  System.out.println("--- File End ---");
 }
}
Independientemente de si sucede la excepción (en este caso IOException), se intentará cerrar el archivo que se está trabajando. Nótese que en este caso la invocación al método close también podría generar una excepción que requiere ser capturada igualmente.

Las clases Exception, Error y Throwable.

Como vimos en los primeros ejemplos, es relativamente típico usar la clase Exception, sin embargo dicha clase es una clase genérica y deberíamos usar hijas de ella (por medio de herencia) para clasificar los posibles tipos de excepción que nuestra aplicación sabe lanzar. En general es una buena práctica definir varios tipos de excepciones que sean dicientes del error que está sucediendo o del componente que lanza la excepción. Por ejemplo, se podrían separar las excepciones de lógica de las excepciones de interfaz de usuario y eso ya sería un buen avance.

Si revisamos la jerarquia de herencia de la clase Exception nos encontraremos con que es una extensión (hereda de) la clase Throwable y que existe una clase Error que es hermana de Exception. Normalmente se trabajan excepciones cuando el flujo del programa principal puede ser restaurado y se puede continuar trabajando, la clase Error se utilizará entonces cuando el problema sea crítico y no se pueda continuar usando la aplicación (por ejemplo cuando no hay memoria disponible). Esa elección depende principalmente del desarrollador, sin embargo cási todos los casos prácticos se pueden resolver usando solo excepciones.



Por otro lado está la clase Throwable, la cual es la única clase que puede ser "lanzada" mediante la cláusula throw, razón por la cual Exception y Error son hijas suyas. Si queremos personalizar excepciones o errores, les recomendaría que extiendan directamente estas dos clases.

Algunas excepciones conocidas

Para terminar este post, quisiera contarles algunas excepciones que se suelen utilizar o que son comunes en el desarrollo. Aquí una lista:


ArrayIndexOutOfBoundsException Se intenta acceder a una posición inválida en un arreglo o matriz
ClassCastException Se intenta hacer cast de tipo con un objeto que no es de ese tipo
NegativeArraySizeException Se intenta crear un arreglo de tamaño negativo (suena increible, pero si hicieron una excepción para eso es porque hay quien lo intenta)
NullPointerException Se intenta acceder a una variable cuyo valor es nulo
StringIndexOutOfBoundsException Lo mismo que un ArrayIndexOutOfBoundsException pero dentro de un String
ConcurrentModificationException Se intenta hacer una modificación y lectura de un elemento al mismo tiempo, típicamente aparece cuando se eliminan elementos de una lista que se esta recorriendo en un mismo ciclo
FileNotFoundException Se intenta abrir un archivo que no existe, típico error de ruta
IOException Error en la lectura o escritura, normalmente cuando se trabaja con archivos.
ConnectException Se intenta una conexión imposible, típico si se intenta conectar a un servidor caído o a una URL incorrecta

No hay comentarios:

Publicar un comentario