Las excepciones son eventos que ocurren durante la ejecución de un programa y hacen que éste salga de su flujo normal de instrucciones. Este mecanismo permite tratar los errores de una forma elegante, ya que separa el código para el tratamiento de errores del código normal del programa. Se dice que una excepción es lanzada cuando se produce un error, y esta excepción puede ser capturada para tratar dicho error.
Tenemos diferentes tipos de excepciones dependiendo del tipo de error que representen. Todas ellas descienden de la clase Throwable, la cual tiene dos descendientes directos:
Dentro de Exception, cabe destacar una subclase especial de excepciones denominada RuntimeException, de la cual derivarán todas aquellas excepciones referidas a los errores que comúnmente se pueden producir dentro de cualquier fragmento de código, como por ejemplo hacer una referencia a un puntero null, o acceder fuera de los límites de un array.
Estas RuntimeException se diferencian del resto de excepciones en que no son de tipo checked. Una excepción de tipo checked debe ser capturada o bien especificar que puede ser lanzada de forma obligatoria, y si no lo hacemos obtendremos un error de compilación. Dado que las RuntimeException pueden producirse en cualquier fragmento de código, sería impensable tener que añadir manejadores de excepciones y declarar que éstas pueden ser lanzadas en todo nuestro código.
Figura 1. Tipos de excepciones
Dentro de estos grupos principales de excepciones podremos encontrar tipos concretos de excepciones o bien otros grupos que a su vez pueden contener más subgrupos de excepciones, hasta llegar a tipos concretos de ellas. Cada tipo de excepción guardará información relativa al tipo de error al que se refiera, además de la información común a todas las excepciones. Por ejemplo, una ParseException se suele utilizar al procesar un fichero. Además de almacenar un mensaje de error, guardará la línea en la que el parser encontró el error.
Cuando un fragmento de código sea susceptible de lanzar una excepción y queramos tratar el error producido o bien por ser una excepción de tipo checked debamos capturarla, podremos hacerlo mediante la estructura try-catch-finally, que consta de tres bloques de código:
Para el bloque catch además deberemos especificar el tipo o grupo de excepciones que tratamos en dicho bloque, pudiendo incluir varios bloques catch, cada uno de ellos para un tipo/grupo de excepciones distinto. La forma de hacer esto será la siguiente:
try { // Código regular del programa // Puede producir excepciones } catch(TipoDeExcepcion1 e1) { // Código que trata las excepciones de tipo // TipoDeExcepcion1 o subclases de ella. // Los datos sobre la excepción los encontraremos // en el objeto e1. } catch(TipoDeExcepcion2 e2) { // Código que trata las excepciones de tipo // TipoDeExcepcion2 o subclases de ella. // Los datos sobre la excepción los encontraremos // en el objeto e2. ... } catch(TipoDeExcepcionN eN) { // Código que trata las excepciones de tipo // TipoDeExcepcionN o subclases de ella. // Los datos sobre la excepción los encontraremos // en el objeto eN. } finally { // Código de finalización (opcional) }
Si como tipo de excepción especificamos un grupo de excepciones este bloque se encargará de la captura de todos los subtipos de excepciones de este grupo. Por lo tanto, si especificamos Exception capturaremos cualquier excepción, ya que está es la superclase común de todas las excepciones.
En el bloque catch pueden ser útiles algunos métodos de la excepción (que podemos ver en la API de la clase padre Exception):
String getMessage() void printStackTrace()
con getMessage() obtenemos una cadena descriptiva del error (si la hay). Con printStackTrace() se muestra por la salida estándar la traza de errores que se han producido (en ocasiones la traza es muy larga y no puede seguirse toda en pantalla con algunos sistemas operativos).
Un ejemplo de uso:
try { ... // Aqui va el codigo que puede lanzar una excepcion } catch (Exception e) { System.out.println ("El error es: " + e.getMessage()); e.printStackTrace(); }
Hemos visto cómo capturar excepciones que se produzcan en el código, pero en lugar de capturarlas también podemos hacer que se propaguen al método de nivel superior (desde el cual se ha llamado al método actual). Para esto, en el método donde se vaya a lanzar la excepción, se siguen 2 pasos:
public void lee_fichero() throws IOException, FileNotFoundException { // Cuerpo de la función }
Podremos indicar tantos tipos de excepciones como queramos en la claúsula throws. Si alguna de estas clases de excepciones tiene subclases, también se considerará que puede lanzar todas estas subclases.
throw new IOException(mensaje_error);
public void lee_fichero() throws IOException, FileNotFoundException { ... throw new IOException(mensaje_error); ... }
Podremos lanzar así excepciones en nuestras funciones para indicar que algo no es como debiera ser a las funciones llamadoras. Por ejemplo, si estamos procesando un fichero que debe tener un determinado formato, sería buena idea lanzar excepciones de tipo ParseException en caso de que la sintaxis del fichero de entrada no sea correcta.
NOTA: para las excepciones que no son de tipo checked no hará falta la cláusula throws en la declaración del método, pero seguirán el mismo comportamiento que el resto, si no son capturadas pasarán al método de nivel superior, y seguirán así hasta llegar a la función principal, momento en el que si no se captura provocará la salida de nuestro programa mostrando el error correspondiente.
Además de utilizar los tipos de excepciones contenidos en la distribución de Java, podremos crear nuevos tipos que se adapten a nuestros problemas.
Para crear un nuevo tipo de excepciones simplemente deberemos crear una clase que herede de Exception o cualquier otro subgrupo de excepciones existente. En esta clase podremos añadir métodos y propiedades para almacenar información relativa a nuestro tipo de error. Por ejemplo:
public class MiExcepcion extends Exception { public MiExcepcion (String mensaje) { super(mensaje); } }
Además podremos crear subclases de nuestro nuevo tipo de excepción, creando de esta forma grupos de excepciones. Para utilizar estas excepciones (capturarlas y/o lanzarlas) hacemos lo mismo que lo explicado antes para las excepciones que se tienen definidas en Java.
La API de Reflection de Java representa (refleja) las clases, interfaces y objetos existentes en la actual JVM (Máquina Virtual de Java). Mediante este API podremos:
La API de reflection se compone de las siguientes clases. Salvo las clases Class y Object (que están en el paquete java.lang), el resto pertenecen al paquete java.lang.reflect.
Para cada clase e interfaz se mantiene un objeto Class que contiene información sobre la misma.
1. Obtener objetos Class
Podemos obtener objetos Class de varias formas:
Class c = unobjeto.getClass();
Class c = java.awt.Button.class;
Class c = Class.forName("java.awt.Button");
2. Obtener elementos de la clase
El método getName() de Class obtiene el nombre de la clase cuya información se guarda en el objeto Class, incluyendo los paquetes y subpaquetes a los que pertenece, separados por '.' (por ejemplo, "java.awt.Button"):
Class c = unobjeto.getClass(); ... String s = c.getName();
En Class también se tienen otros métodos para acceder a elementos de la clase, como por ejemplo getSuperClass() (para acceder a su superclase), y otros. A continuación veremos cómo acceder a algunos objetos específicos de una clase, mediante métodos de Class.
3. Campos
Para obtener los campos de una clase se tienen los métodos getField(), getFields(), getDeclaredField() y getDeclaredFields() del objeto Class. El primero y tercero devuelven un campo con un nombre dado, y el segundo y cuarto devuelven un array de Field con los campos accesibles que tenga. Los métodos ...Declared... obtienen todos los campos declarados en la misma clase, ya sean públicos o privados, pero no en las superclases que tenga, mientras que los otros dos obtienen los campos públicos que ofrece el objeto tanto si se han definido en la misma clase como en superclases de ésta.
Class c = unobjeto.getClass(); ... Field campo = c.getField("nombrecampo"); Field[] campos = c.getFields();
En la clase Field se tienen luego métodos para manipular cada campo.
4. Constructores
Para obtener los constructores de una clase se tienen, entre otros, los métodos getConstructors() y getDeclaredConstructors(). Con el primero obtendremos todos los constructores declarados en la misma clase, ya sean públicos o privados, pero no en las superclases que tenga, mientras que en el segundo obtendremos los constructures públicos que ofrece el objeto tanto si se han definido en la misma clase como en superclases de ésta:
Class c = unobjeto.getClass(); ... Constructor[] constructores = c.getDeclaredConstructors();
En la clase Constructor se tienen luego métodos para acceder a la información del constructor (nombre, parámetros, tipos de los parámetros, etc). Con el método newInstance() de Constructor podremos crear objetos llamando al constructor correspondiente.
5. Métodos
Para obtener los métodos de una clase se tienen, entre otros, los métodos getMethods() y getDeclaredMethods(). Con el primero obtendremos todos los métodos declarados en la misma clase, ya sean públicos o privados, pero no en las superclases que tenga, mientras que en el segundo obtendremos los métodos públicos que ofrece el objeto tanto si se han definido en la misma clase como en superclases de ésta:
Class c = unobjeto.getClass(); ... Method[] metodos = c.getDeclaredMethods();
En la clase Method se tienen luego métodos para acceder a la información del método (nombre, parámetros, tipos de los parámetros, etc). Con el método invoke() de Method podremos llamar al método correspondiente.
6. Modificadores
Para obtener los modificadores de una clase (es decir, ver si es public, abstract, final...), se tiene el método getModifiers() del objeto Class:
Class c = unobjeto.getClass(); ... int mod = c.getModifiers();
El método devuelve un entero que codifica los modificadores de la clase. Luego podemos utilizar los métodos de la clase Modifier para ver si es public, abstract, etc:
Class c = unobjeto.getClass(); ... int mod = c.getModifiers(); ... if (Modifier.isPublic(mod)) ... else if (Modifier.isAbstract(mod)) ...
Podemos utilizar la clase Modifier para obtener los modificadores de campos (Field), constructores (Constructor), métodos (Method), etc, sin más que llamar al método getModifiers() de cada una de estas clases, y luego llamar a los métodos de Modifier para comprobar si se tienen cada uno de los modificadores que se quieran. Se tienen métodos en la clase Modifier para cada uno de los posibles modificadores:
Class c = unobjeto.getClass(); Method[] metodos = c.getMethods(); int mod = metodos[0].getModifiers(); ... if (Modifier.isPublic(mod)) ... else if (Modifier.isAbstract(mod)) ...
1. Crear nuevos objetos
Si no conocemos el nombre de una clase hasta tiempo de ejecución, tendremos que crear los objetos de esa clase mediante Reflection. Distinguimos dos tipos de creación de objetos:
Class c = Class.forName("NombreClase"); Object o = c.newInstance();
Class c = Class.forName("NombreClase"); Constructor[] cons = c.getConstructors(); Object[] parametros = new Object[2]; parametros[0] = new Integer(2); parametros[1] = "Hola"; Object o = cons[0].newInstance(parametros);
Luego podemos utilizar el Object para llamar a métodos, convertir a un objeto concreto, etc.
2. Acceso a campos
La clase Field cuenta con métodos para poder obtener o modificar el valor de los campos de un objeto:
Class c = Class.forName("NombreClase"); Object o = c.newInstance(); Field f = c.getField("campo1"); f.set(o, new Integer(3)); int valor = (Integer)(f.get(o));
A los métodos get() y set() se les pasa como primer parámetro el objeto a cuyo campo se accede. Se tienen métodos para obtener/establecer otros tipos de datos (int, char, double..., etc, y Object para el caso general).
3. Invocar métodos
El método invoke() de la clase Method permite llamar al método en cuestión. Se le pasan dos parámetros:
La conversión de los Object que se pasan en los parámetros al tipo de datos adecuado se realiza automáticamente. Por ejemplo, suponiendo que llamamos a un método que toma un String y un double:
Class c = Class.forName("NombreClase"); Object o = c.newInstance(); Method[] m = c.getMethods(); Object[] parametros = new Object[2]; parametros[0] = "Pepe"; parametros[1] = new Double(4.0); m.invoke(o, parametros);
NOTA: las llamadas a los métodos de Reflection lanzan algunas excepciones, dependiendo del método. Es conveniente ponerlas en bloques try-catch.
NOTA: Hay que hacer notar que, si podemos emplear herramientas más fáciles que Reflection, debemos utilizarlas. Por ejemplo, si sabemos que vamos a crear un objeto de tipo MiClase, debemos crear este objeto en tiempo de compilación (con un new MiClase()), y no poner código Reflection para crearlo en tiempo de ejecución.