Download 10. Identificación de Tipo

Document related concepts
no text concepts found
Transcript
10: Identificación de tipo
en tiempo de ejecución
La idea de la identificación de tipo en tiempo de ejecución (RTTI, por run-time
type identification) parece medianamente simple al principio: permite encontrar
el tipo exacto de un objeto cuando sólo se tiene una referencia al tipo base.
Por supuesto, la necesidad de RTTI revela un abundante conjunto de interesantes
(y a menudo desconcertantes) cuestiones de diseño OO (Orientado a Objetos), y
plantea preguntas fundamentales acerca de cómo debería estructurar sus
programas.
Este capítulo apunta a las formas en que Java le permite descubrir informaci ón
acerca de objetos y clases en tiempo de ejecuci ón. Esto toma dos formas: RTTI
"tradicional", el cual asume que se tienen todos los tipos disponibles en tiempo de
compilación y en tiempo de ejecución, y el mecanismo " reflection ", el cual
permite averiguar información de clase únicamente en tiempo de ejecuci ón. El
RTTI "tradicional" será cubierto primero, seguido por una discusi ón acerca de
reflection .
La necesidad de RTTI
Considere el ahora familiar ejemplo de una jerarquía de clases que usa
polimorfismo. El tipo genérico es la clase base Shape (forma), y los tipos
específicos derivados son Circle (círculo), Square (cuadrado) y Triangle
(triángulo):
Este es un diagrama de jerarquía de clases típico, con la clase base en la cima y
las clases derivadas creciendo en forma descendiente. El objetivo normal en la
programación orientada a objetos es codificar la mayoría de su código haciendo
referencia a la clase base ( Shape , en este caso), de manera que si decide
extender el programa agregando una nueva clase ( Rhomboid , derivada de
Shape , por ejemplo), el grueso del código no sea afectado. En este ejemplo, el
método enlazado dinámicamente en la interfaz Shape es draw( ) (dibujar), de
manera que el intento es que el programador cliente llame a draw( ) por medio
de una referencia genérica a Shape . draw( ) está sobrescrito en todas las clases
derivadas y, dado que es un método enlazado dinámicamente, tendrá el
comportamiento adecuado aunque sea llamado a través de una referencia
genérica a Shape . Eso es polimorfismo.
De esta manera, generalmente usted crea un objeto de una clase específica
( Circle , Square o Triangle ), realiza una operación de molde hacia arriba
(upcast) a Shape (olvidando el tipo específico del objeto), y usa esa referencia
anónima a Shape en el resto del programa.
Como un breve repaso de polimorfismo y moldeado hacia arriba (upcasting),
podría codificar el ejemplo anterior de la siguiente manera:
//: c12:Shapes.java
import java.util.*;
class Shape {
void draw() {
System.out.println(this + ".draw()");
}
}
class Circle extends Shape {
public String toString() { return "Circle"; }
}
class Square extends Shape {
public String toString() { return "Square"; }
}
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
public class Shapes {
public static void main(String[] args) {
ArrayList s = new ArrayList();
s.add(new Circle());
s.add(new Square());
s.add(new Triangle());
Iterator e = s.iterator();
while(e.hasNext())
((Shape)e.next()).draw();
}
} ///:~
La clase base contiene un método draw( ) que usa indirectamente a toString( )
para imprimir un identificador de la clase pasando this a System.out.println( ) .
Si esa función ve un objeto, automáticamente llama al método toString( ) para
producir una representación como cadena ( String ).
Cada una de las clases derivadas sobrescribe el método toString( ) (de Object ),
de manera que draw( ) termina imprimiendo algo distinto en cada caso. En main
( ) , son creados tipos específicos de Shape y agregados a un ArrayList . Este es
el punto en el cual ocurre el molde hacia arriba (upcast) porque ArrayList puede
contener solamente Object s. Ya que todo en Java (con la excepción de los tipos
primitivos) es un Object , un ArrayList puede además contener objetos de tipo
Shape . Pero en el transcurso de un molde hacia arriba (upcast) a Object se
pierde además toda información específica, incluyendo el hecho de que los objetos
son Shape s. Para el ArrayList , son sólo Object s.
En el momento que usted saca un elemento del ArrayList con next( ) las cosas
se complican un poco. Puesto que ArrayList contiene sólo Object s, naturalmente
next( ) produce una referencia a Object . Pero nosotros sabemos que es
realmente una referencia a Shape y queremos enviar mensajes de Shape a ese
objeto. De manera que es necesario un molde a Shape , usando el molde (cast)
tradicional " (Shape) ". Esta es la forma más básica de RTTI, puesto que, en
Java, todos los moldes (casts) son controlados en tiempo de ejecución. Eso es
exactamente lo que RTTI significa: en tiempo de ejecución, el tipo de un objeto es
identificado.
En este caso, el molde RTTI es sólo parcial: se hace de Object a Shape y no
hasta el final, a Circle , Square o Triangle . Esto es porque lo único que
sabemos en este punto es que el ArrayList está lleno de Shape s. En tiempo de
compilación, esto es forzado sólo por reglas autoimpuestas, pero en tiempo de
ejecución es asegurado por el molde.
Ahora entra en juego el polimorfismo y el método exacto que es llamado para el
Shape es determinado de acuerdo a si la referencia es a un Circle , Square o
Triangle . Y, en general, esto es como debería ser; usted desea que el grueso de
su código sepa tan poco como sea posible acerca de los tipos espec íficos de los
objetos y lo justo para tratar con la representación general de una familia de
objetos (en este caso Shape ). Como resultado, su código será más fácil de
escribir, leer y mantener y sus diseños serán más fáciles de implementar,
entender y cambiar. De manera que el polimorfismo es la meta general de la
programación orientada a objetos.
¿Pero que pasa si usted tiene un problema de programación especial que es más
fácil de resolver si se conoce el tipo exacto de una referencia genérica? Por
ejemplo, suponga que usted desea permitirle a sus usuarios resaltar todas las
formas de un tipo particular volviéndolas púrpura. De esta manera, podemos
encontrar todos los triángulos en la pantalla resaltándolos. Esto es lo que RTTI
lleva a cabo: usted le puede preguntar a una referencia a Shape el tipo exacto al
cual está haciendo referencia.
El objeto Class
Para entender cómo trabaja RTTI en Java, debe saber primero cómo es
representada la información de tipo en tiempo de ejecución. Esto es llevado a cabo
por medio de una clase especial de objeto llamado el objeto Class , el cual
contiene información acerca de la clase. (Esto es algunas veces llamado
metaclase. ) De hecho, el objeto Class es usado para crear todos los objetos
"regulares" de su clase.
Hay un objeto Class por cada clase que es parte de su programa. Esto es, cada
vez que usted escribe y compila una nueva clase, un único objeto Class es creado
(y almacenado, en un archivo .class con el mismo nombre). En tiempo de
ejecución, cuando usted crea un objeto de esa clase, la máquina virtual de Java
(JVM), que está ejecutando su programa, primero chequea si el objeto Class para
ese tipo está cargado. Si no, lo carga encontrando el archivo .class con ese
nombre. De este modo, un programa Java no es cargado completamente antes de
comenzar, lo cual es diferente a muchos lenguajes tradicionales.
Una vez que el objeto Class para ese tipo está en memoria, es usado para crear
todos los objetos de ese tipo.
Si esto le parece sombrío o no lo cree realmente, aquí hay un programa de
demostración que puede probarlo:
//: c12:SweetShop.java
// Examen de la forma en que trabaja el cargador de
// clases (class loader).
class Candy {
static {
System.out.println("Cargando Candy");
}
}
class Gum {
static {
System.out.println("Cargando Gum");
}
}
class Cookie {
static {
System.out.println("Cargando Cookie");
}
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("dentro de main");
new Candy();
System.out.println("Después de crear Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
e.printStackTrace(System.err);
}
System.out.println(
"Después de Class.forName(\"Gum\")");
new Cookie();
System.out.println("Después de crear Cookie");
}
} ///:~
Cada una de las clases Candy (caramelo), Gum (chicle) y Cookie (galleta) tienen
una cláusula static que es ejecutada cuando la clase es cargada por primera vez.
Se imprimirá información para notificarle acerca de cuándo es cargada una clase.
En main( ) , las creaciones de objetos están esparcidas entre sentencias de
impresión, para ayudar a detectar el momento de carga.
Una línea particularmente interesante es:
Class.forName("Gum");
Este método es una miembro estático de Class (a la cual pertenecen todos los
objeto Class ). Un objeto Class es como cualquier otro, usted puede obtener y
manipular una referencia a él. (Eso es lo que el cargador hace.) Una de las
maneras de obtener una referencia al objeto Class es forName( ) , el cual toma
un String conteniendo el nombre textual (cuidado con el deletreo y la
capitalización!) de la clase particular de la cual usted quiere una referencia.
Retorna una referencia a Class .
La salida de este programa para una JVM es:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
Puede ver que cada objeto Class es cargado sólo cuando se necesita y la
inicializaci ón static es realizada en la carga de la clase.
Literales de clase
Java provee una segunda forma de producir una referencia al objeto Class ,
usando un literal de clase. En el programa anterior, esto podría verse como:
Gum.class;
Lo cual no es sólo más simple, sino también más seguro, ya que es controlado en
tiempo de ejecución. Debido a que elimina la llamada a un método, también es
más eficiente.
Los literales de clase trabajan con clases comunes, así como también con
interfaces, arreglos y tipos primitivos. En suma, hay un campo estándar llamado
TYPE que existe por cada una de las clases envoltura primitivas. El campo TYPE
produce una referencia al objeto Class para el tipo primitivo asociado, de esta
forma:
...es equivalente a...
boolean.class
Boolean.TYPE
char.class
Character.TYPE
byte.class
Byte.TYPE
short.class
Short.TYPE
int.class
Integer.TYPE
long.class
Long.TYPE
float.class
Float.TYPE
double.class
Double.TYPE
void.class
Void.TYPE
Prefiero usar la versión ". Class ", de ser posible, ya que es más consistente con
las clases regulares.
Controlando antes de realizar una operación de molde
(cast)
Hasta el momento, se ha visto que las formas de RTTI incluyen:
1. El molde cl ásico, por ejemplo " (Shape) ", el cual usa RTTI para asegurar que el molde
es correcto y lanza una ClassCastException si usted realizó un moldeado malo.
2. El objeto Class , representando el tipo de su objeto. El objeto Class puede ser
consultado para obtener información útil en tiempo de ejecución.
En C++, el molde clásico " (Shape) " no realiza RTTI. Simplemente le dice al
compilador que trate el objeto como si fuera del tipo especificado. En Java, donde
se realiza control de tipos, este molde es a menudo llamado "molde de tipo hacia
abajo seguro" (type safe downcast).
La razón para el término "molde hacia abajo" (downcast) es el ordenamiento
histórico del diagrama de jerarquía de clases. Si moldear un Circle a un Shape es
un molde hacia arriba (upcast), entonces moldear un Shape a un Circle es un
molde hacia abajo (downcast). Por supuesto, usted sabe que un Circle es además
un Shape y el compilador permite realizar una asignación de molde hacia arriba
libremente, pero no sabe si un Shape es necesariamente un Circle , de manera
que el compilador no permite realizar una asignaci ón de molde hacia abajo sin
usar un molde explícito.
Hay una tercera forma de RTTI en Java. Esta es la palabra clave instanceof que
le dice si un objeto es una instancia de un tipo particular. Retorna un boolean , de
manera que puede usarlo en forma de pregunta, de esta forma:
if(x instanceof Dog)
((Dog)x).bark();
La sentencia if anterior controla si el objeto x pertenece a la clase Dog antes de
aplicar a x un molde a Dog . Es importante usar instanceof antes de realizar un
molde hacia abajo cuando usted no tiene otra información que le diga el tipo del
objeto; de otra manera, terminará con una ClassCastException .
Comunmente, podría capturar un tipo (triángulos para volverlos púrpura, por
ejemplo), pero también podría fácilmente etiquetar todos los objetos usando
instanceof .
Suponga que tiene una familia de clases Pet :
//: c12:Pets.java
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; } ///:~
La clase Counter es usada para llevar el rastro de la cantidad de un tipo particular
de Pet . Podría pensar en él como un Integer que puede ser modificado.
Usando instanceof se pueden contar todos las mascotas:
//: c12:PetCount.java
// Usando instanceof.
import java.util.*;
public class PetCount {
static String[] typenames = {
"Pet", "Dog", "Pug", "Cat",
"Rodent", "Gerbil", "Hamster",
};
// Las excepciones son lanzadas a la consola:
public static void main(String[] args)
throws Exception {
ArrayList pets = new ArrayList();
try {
Class[] petTypes = {
Class.forName("Dog"),
Class.forName("Pug"),
Class.forName("Cat"),
Class.forName("Rodent"),
Class.forName("Gerbil"),
Class.forName("Hamster"),
};
for(int i = 0; i < 15; i++)
pets.add(
petTypes[
(int)(Math.random()*petTypes.length)]
.newInstance());
} catch(InstantiationException e) {
System.err.println("No se puede instanciar");
throw e;
} catch(IllegalAccessException e) {
System.err.println("No se puede acceder");
throw e;
} catch(ClassNotFoundException e) {
System.err.println("No se puede encontrar la clase");
throw e;
}
HashMap h = new HashMap();
for(int i = 0; i < typenames.length; i++)
h.put(typenames[i], new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.get(i);
if(o instanceof Pet)
((Counter)h.get("Pet")).i++;
if(o instanceof Dog)
((Counter)h.get("Dog")).i++;
if(o instanceof Pug)
((Counter)h.get("Pug")).i++;
if(o instanceof Cat)
((Counter)h.get("Cat")).i++;
if(o instanceof Rodent)
((Counter)h.get("Rodent")).i++;
if(o instanceof Gerbil)
((Counter)h.get("Gerbil")).i++;
if(o instanceof Hamster)
((Counter)h.get("Hamster")).i++;
}
for(int i = 0; i < pets.size(); i++)
System.out.println(pets.get(i).getClass());
for(int i = 0; i < typenames.length; i++)
System.out.println(
typenames[i] + " cantidad: " +
((Counter)h.get(typenames[i])).i);
}
} ///:~
Hay una restricción más bien ligada a instanceof : puede usarlo para comparar
con un tipo nombrado solamente y no con un objeto Class . En el ejemplo
anterior, usted puede sentir que es tedioso escribir todas esas expresiones
instanceof y estaría en lo correcto. Pero no hay forma de automatizar
ingeniosamente instanceof creando un ArrayList de objetos Class y realizando
la comparación con ellos, en cambio (mant éngase en sintonía - verá una
alternativa). Esto no es una gran restricción, como usted podría pensar, porque
eventualmente usted entenderá que su diseño es probablemente defectuoso si
termina escribiendo muchas expresiones instanceof .
Por supuesto, este ejemplo es inventado - probablemente usted ponga un
miembro estático en cada tipo y lo incremente en el constructor para seguir el
rastro de las cantidades. Usted podría hacer algo como esto si tiene control sobre
el código fuente de la clase y puede cambiarlo. Ya que este no es siempre el caso,
RTTI puede resultar conveniente.
Usando literales de clase
Es interesante ver cómo el ejemplo PetCount.java puede ser escrito nuevamente
usando literales de clase. El resultado es más claro, en muchos sentidos:
//: c12:PetCount2.java
// Usando literales de clase.
import java.util.*;
public class PetCount2 {
public static void main(String[] args)
throws Exception {
ArrayList pets = new ArrayList();
Class[] petTypes = {
// Literales de clase:
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Desplazamiento en 1 para
// eliminar a Pet.class:
int rnd = 1 + (int)(
Math.random() * (petTypes.length - 1));
pets.add(
petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {
System.err.println("No se puede instanciar");
throw e;
} catch(IllegalAccessException e) {
System.err.println("No de puede acceder");
throw e;
}
HashMap h = new HashMap();
for(int i = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(),
new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.get(i);
if(o instanceof Pet)
((Counter)h.get("clase Pet")).i++;
if(o instanceof Dog)
((Counter)h.get("clase Dog")).i++;
if(o instanceof Pug)
((Counter)h.get("clase Pug")).i++;
if(o instanceof Cat)
((Counter)h.get("clase Cat")).i++;
if(o instanceof Rodent)
((Counter)h.get("clase Rodent")).i++;
if(o instanceof Gerbil)
((Counter)h.get("clase Gerbil")).i++;
if(o instanceof Hamster)
((Counter)h.get("clase Hamster")).i++;
}
for(int i = 0; i < pets.size(); i++)
System.out.println(pets.get(i).getClass());
Iterator keys = h.keySet().iterator();
while(keys.hasNext()) {
String nm = (String)keys.next();
Counter cnt = (Counter)h.get(nm);
System.out.println(
nm.substring(nm.lastIndexOf('.') + 1) +
" cantidad: " + cnt.i);
}
}
} ///:~
Aquí, el arreglo typenames ha sido removido a favor de obtener las cadenas con
nombre de tipo a partir del objeto Class . Note que el sistema puede distinguir
entre clases e interfaces.
Puede ver además que la creación de petTypes no necesita ser rodeada por un
bloque try ya que es evaluada en tiempo de compilación y de esta forma no
lanzará ninguna excepción, a diferencia de Class.forName( ) .
Cuando los objetos Pet son creados dinámicamnete, usted puede ver que el
número aleatorio está restringido entre uno y petTypes.length y no incluye el
cero. Esto es porque cero se refiere a Pet.class y probablemente un Pet genérico
no es interesante. Por supuesto, ya que Pet.class es parte de petTypes , el
resultado es que todas las mascotas son contadas.
Un instanceof dinámico
El método isInstance de Class provee una forma de llamar dinámicamente al
operador instanceof . Así, todas esas tediosas sentencias instanceof pueden ser
removidas en el ejemplo PetCount :
//: c12:PetCount3.java
// Usando isInstance().
import java.util.*;
public class PetCount3 {
public static void main(String[] args)
throws Exception {
ArrayList pets = new ArrayList();
Class[] petTypes = {
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Desplazamiento en 1 para
// eliminar a Pet.class:
int rnd = 1 + (int)(
Math.random() * (petTypes.length - 1));
pets.add(
petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {
System.err.println("No se puede instanciar");
throw e;
} catch(IllegalAccessException e) {
System.err.println("No se puede acceder");
throw e;
}
HashMap h = new HashMap();
for(int i = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(),
new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.get(i);
// Usando isInstance para eliminar
// expresiones instanceof individuales:
for (int j = 0; j < petTypes.length; ++j)
if (petTypes[j].isInstance(o)) {
String key = petTypes[j].toString();
((Counter)h.get(key)).i++;
}
}
for(int i = 0; i < pets.size(); i++)
System.out.println(pets.get(i).getClass());
Iterator keys = h.keySet().iterator();
while(keys.hasNext()) {
String nm = (String)keys.next();
Counter cnt = (Counter)h.get(nm);
System.out.println(
nm.substring(nm.lastIndexOf('.') + 1) +
" cantidad: " + cnt.i);
}
}
} ///:~
Usted puede ver que el método isInstance( ) ha eliminado la necesidad de
expresiones instanceof . En suma, esto significa que usted puede agregar nuevos
tipos de mascotas simplemente cambiando el arreglo petTypes ; el resto del
programa no necesita modificación (la cual si necesita cuando se usan expresiones
instanceof ).
Equivalencia instanceof vs. Class
Cuando se consulta acerca de informaci ón de tipo hay una diferencia importante
entre ambas formas de instanceof (esto es, instanceof e isInstance( ) , los
cuales producen los mismos resultados) y la comparaci ón directa de los objetos
Class . Aquí hay un ejemplo que demuestra la diferencia:
//: c12:FamilyVsExactType.java
// La diferencia entre instanceof y class
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
System.out.println("Probando x de tipo " +
x.getClass());
System.out.println("x instanceof Base " +
(x instanceof Base));
System.out.println("x instanceof Derived " +
(x instanceof Derived));
System.out.println("Base.isInstance(x) " +
Base.class.isInstance(x));
System.out.println("Derived.isInstance(x) " +
Derived.class.isInstance(x));
System.out.println(
"x.getClass() == Base.class " +
(x.getClass() == Base.class));
System.out.println(
"x.getClass() == Derived.class " +
(x.getClass() == Derived.class));
System.out.println(
"x.getClass().equals(Base.class)) " +
(x.getClass().equals(Base.class)));
System.out.println(
"x.getClass().equals(Derived.class)) " +
(x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
} ///:~
El método test( ) realiza el control de tipo con su argumento usando ambas
formas de instanceof . Entonces, obtiene la referencia a Class y luego usa
equals( ) y == para probar la igualdad de los objetos Class . Aquí está la salida:
Testing x of type class Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
De modo tranquilizador, instanceof e isInstance( ) producen exactamente los
mismos resultados, como también ocurre con equals( ) y ==. Pero las pruebas
mismas bosquejan distintas conclusiones. Manteniéndose con el concepto de tipo,
instanceof dice "¿está usted en esta clase o en una clase derivada de esta?". Por
otro lado, si usted compara los objetos Class actuales usando ==, no hay
consideración con la herencia - es el tipo exacto o no lo es.
Sintaxis RTTI
Java ejecuta RTTI usando un objeto Class , aún si estamos haciendo algo como un
molde. La clase Class cuenta con un número de otros caminos en los que
podemos usar RTTI.
Primero, debemos hacer referencia a el objeto Class apropiado. Una forma de
hacer esto, como se muestra en el ejemplo anterior, es usar un String y el
método Class.forName() . Esto es conveniente porque no necesitamos un objeto
de ese tipo a fin de hacer referencia a la clase. Por supuesto, si ya se tiene un
objeto del tipo que nos interesa, podemos hacer referencia al objeto Class
llamando a un método que es parte de la clase raíz Object : getClass() . Este
retorna la referencia a Class que representa el tipo actual del objeto. Class tiene
muchos métodos interesantes, como se muestra en el siguiente ejemplo:
//: c12:ToyTest.java
// Probando la clase Class.
interface HasBatteries {}
interface Waterproof {}
interface ShootsThings {}
class Toy {
// Comente el siguiente constructor
// por defecto para ver
// NoSuchMethodError from (*1*)
Toy() {}
Toy(int i) {}
}
class FancyToy extends Toy
implements HasBatteries,
Waterproof, ShootsThings {
FancyToy() { super(1); }
}
public class ToyTest {
public static void main(String[] args) throws Exception {
Class c = null;
try {
c = Class.forName("FancyToy");
} catch(ClassNotFoundException e) {
System.err.println("No puedo encontrar FancyToy");
throw e;
}
printInfo(c);
Class[] faces = c.getInterfaces();
for(int i = 0; i < faces.length; i++)
printInfo(faces[i]);
Class cy = c.getSuperclass();
Object o = null;
try {
// Requiere constructor por defecto:
o = cy.newInstance(); // (*1*)
} catch(InstantiationException e) {
System.err.println("No se puede instanciar");
throw e;
} catch(IllegalAccessException e) {
System.err.println("No se puede acceder");
throw e;
}
printInfo(o.getClass());
}
static void printInfo(Class cc) {
System.out.println(
"Nombre de la clase: " + cc.getName() +
" es interfaz? [" +
cc.isInterface() + "]");
}
} ///:~
Podemos observar que la clase FancyToy es algo complicada, puesto que esta
hereda de Toy e implementa las interfaces HasBatteries , Waterproof y
ShootsThings . En main( ) , una referencia a Class es creada e inicializada al
Class de FancyToy usando forName( ) dentro de un bloque try apropiado.
El método Class.getInterfaces( ) retorna un arreglo de objetos Class que
representa las interfaces que están contenidas en el objeto Class que nos
interesa.
Si tenemos un objeto Class podemos preguntarle por la clase base directa usando
gerSuperclass( ) . Esto, por supuesto, retorna una referencia a Class a la cual
también se puede consultar. Esto significa que, en tiempo de ejecución , podemos
descubrir la jerarquía de clases completa de un objeto.
El método newInstance( ) de Class puede, al principio, parecer sólo otra forma
de clone( ) . Sin embargo, podemos crear un nuevo objeto con newInstance( )
sin un objeto existente, como se vi ó aquí, porque no hay un objeto Toy - sólo un
objeto cy , el cual es una referencia al objeto Class de y . Esta es una forma de
implementar un "constructor virtual", el cual permite que digamos "no se
exactamente de que tipo eres, pero créate a ti mismo de forma apropiada" . En el
ejemplo citado anteriormente, cy es sólo una referencia a Class sin ningún tipo de
información en tiempo de compilación. Y cuando creamos una nueva instancia,
obtenemos una referencia a Object . Pero esta referencia apunta a un objeto
Toy . Por supuesto, antes de poder enviar cualquier mensaje, aparte de los
aceptados por Object , tenemos que investigar un poco y hacer algún tipo de
molde. En suma, la clase que está siendo creada con newInstance( ) debe tener
un constructor por defecto. En la siguiente sección, verá cómo crear
dinámicamente objetos de clases usando cualquier constructor, con la API de Java
reflection .
El método final de la lista es printInfo( ) , que toma una referencia a Class y
obtiene el nombre con getName( ) y determina si es una interfaz con
isInterface( ) .
La salida del programa es:
Class
Class
Class
Class
Class
name:
name:
name:
name:
name:
FancyToy is interface? [false]
HasBatteries is interface? [true]
Waterproof is interface? [true]
ShootsThings is interface? [true]
Toy is interface? [false]
De esta manera, con el objeto Class podemos encontrar todo lo que queremos
conocer acerca de un objeto.
Reflection: Información de la clase en
tiempo de ejecución
Si no conocemos el preciso tipo de un objeto, RTTI nos lo puede decir.
Por supuesto, hay una limitación: el tipo debe ser conocido en tiempo de
compilación a fin de poder ser detectado usando RTTI y hacer algo útil con la
información. Para ponerlo de otra forma, el compilador debe saber acerca de todas
las clases con la que estamos trabajando para usar RTTI.
Esto no parece mucha limitación al principio, pero supongamos que debemos dar
una referencia a un objeto que no está en el ámbito de nuestro programa. De
hecho, ni siquiera la clase del objeto está disponible en tiempo de compilación de
nuestro programa. Por ejemplo, supongamos que tomamos un número de bytes
de un archivo en el disco o de una conexión de red y decimos que esos bytes
representan una clase. Dado que el compilador no conoce la clase mientras
compila el código, ¿cómo es posible que haga uso de la misma?
En un ambiente de programaci ón tradicional esto parece un escenario lejano. Pero
a medida que nos movemos dentro del amplio mundo de la programaci ón
aparecen casos importantes donde esto ocurre. El primero es la programaci ón
basada en componentes, en la cual podemos construir proyectos usando
herramientas rápidas de desarrollo de aplicaciones (Rapid Aplication Development
(RAD)) . Esto es una metodología visual para crear un programa (el cual se puede
ver en la pantalla como "formulario") moviendo de íconos que representan
componentes sobre el formulario. Estos componentes son entonces configurados
colocando algunos valores en tiempo de programación. Esta configuración en
tiempo de diseño requiere que todo componente sea instanciable, exponga parte
de sí mismo y permita que estos valores sean leídos y establecidos. En suma, los
componentes que manejan eventos GUI deben dar a conocer información acerca
de métodos apropiados, de manera que el entorno RAD pueda asistir al
programador en la tarea de sobreescribir estos método que manejan eventos.
Reflection provee el mecanismo para detectar los métodos disponibles y producir
los nombres de los métodos. Java nos da una estructura basada en programaci ón
de componentes a través de JavaBeans (descripta en el capítulo 13).
Otra motivación urgente para descubrir información de las clases en tiempo de
ejecución es proveer la capacidad de crear y ejecutar objetos en plataformas
remotas a través de la red. Esto es llamado Invocación Remota de Métodos
(Remote Method Invocation(RMI)) el cual permite a un programa Java tener
objetos distribuidos a través de varias máquinas. Esta distribución puede ocurrir
por muchas razones: por ejemplo, si estamos haciendo tareas de cálculo intensivo
y queremos dividir el trabajo y ponerlo sobre máquinas que están libres con el fin
de acelerar las cosas. En algunas situaciones podrímos querer colocar parte del
código que maneje tipos particulares de tareas (ej. Reglas de Negocio en una
arquitectura de cliente/servidor) en una máquina particular, de manera que la
misma se convierta en un repositorio común que describe esas acciones y puede
se cambiado fácilmente, afectando a todo el sistema. (Este es un desarrollo
interesante, ya que la máquina existe sólo para hacer cambios al software
fácilmente!) En este sentido, la computación distribuida soporta además hardware
especializado que podría ser bueno para una tarea en particular -inversión de
matrices, por ejemplo- pero inapropiado o muy costoso para programación de
propósito general.
La clase Class (descripta previamente en este capítulo) soporta el concepto de
reflection y hay una librería adicional, java.lang.reflect , con clases Field ,
Method , y Constructor (cada una de los cuales implementa la interfaz
Member ). Los objetos de ese tipo son creados por la JVM en tiempo de ejecuci ón
para representar el miembro correspondiente de la clase desconocida. Podemos
usar luego los Constructors para crear nuevos objetos, el método get( ) y set( )
para leer y modificar los campos asociados con objetos de tipo Field , y el método
Invoke( ) para llamar métodos asociados con objetos del tipo Method . En suma,
podemos llamar a conveniencia los métodos getField( ) , getMethods( ) ,
getConstructors( ) , etc., para retornar arreglos de objetos representando los
campos, métodos y constructores. (Podemos encontrar más buscando en la
documentación en línea de la clase Class ) De esta manera la información de clase
de objetos anónimos puede ser determinada completamente en tiempo de
ejecución, y no necesitamos saber nada de ellos en tiempo de compilación.
Esto es importante para caer en cuenta de no hay nada mágico acerca de
reflection. Cuando usemos reflection para interactuar con un objeto del cual no
conocemos el tipo, la JVM simplemente mirará el objeto y verá que pertenece a
una clase particular (como el RTTI común) pero entonces, antes de que pueda
hacer algo más, el objeto Class debe ser cargado. De esta manera, el
archivo .class para ese tipo particular todavía debe estar disponible para la JVM,
ya sea en el equipo local o a través de la red. De esta forma, la verdadera
diferencia entre RTTI y reflection es que con RTTI el compilador abre y examina el
archivo .class en tiempo de compilación. Para decirlo de otro modo, podemos
llamar métodos de un objeto en una forma "normal". Con reflection, el
archivo .class no está disponible en tiempo de compilación; este es abierto y
examinado en tiempo de ejecución.
Un extractor de métodos de clase
Con poca frecuencia necesitará usar las herramientas de reflection directamente,
ellos están en el lenguaje para dar apoyo a otras características de Java, como es
la serialización de objetos (Capítulo 11), JavaBeans(Capítulo13) y RMI (Capítulo
15). Por supuesto, hay momentos en los que es útil ser capaz de extraer
dinámicamente
información
acerca
de
una
clase.
Una
herramienta
extremadamente útil es el extractor de métodos de clase. Como se mencionó
antes, observando la definición de la clase en el código fuente o en la
documentación en línea vemos sólo los métodos definidos o sobreescritos dentro
de la definición de esa clase . Pero podría haber docenas de métodos disponibles
que provienen de las clases bases. Localizarlos es muy tedioso y consume tiempo.
Afortunadamente, reflection provee una forma de escribir una herramienta simple
que nos mostrará automáticamente la interfaz completa.
Así es como funciona:
//: c12:ShowMethods.java
// Usando reflection para mostrar todos los métodos de
// una clase, incluso si los métodos están definidos en
// la clase base.
import java.lang.reflect.*;
public class ShowMethods {
static final String usage =
"uso: \n" +
"ShowMethods qualified.class.name\n" +
"Para mostrar todos los métodos en una clase o: \n" +
"ShowMethods qualified.class.name palabra\n" +
"Para buscar todos lo métodos que contengan 'palabra'";
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class c = Class.forName(args[0]);
Method[] m = c.getMethods();
Constructor[] ctor = c.getConstructors();
if(args.length == 1) {
for (int i = 0; i < m.length; i++)
System.out.println(m[i]);
for (int i = 0; i < ctor.length; i++)
System.out.println(ctor[i]);
} else {
for (int i = 0; i < m.length; i++)
if(m[i].toString().indexOf(args[1])!= -1)
System.out.println(m[i]);
for (int i = 0; i < ctor.length; i++)
if(ctor[i].toString().indexOf(args[1])!= -1)
System.out.println(ctor[i]);
}
} catch(ClassNotFoundException e) {
System.err.println("No existe la clase: " + e);
}
}
} ///:~
Los métodos de Class , getMethods( ) y getConstructors( ) retornan un
arreglo de Method y Constructor , respectivamente. Cada una de estas clases
tienen más métodos para disecar los nombres, argumentos y valores que retornan
los métodos que representan. Pero también podemos hacer uso de toString( ) ,
como se hizo aquí, para producir un String con la firma competa del método. El
resto de código es sólo para extraer información de la línea de comando,
determinando si una firma particular concuerda con el String objetivo (usando
indexof( ) ) e imprimiendo los resultados.
Esto muestra a reflection en acción, ya que el resultado elaborado por
Class.forName() no puede ser conocido en tiempo de compilación, y por
consiguiente, toda la informació de firma de método es extraída en tiempo de
ejecución. Si investigamos la documentación en línea sobre reflection, veremos
que hay apoyo suficiente para establecer y hacer llamadas a un método sobre un
objeto que es totalmente desconocido en tiempo de compilación (veremos más
ejemplos luego en este libro). Otra vez, esto es algo que podríamos no necesitar
hacer nunca -el apoyo existe para RMI y para que el entorno de programaci ón
pueda manipular JavaBeans- pero es interesante.
Un experimento interesante es correr
java ShowMethods ShowMethods
Esto produce una lista que incluye un constructor por defecto público, aunque
podamos ver desde el código que no hay ningún constructor definido. El
constructor que vemos es unos de los que el compilador genera automáticamente.
Si después hacemos ShowMethods a una clase no pública (que es amigable), el
constructor por defecto generado no se muestra más en la salida. El constructor
por defecto generado tiene automáticamente el mismo acceso que la clase.
La salida para ShowMethods es aún un poco tediosa. Por ejemplo, esta es una
porción de la salida producida por invocar a java ShowMethods
java.lang.String :
java.lang.String:
public boolean
java.lang.String.startsWith(java.lang.String,int)
public boolean
java.lang.String.startsWith(java.lang.String)
public boolean
java.lang.String.endsWith(java.lang.String)
Podría ser mejor incluso si los calificadores como java.lang fueran desmenuzados.
La clase StreamTokenizer introducida en el capítulo anterior puede ayudar a
crear una herramienta para resolver este problema:
//: com:bruceeckel:util:StripQualifiers.java
package com.bruceeckel.util;
import java.io.*;
public class StripQualifiers {
private StreamTokenizer st;
public StripQualifiers(String qualified) {
st = new StreamTokenizer(
new StringReader(qualified));
st.ordinaryChar(' '); // Mantenga los espacios
}
public String getNext() {
String s = null;
try {
int token = st.nextToken();
if(token != StreamTokenizer.TT_EOF) {
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = null;
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = new String(st.sval);
break;
default: // Caracter simple en ttype
s = String.valueOf((char)st.ttype);
}
}
} catch(IOException e) {
System.err.println("Error en token");
}
return s;
}
public static String strip(String qualified) {
StripQualifiers sq =
new StripQualifiers(qualified);
String s = "", si;
while((si = sq.getNext()) != null) {
int lastDot = si.lastIndexOf('.');
if(lastDot != -1)
si = si.substring(lastDot + 1);
s += si;
}
return s;
}
} ///:~
Para facilitar la reutilización, esta clase es colocada en com.bruceeckel.util .
Como se puede ver, usa StreamTokenizer y manipulación de String para hacer
su trabajo.
La nueva versión del programa usa la clase anterior para limpiar la salida:
//: c12:ShowMethodsClean.java
// ShowMethods con los calificadores quitados
// para hacer los resultados más fáciles de leer.
import java.lang.reflect.*;
import com.bruceeckel.util.*;
public class ShowMethodsClean {
static final String usage =
"uso: \n" +
"ShowMethodsClean qualified.class.name\n" +
"Para mostrar todos los métodos en una clase o: \n" +
"ShowMethodsClean qualif.class.name palabra\n" +
"Para buscar todos lo métodos que contengan 'palabra'";
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class c = Class.forName(args[0]);
Method[] m = c.getMethods();
Constructor[] ctor = c.getConstructors();
// Convierte a un arreglo de Strings limpias:
String[] n =
new String[m.length + ctor.length];
for(int i = 0; i < m.length; i++) {
String s = m[i].toString();
n[i] = StripQualifiers.strip(s);
}
for(int i = 0; i < ctor.length; i++) {
String s = ctor[i].toString();
n[i + m.length] =
StripQualifiers.strip(s);
}
if(args.length == 1)
for (int i = 0; i < n.length; i++)
System.out.println(n[i]);
else
for (int i = 0; i < n.length; i++)
if(n[i].indexOf(args[1])!= -1)
System.out.println(n[i]);
} catch(ClassNotFoundException e) {
System.err.println("No existe la clase: " + e);
}
}
} ///:~
La clase ShowMethodsClean es bastante similar a ShowMethods , excepto en
que toma los arreglos de Method y Constructor y los convierte en un único
arreglo de String . Cada uno de esos objetos String es entonces pasado a través
StripQualifiers.Strip( ) para remover toda la calificación del método.
Esta herramienta puede realmente ahorrar tiempo mientras se está programando,
cuando no pueda recordar si una clase tiene un método particular y no quiere
caminar a través de la jerarquía de clases en la documentaci ón en línea, o si no se
sabe que se puede hacer con esa clase, por ejemplo, con los objetos Color .
El Capítulo 13 contiene una versión GUI de este programa (personalizado para
extraer informaci ón para componentes Swing) de manera que puede dejarlo
corriendo mientras escribe código y permitir un vistazo rápido.
Resumen
RTTI permite descubrir infromación de tipo a partir de una referencia a una clase
base anónima. Así, se presta para ser mal usado por parte de los novicios, ya que
podría ser utilizado en lugar de las llamadas polimórficas. Para muchas personas
provenientes del paradigma procedural, es difícil no organizar sus programas como
un conjunto de sentencias switch . Estas personas podrían hacer esto con RTTI y
perder de esta forma el importante valor del polimorfismo en el desarrollo y
mantenimiento de código. La intención de Java es que se use llamadas a métodos
polimórficos en su código, y usar RTTI sólo cuando se deba.
Por supuesto, usar llamadas a métodos polimórficos requiere que se tenga el
control de la definición de la clase base porque en algún punto de la extensión de
su programa podría descubrir que la clase base no incluye el método que necesita.
Si la clase base proviene de una librería o es de alguna forma controlada pro otra
persona, una solución al problema es RTTI: puede heredar un nuevo tipo y
agregar un nuevo método. En el resto del código se puede detectar el tipo
particular e invocar al método especial. Esto no destruye el polimorfismo y la
extensibilidad del programa porque agregar un tipo nuevo no requerirá buscar
sentencias switch en su código. Por supuesto, cuando agregue código en el
cuerpo principal que requiera las características nuevas, se deberá usar RTTI.
Poner una característica en una clase base podría significar que, para el beneficio
de una clase particular, todas las otras clases derivadas de esa clase base deberán
tener algún método sin sentido. Esto hace que la interfaz sea menos clara y
molesta a quienes deben sobrescribir métodos abtractos cuando derivan de esta
clase base. Por ejemplo, considere una jerarquía de clases que representa
insturmentos musicales. Suponga que quiere limpiar las válvulas de todos los
instrumetos apropiados en su orquesta. Una opción es colocar un método
clearSpitValve( ) en la case base Instrument , pero esto es confuso porque
implica que los instrumentos Percussion y Electronic también tienen válvulas.
RTTI provee una soluci ón mucho más razonable en este caso porque se puede
colocar el método en la clase específica ( Wind en este caso), donde es apropiado.
Por supuesto, una soluci ón más apropiada es colocar un método
prepareInstrument( ) en la clase base, pero podría no verlo de esta forma
cuando resuelve el problema por primera vez y erróneamente asumir que se debe
usar RTTI.
Finalmente, RTTI resolverá a veces problemas de eficiencia. Si su código usa bien
el polimorfismo, pero ocurre que uno de sus objetos reaccionan ante este código
de propósito general en una forma horriblemente ineficiente, usted puede detectar
ese tipo usando RTTI y escribir código específico para el caso a fin de mejorar la
eficiencia. Es una trampa seductora. Es mejor hacer que el programa funcione
primero y, entonces, decidir si está corriendo lo suficientemente rápido y, sólo
entonces, se debería atacar las cuestiones de eficiencia.
Ejercicios
Las soluciones a los ejercicios elegidos pueden encontrarse en el documento
electrónico The Thinking in Java Annotated Solution Guide, disponibles por un
pequeño monto en www.BruceEckel.com.
1. Agregue Rhomboid a Shapes.java . Construya un Rhomboid , aplíquele un molde
hacia arriba a Shape , luego aplíquele un molde hacia abajo a Rhomboid . Intente
aplicar un molde hacia abajo a Circle y vea que pasa.
2. Modifique el ejercicio 1 para que use instanceof para controlar el tipo antes de realizar
el molde hacia abajo.
3. Modifique Shapes.java para que se pueda "remarcar" (establecer una bandera) en todas
las figuras de un tipo particular. El método toString( ) para cada Shape derivada
debería indicar si la figura está "remarcada".
4. Modifique SweetShop.java para que cada tipo de creación de objeto sea controlada por
un argumento de la línea de comando. Esto es, si su línea de comando es "java SweetShop
Candy", entonces sólo el objeto Candy es creado. Note cómo usted puede controlar que
objetos Class son cargados vía el argumento de la línea de comando.
5. Agregue un nuevo tipo de Pet a PetCount3.java . Verifique que es creado y contado de
forma correcta en main( ) .
6. Escriba un método que tome un objeto e impima recursivamente todas las clases en la
jerarquía del dicho objeto
7. Modifique el ejercicio 6 para que use Class.getDeclaredFields( ) para mostrar además
información acerca de los campos en una clase.
8. En ToyTest.java , comente el constructor por defecto de Toy y explique qué ocurre.
9. Incorpore un nuevo tipo de interfaz en ToyTest.java y verifique es es detectada y
mostrada en forma correcta
10. Cree un nuevo tipo de contenedor que use un ArrayList privado para contener los
objetos. Capture el tipo del primer objeto que colocó y luego permítale al usuario insertar
objetos sólo de este tipo.
11. Escriba un programa para detemrinar si un arreglo de char es un tipo primitivo o un
objeto verdadero.
12. Implemente clearSpitValve( ) como se describió en el resumen.
13. Implemente el método rotate(Shape) descripto en este capítulo, de manera tal que
controle para ver si está rotando un Circle (y, de ser así, no realice la operación).
14. Modifique el ejercicio 6 para que use reflection en lugar de RTTI
15. Modifique el ejercicio 7 para que use reflection en lugar de RTTI
16. En ToyTest.java , use reflection para crear un objeto Toy usando un constructor que no
sea por defecto.
17. Busque la interfaz de java.lang.Class en la documentación HTML de Java de
java.sun.com . Escriba un programa que tome el nombre de una clase como un
argumento de la línea de comando, luego use los métodos de Class para volcar toda la
información disponible para esa clase. Pruebe su programa con una clase de la librería
estándar y con una clase creada por usted.