No es un bug, es una característica no documentada

martes, 14 de abril de 2015

Programación. Control de errores. Las excepciones

23:34 Posted by Inazio , No comments

Concepto

Cuando programamos en un lenguaje escribimos instrucciones destinadas a:
è Implementar la solución a nuestro problema
è Detectar y resolver las posibles situaciones excepcionales que se puedan dar (no se encuentra un fichero, el usuario se equivoca al escribir, fallos de red o de bases de datos…)

Es como si nuestro programa pudiera recorrer dos caminos bien distintos. El “normal” y el “excepcional”.

Para tartar las situciaciones atípicas Java usa siempre el mismo protocol de actuación, basado en el uso de las excepciones.

Una excepción es un evento, que ocurre durante la ejecución del programa, y que interrumpe el flujo “normal” del mismo.

Ejemplo de excepciones:
è No se encuentra un fichero o no se puede abrir, leer…
è Una operación divide por cero o accede a una posición inexistente de una table
è Fallos de hardware (disco duro, tarjeta de red, video…)

Veamos el siguiente trozo de código

public class Linterna{
   private Bombilla b = null;
   private boolean encendida;
   private double porcentajeCargaPilas;
   public Linterna(){
     // Olvidamos crear la bombilla
     porcentajeCargaPilas = 100;
   }
   public void encender(){
     b.encender();
     encendida = true;
   }
}…

public class PruebaLinterna{
   public class void main(String args[]){
     Linterna lin = new Linterna();
     lin.encender(); // Provoca la excepción siguiente
   }
}
Este código provocará el siguiente error: EXCEPTION IN THREAD MAIN JAVA LANG NULLPOINTEREXCEPTION…

Protocolo de actuación

Cuando ocurre algún problema en la ejecución de un método se realiza lo siguiente:

1.      Se lanza una excepción, esto consiste en que:
a.       Se detiene el camino “normal” de la ejecución del programa
b.      El método en el que se produjo el problema crea  un objeto (objeto excepción) que contiene toda la información sobre el error y el estado del programa cuando éste ocurrió.
c.       A continuación el objeto creado es enviado (lanzado) a un modulo de la MVJ que actúa como gestor de la ejecución y que toma el control del programa.
2.      Se intent capturer la excepción lanzada
a.       A continuación el Gestor de la Ejecución tiene que buscar algún trozo de código capaz de manejar el error que se ha producido.
b.      Para ello, primero busca el código manejador de la excepción en el método que lanzó la excepción.
                                                              i.      Si lo encuentra entonces tratamos el error y el gestor de ejecución devuelve el control al programa que continua con la siguiente instrucción.
                                                            ii.      Si no lo encuentra entonces el Gestor de la Ejecución busca en el método que llamó al que provocó el error par aver si éste es capaz de solucionarlo.
c.       De esta forma la excepción se va prolongando por la cadena de método hasta que encuentra alguno que solucione el problema.
d.      Si el objeto excepción llega al método main y tampoco es capaz de manejarlo entonces se cierra el programa y se imprime la información de lo ocurrido.


En nuestro ejemplo ocurrió lo siguiente:


Resumiendo:
è El método en el que se produce el error crea y lanza (también se usa eleva o levanta) un objeto excepción al gestor de ejecución
è El gestor de ejecución tiene que capturer la excepción lanzada encontrando un trozo de código capaz de manejarla en la cadena de método llamados (propagación de la excepción).

El objeto excepción lanzado se comporta como una “patata caliente” que pasa de mano en mano (se propaga) hasta que alguien sea capaz de aguantarla (se captura) o si no se cae al suelo (se cierra el programa).

La jerarquía de objetos “lanzables”

Los problemas que provocan que se lancen una excepción los podemso clasificar en dos grupos:

è Predecibles. Hay algunas instrucciones que por su naturaleza el compilador las considera peligrosas, en el sentido de que se pueden lanzar una expceción. Ej: Las instrucciones que intentan abrir un fichero pueden lanzar una excepción porque no se encuentre el fichero o no tengamos permiso.
è No predecibles. Son problemas que el compilador no puede prever porque ocurren durante la ejecución. Se deben a:
o   Fallos de programación. Se accede a un puntero nulo, se divide por cero un entero, se accede a una posición inexistente de una table…
o   Fallos del software o del hardware del sistema. Falla la máquina virtual java o el disco duro…

Java proporciona la siguiente jerarquía de clases:

Tratamiento de una excepción

El siguiente código no compila


import java.io.*;
public class ListaDeNumeros{
   private int vector[];
   private int tamanio = 10;

   public ListaDeNumeros{
     vector = new int[tamanio];
     for (int i = 0; i < tamanio; i++)
         vector[i] = i;
   }

   public void escribeLista(){
     PrintWriter fichTexto = new PrintWriter(“Lista.txt”);
     for (int i = 0; i < tamanio; i++)
         fichTexto.println(“El valor de ” + i + “ = ” + vector[i]);
     fichTexto.close();
   }
}

El compilador nos dice:
<< Tipo de excepción FileNotFundException no manejada >>

El compilador ha identificado un problema potencial (o predecible) en el método escribeLista y nos oblige a elegir entre:
è Afrontar el problema. Es decir, capturer la excepción escribiendo un trozo de código que la maneje
è Ignorar el problema. No escribimos código para capturer la excepción, simplementen dejamos que se propague al siguiente método de la cadena de llamadas.

Si elegimos afrontar el problema entonces tenemos que capturer la excepción. Para ello hay que encerrar la instrucción peligrosa en un bloque try (intentar) y su solución en un bloque catch (capturar).

try{
   …
   Instrucción peligrosa
   …
}
catch(TipoDeExcepción e){
   tratamiento del problema
}

En nuestro ejemplo, ahora modifcamos escribeLista para que captrue la excepción:

… public void escribeLista(){
   try{
     PrintWriter fichTexto = new PrintWriter(“lista.txt”);
     for(int i = 0; i < tamanio; i++)
         fichTexto.println(“El valor de ” + i + “ = “ + vector[i]);
     fichTexto.close();
   }
   catch(FileNotFoundException e){
     System.out.println(“Error al abrir el fichero lista.txt”);
     e.printStackTrace(); // Saca los errores por consola de Java
   }
}


Probamos la clase ListaDeNumeros con

public class PruebaListaDeNumeros(){
   public static void main(String[] args){
     ListaDeNumeros lista = new ListaDeNumeros();
     lista.escribeLista();
   }
}

Ahora vamos a provocar el fallo. Para ello, vamos a proteger contra escritura el fichero lista.txt y volvemos a ejecutar.

Si elegimos ignorarlo debemos saber que para que un método ignore una excepción hay que indicarlo en su prototipo. En nuestro ejemplo:

public void escribeLista() throws FileNotFoundException{
   PrintWriter fichTexto = new PrintWriter(“lista.txt”);
   For (int i = 0; i < tamanio; i++)
     fichTexto.println(“El valor de ” + i + “ = “ + vector[i]);
   ficTexto.close();
}

Así se informa al compilador de que el método lanza y propaga la excepción, no la captura.

Con la nueva versión de escribeLista ahora sería main quien tiene que decidir entre las dos opciones:

Ignorar de nuevo la excepción

public class PruebaListaDeNumeros(){
   public static void main(String [] args) throws FileNotFoundException{
     ListaDeNumeros lista = new ListaDeNumeros();
     lista.escribeLista();
   }
}

O capturarla

public class PruebaListaDeNumeros(){
   public static void main(String [] args){
     ListaDeNumeros lista = new ListaDeNumeros();
     try{
         lista.escribeLista();
     }
     catch(FileNotFoundException e){
         System.out.println(“Error al abrir lista.txt”);    
         e.printStackTrace();
     }
   }
}

Excepciones comprobadas y no comprobadas

El compilador sólo nos obliga a elegir entre capturar o ignorar una excepción si ésta se corresponde con un problema predecible.

Esto divide las excepciones en dos grupos:

è Las comprobadas por el compilador (checked exceptions)
è Y las no comprobadas (unchecked exceptions)

Las excepciones no comprobadas se producen en tiempo de ejecución y son muy variadas y numerosas, si el compilador nos obligará a tratarlas se reduciría mucho la claridad del código.

Aunque no es muy común, nada nos impide capturar o ignorar una excepción no comprobada.

Tratamiento de varias excepciones

Cuando un bloque de código puede lanzar varias excepciones podemos:
è Ignorarlas todas
è Ignorar algunas y capturar todas
è Capturarlas todas

Java permite ignorar varias excepciones comprobadas siempre que las especifiqumos en el prototipo del método.

modVisib tipoRetorno nombre() throws exc1, exc2, …{ … }
Ejemplo

public void visualizar(String nombre) throws FileNotFoundException, IOException{ … }

Para capturar varias excepciones podemos escribir varios bloques catch para un mismo try.

Sintaxis resultante

try{
   // Bloque de instrucciones que puede lanzar varias excepciones
}
catch(ClaseExcepcion1 excep1){
   // Tratamiento excep1
}
catch(ClaseExcepcion2 excep2){
   // Tratamiento excep2
}
catch(ClaseExcepcion3 excep3){
   // Tratamiento excep3
}

Ademas los bloques try … catch se pueden anidar y repetirse tantas veces como se quiera.

Veamos el siguiente ejemplo

import java.io.*;
public class ManejaFicheroTexto{
   private FileReader manejadorFichero;

   public void visualiza(String nombreFichero){
     int carácter;
     try{
         // Se intenta abrir el fichero y puede que lance un FileNotFoundException
         manejadorFichero = new FileReader(nombreFichero);
         carácter = manejadorFichero.read(); // Puede lanzar una IOException
         while(caracter != -1){
              System.out.write(caracter);
              Carácter = manejadorFichero.read(); // Puede lanzar un IOException
         }
         manejadorFichero.close(); // Puede lanza un IOException
     }
     catch(FileNotFoundException e){
         System.out.println(“Error al abrir el fichero ” + nombreFichero);
     }
     catch(IOException){
         System.out.println(“Error al leer o cerrar el fichero” + nombreFichero);
         e.printStackTrace();
     }
   }
}

Si se produce una excepción el Gestor de Ejecución comprueba los bloques catch pororden de aparición preguntando si la excepción lanzada “encaja” con algún catch.

Recordemos que:
è El operador instanceof devuelve cierto si el objeto comparado es una instancia de la clase indicada o de alguna de sus subclases.
è Por otro lado, en nuestro ejemplo tenemos la siguiente jerarquía de clases.


Regla del orden de los bloques catch

Debemos capturar las excepciones de las más concretas a más generales.

Otra forma de enunciar la regla sería “Debemos capturar las excepciones desde las hojas a la raíz del árbol de objetos”.


El bloque finally

El bloque finally acompaña al bloque try y se escribe al final de los bloques catch (si existen).

Sintaxis:

try{
   …
}
finally{
   …
}


try{
   …
}
catch(tipoExcepcion e1){
   …
}
catch(tipoExcepcion e2){
   …
}
finally{
   …
}

El bloque finally se ejecutará siempre que se ejecute el bloque try que acompaña, se produza o no una excepción.

El útil cuando queremso cerrar recursos (ficheros, conexiones a una base de datos). En nuestra clase ManejaFicherosTexto podríamsos hacer:

public void visualiza(String nombreFichero){
   int caracter;
   try{
     manejadorFichero = new FileReader(nombreFichero);
     caracter = manejadorFichero.read();
     while (caracter != -1){
         System.out.write(caracter);
         caracter = manejadorFichero.read();
     }
   }
   catch(FileNotFoundException e){
     System.out.println(“Error al leer el fichero ” + nombreFichero);
   }
   catch(IOException e){
     System.out.println(“Error al leer o cerrar el fichero” + nombreFichero);
     e.printStackTrace();
   }
   finally{
     if(manejadorFichero != null)
         manejadorFichero.close();
   }
}

La creación de excepciones de usuario

Al crear una clase tenemos que pensar tanto en su funcionalidad como en las posibles situaciones anómalas o malos usos a los que se puede enfrentar.
Java permite crear y lanzar excepciones propias del usuario simplemente heredando de Exception o de alguna de sus subclases.

De esta forma durante el desarrollo de un sistema debemos pensar en que excepciones vamos a utilizar, cómo se podrían jerarquizar y en que condiciones se van a lanzar.

Seguiremos los siguientes pasos:
1.      Identificar las excepciones que necesitamos. Si hay una condición de los parámetros de entrada de un método o del estado del objeto que impide que dicho método se ejecute de forma correcta estamos ante una situación en la que posiblemente se lance una excepción.
2.      Escoger un nombre significativo. Para respetar el convenio se sigue la librería de clases de Java, las excepciones de usuario se suelen terminar con la palabra Exception.
Ejemplos: NumeroTarjetaInvalidoException, DniIncorrectoException, CocheSinCombustibleException…
3.      Elegir la clase de la que vamos a heredar y crear la nueva clase excepción. Tenemos que escoger a Exception o alguna de sus subclases como superclase de nuestra excepción. La elección de una u otra dependerá de la naturaleza de la excepción a crear. Sería conveniente estudiar si tiene sentida crear una superclase común a todas nuestras excepciones.
4.      Para lanzar la excepción desde un método debemos crear el objeto excepción y después lanzarlo. Ejemplo:
DniIncorrectoException e = new DniIncorrectoException{ … };
Throw e;

Aunque es más común escribirlo todo en una línea
throw new DniIncorrectoException{ … };

Volvemos con la clase Bombilla para poner un ejemplo.

Ahora queremos que nuestra clase pueda ser utilizada por otras clases y sean éstas las que decidan qué hacer en caso de errores / excepciones.

Siguiendo los anteriores pasos, nuestro código quedaría

public class BombillaFundidaException extends Exception{
   // Creamos dos versiones del constructor de la clase
   public BombillaFundidaException(){}
   public BombillaFundidaException(String mensaje){
     super(mensaje); // Llamamos al constructor del padre
   }
   // Heredamos el resto de métodos
}

public class Bombilla{
   private int potencia;
   private int numEncendidos;
   private boolean encendida;
   private boolean fundida;

   public Bombilla(int potencia){
     this.potencia = potencia;
   }

   public void apagar() throws BombillaFundidaException{
     if (fundida == true)
         throw new BombillaFundidaException();
     else
         encendida = false;
   }

   public void encender() throws BombillaFundidaException{
     if (fundida = true)
         throw new BombillaFundidaException();
     else{
         if (encendida == false){
              numEncendidos ++;
              if (numEncendidos == 1000){
                   fundida = true;
                   encendida = false;
                   throw new BombillaFundidaException(“Recien fundida”);
              }
         }   
     }
   }
   public boolean estaFundida(){
     return fundida
   }
}

Ahora al usar los métodos “peligrosos” el compilador nos obligará a capturar la excepción o ignorarla.

public class PruebaBombilla{
   public static void main(String args[]){
     Bombilla b = new Bombilla(100);

     try{
         b.encender();
         b.apagar();
     }
     catch(BombillaFundidaException e){
         System.out.println(“Bombilla fundida”);
         // SI hay mensaje hacer un getMensaje
     }
     …
   }
}

Consideraciones sobre las excepciones

Veamos algunas observaciones a tener en cuenta en el uso de excepciones:
è Si creamos una excepción de usuario y hacemos que extienda la clase RuntimeException entonces el compilador la considerará una excepción no comprobada y no nos obligará a capturarla o ignorarla.
Esto es posible, pero no recomendable, ya que obtenemos un código menos seguro.
è Los constructores pueden lanzar excepciones
è Los métodos especificados en una interfaz pueden lanzar excepciones
è Si tenemos una relación de herencia entre dos clases, la subclase hereda los métodos con las excepciones que lanzan.
è Si la subclase sobrescribe un método, lanza una excepción lanzanza siempre que ésta sea una subclase de la excepción especificada por el método de la superclase

Ventajas del uso de excepciones

Permite separar el código normal del código de manejo de errores, mejorando la legibilidad y comprensión del código (evitamos códigos muy anidados).

Se agrupan los errores en una jerarquía de clases de manera que permite la captura y tratamiento por grupos.

Además se evite el diseño y uso de los códigos de error que es un mecanismo bastante tedioso y propenso a provocar fallos de programación.
La propagación de una excepción lanzada por la cadena de llamadas permite que la capturemos en el método que queramos. De esta forma podemos distinguir métodos que se preocupan o no de los errores.

Permite al usuario crear sus propias excepciones mediante la herencia, de forma que se estandariza la forma de tratar los errores para cualquier clase.

0 comentarios:

Publicar un comentario