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.