Personal Website

My Web: MindEchoes.com

Tuesday, October 20, 2009

Reflexion en Java

Tanto andar programando QuickDB, no podia faltar un Post explicando como jugar un poco con la API de reflection en Java.

Reflexión es un componente de la API Java la cual permite al código Java examinar y "reflexionar" sobre los componentes Java en tiempo de ejecución y para usar miembros reflexionados.
La Reflexión se usa para instanciar clases e invocar métodos usando sus nombres, un concepto que permite la programación dinámica. Clases, interfaces, métodos, campos, y constructores pueden ser todos descubiertos y usados en tiempo de ejecución.

Vamos al Codigo!!
En la API Java de Reflexión tenemos 2 clases muy importantes:
  • Field
  • Method
"Class" no es parte de esta API, pero es de vital importancia igual!

Estas por lo menos son de las que mas se suelen utilizar para interactuar con objetos a traves de reflexión, aunque si nos ponemos a ver, muchas de las cosas que podemos obtener de un objeto del tipo Class son muy interesantes...

De forma rapida como podemos ver estas cosas??
Creamos una instancia de una clase cualquier y hacemos:

Para ordernar un poco lo que sigue, los ejemplos a continuacion van a consistir en:
  1. Imprimir el nombre de los atributos de una Clase
  2. Leer el valor de los atributos utilizando los Metodos Getters.
  3. Creando una instancia de un Objeto (combinado con lectura de atributo para caso que retorna NULL)
  4. Escribiendo datos en un objeto utilizando los Metodos Setters.
  5. Como se pueden leer atributos heredados de una Clase Padre.

Comencemos:
1)
Tomemos como ejemplo la siguiente Clase sobre la que se aplicaran los siguientes ejemplos:

public class Entidad {

private int id;
private String nombre;
private String otroAtributo;
private OtraClase otra;

public Entidad(){}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getNombre() {
return nombre;
}

public void setNombre(String nombre) {
this.nombre = nombre;
}

public String getOtroAtributo() {
return otroAtributo;
}

public void setOtroAtributo(String otroAtributo) {
this.otroAtributo = otroAtributo;
}

public OtraClase getOtra() {
return otra;
}

public void setOtra(OtraClase otra) {
this.otra = otra;
}

}

Como se puede ver es una simple Clase con 4 Atributos (uno de ellos una referencia a otra Clase) y los respectivos metodos Getters y Setters para sus atributos.

Ahora queremos hacer un Ciclo cuyo fragmento de código puede ser parte del Programa que contiene a esta Clase o ajeno a esta mediante el cual podamos recorrer los atributos de la Clase e ir imprimiendolos por consola.
Esta es una de las Caracteristicas fundamentales de Reflexión, justamente el hecho de no tener la obligación de "conocer" a la Clase o el Objeto...
Los ejemplos del tipo "como setear un valor a un atributo" uno podria preguntarse: "para que quiero utilizar Reflexión cuando puedo hacer un [objeto.setAtributo(valor)]??"......
Es cierto... pero supongamos que estamos haciendo una Libreria que debe ser capaz de trabajar con todo tipo de objetos, tanto los creados por nosotros, como otras entidades de software que hagan uso de la misma funcionalidad, en ese caso es probable que gran cantidad de los objetos que interactuen con nuestra Libreria sean ajenos a nuestro conocimiento y podamos necesitar de la ayuda de Reflexión para este trabajo... cuando se entienden las caracteristicas y los usos que se le puede dar a esta API se pueden desarrollar cosas muy interesantes.

Imprimir el Nombre de los Atributos de una Clase:

import java.lang.reflect.Field;

public class App
{
public static void main( String[] args )
{

//Obtener Array con los Fields(Campos/Atributos)
//de la Clase
Field[] fields = Entidad.class.getDeclaredFields();

//Recorrer cada uno de los Campos en el Array
//e imprimir su Nombre
for(Field f : fields){
System.out.println(f.getName());
}

}
}
La Clase Field represta cada Campo o Atributo de la Clase.
El metodo "getDeclaredFields()" como se puede ver nos devuelve un Array con los respectivos Atributos de esa Clase, esta opción incluye los atributos public, protected, default (package) access, private, pero excluye los campos heredados.


2)
Supongamos ahora que queremos leer el Valor de estos atributos utilizando el Metodo "Get" de cada uno... y no sabemos cual es el nombre de estos metodos, pero asumimos que siguen la convencion de: "getNombre" donde cada palabra despues de "get" se escribe con la primera letra en mayuscula.
(esta idea de como realizar esta lectura la saque del Blog de Pablo Frias)

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class App
{
public static void main( String[] args )
{

//Obtener Array con los Fields(Campos/Atributos)
//de la Clase
OtraClase otra = new OtraClase();
Entidad entidad = new Entidad(5, "Diego", "Gato", otra);
Field[] fields = entidad.getClass().getDeclaredFields();

//Recorrer cada uno de los Campos en el Array
//e imprimir el Valor de cada Campo
for(Field f : fields){
//Obtener nombre de Atributo
String field = f.getName();

try{
//Obtener nombre de Metodo Getter
Method getter = entidad.getClass().getMethod("get" +
String.valueOf(field.charAt(0)).toUpperCase() +
field.substring(1));

//Llamo al Metodo especificado de este Objeto
//con un array con los respectivos Parametros
//En este caso al ser un Getter no recibe parametros
Object value = getter.invoke(entidad, new Object[0]);

System.out.println(value);
}catch(Exception ex){
ex.printStackTrace();
}
}

}
}
Como se puede ver en el Codigo, se crea un Objeto del tipo "Method" con el nombre del Metodo al que se lo intentara relacionar, y luego sobre ese objeto Metodo creado se invoca su operación asignada... en este caso obtener el valor del respectivo atributo.

3)
Ahora supongamos que en lugar de imprimir el Valor de cada Atributo, queremos imprimir el Tipo de dato que es.
Si en el caso de la Clase con la que estamos trabajando (Entidad), no se le setteara un valor para la referencia a la Clase "OtraClase", esta tendria por defecto NULL, y al intentar imprimir tanto su Valor como su Tipo de Dato nos arrojaria una excepción, para este podemos utilizar un metodo de la Clase "Method" que nos permite obtener el Tipo de Dato que devuelve ese Metodo, y llegado el caso de ser necesario para algun fin... en base a ese tipo de dato que nos devuelve, podriamos crear una instancia vacia de esa clase:

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class App
{
public static void main( String[] args )
{
Entidad entidad = new Entidad(5, "Diego", "Gato", null);
Field[] fields = entidad.getClass().getDeclaredFields();

for(Field f : fields){
String field = f.getName();

try{
Method getter = entidad.getClass().getMethod("get" +
String.valueOf(field.charAt(0)).toUpperCase() +
field.substring(1));

Object value = getter.invoke(entidad, new Object[0]);
//Si el objeto obtenido es NULL, intenta crear una
//instancia vacia de ese tipo de objeto.
if(value == null){
value = App.emptyInstance(value.getClass().getName());
}


}catch(Exception ex){
ex.printStackTrace();
}
}

}

public static Object emptyInstance(String type) {
//Este metodo crea una instancia de Class con el tipo
//de dato obtenido por parametro, y luego itera sobre
//los Constructores de esta Clase para intentar
//crear una Instancia en base a un Constructor Vacio
Object obj = null;
try {
Class clazz = Class.forName(type);

for (java.lang.reflect.Constructor con : clazz.getConstructors()) {
if (con.getParameterTypes().length == 0) {
obj = con.newInstance();
break;
}
}
} catch (Exception e) {
return null;
}

return obj;
}
}

4)

import java.lang.reflect.Method;
import java.util.ArrayList;

public class App
{
public static void main( String[] args )
{
Entidad entidad = new Entidad(5, "Diego", "Gato", null);
Field[] fields = entidad.getClass().getDeclaredFields();
String field = fields[1].getName();

try{
String setterName = "set" +
String.valueOf(field.charAt(0)).toUpperCase() +
field.substring(1);

//Crea Metodo Setter en base al Nombre establecido
//arriba siguiendo la misma convencio que para Get
Method setter = entidad.getClass().
getMethod(setterName, fields[1].getType());

//Crea un Array con los Parametros que se le pasaran
//al Metodo al ser invocado, la funcion se encarga
//de los casteos correspondientes.
ArrayList results = new ArrayList();
results.add("Otro Nombre!!");

//se invoca al Metodo del respectivo Objeto
//y pasando un Array con los Parametros
setter.invoke(entidad, new Object[]{results});


}catch(Exception ex){
ex.printStackTrace();
}

}

}
No mucho mas que explicar, se puede ver en los comentarios la funcionalidad de cada cosa y el comportamiento es bastante similar a lo que ya se venia explicando.

5)
Ahora para leer Atributos heredados de una Clase es otra historia, ya que al usar el metodo "getDeclaredFields" para obtener el nombre de los Campos, no nos devuelve aquellos que se esten heredando, pero por cuestiones de herencia, sabemos que estan ahi, y sabemos que estaran sus metodos publicos Getters y Setters tambien (de seguir esa practica), entonces... como podemos hacer para leerlos??:


import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class App
{
public static void main( String[] args )
{
OtraClase otra = new OtraClase();
Entidad entidad = new Entidad(5, "Diego", "Gato", null);

//De esta forma obtenemos un Array con los Campos
//de la Clase de la que se extiende Directamente, si
//a su vez quisieramos los atributos del Padre del Padre
//tendriamos que repetir este proceso de forma recursiva.
Field[] fields = entidad.getClass().getSuperclass().getDeclaredFields();

for(Field f : fields){
String field = f.getName();

try{
Method getter = entidad.getClass().getMethod("get" +
String.valueOf(field.charAt(0)).toUpperCase() +
field.substring(1));

//Armamos el Metodo Getter en base al Field obtenido
//de la Clase Padre, pero lo ejecutamos sobre la Clase
//Hija, ya que sabemos que heredara este Metodo, por lo
//tanto estara disponible aunque no podamos obtener
//directamente el nombre de su Campo
Object value = getter.invoke(entidad, new Object[0]);

System.out.println(value);

}catch(Exception ex){
ex.printStackTrace();
}
}
}

}

Estas son solo algunas de las cosas que se pueden hacer con Reflexión, quedan muchisimas cosas pendientes, y ni se hablo de las Anotaciones que pueden llegar a ser un recurso muy valioso para este tipo de Tareas... eso si... A no Abusar!! Todo trae aparejado alguna perdida de Performance... pero dependiendo de la situación pueden ser muchos mas los PROs que los CONTRAs...

4 comments:

Anonymous said...

En el ejemplo 3), al hacer el value=App.emptyInstance(value.getClass().getName()); salta un nullpointer, normal por otra parte porque solo lo hace si value es null.

fabkius said...
This comment has been removed by the author.
fabkius said...

Muy buen aporte, me aclaro mucho el tema de reflexiones! saludos.

Darwin Omar Guevara Diaz said...

Muy bueno tu post :...