Cat
clase:
package learn.codegym;
public class Cat {
private String name;
private int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public void sayMeow() {
System.out.println("Meow!");
}
public void jump() {
System.out.println("Jump!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Sabes todo al respecto, y puedes ver los campos y métodos que tiene. Suponga que de repente necesita introducir otras clases de animales en el programa. Probablemente podría crear una estructura de herencia de clase con una Animal
clase principal por conveniencia. Anteriormente, incluso creamos una clase que representaba una clínica veterinaria, a la que podíamos pasar un Animal
objeto (instancia de una clase principal), y el programa trataba al animal de manera adecuada en función de si era un perro o un gato. Aunque estas no son las tareas más simples, el programa puede aprender toda la información necesaria sobre las clases en tiempo de compilación. En consecuencia, cuando pasa un Cat
objeto a los métodos de la clase clínica veterinaria en elmain()
método, el programa ya sabe que es un gato, no un perro. Ahora imaginemos que nos enfrentamos a una tarea diferente. Nuestro objetivo es escribir un analizador de código. Necesitamos crear una CodeAnalyzer
clase con un solo método: void analyzeObject(Object o)
. Este método debería:
- determine la clase del objeto que se le pasa y muestre el nombre de la clase en la consola;
- determine los nombres de todos los campos de la clase aprobada, incluidos los privados, y muéstrelos en la consola;
- determine los nombres de todos los métodos de la clase pasada, incluidos los privados, y muéstrelos en la consola.
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
// Print the name of the class of object o
// Print the names of all variables of this class
// Print the names of all methods of this class
}
}
Ahora podemos ver claramente en qué se diferencia esta tarea de otras tareas que haya resuelto anteriormente. Con nuestro objetivo actual, la dificultad radica en que ni nosotros ni el programa sabemos qué se pasará exactamente alanalyzeClass()
método. Si escribe un programa de este tipo, otros programadores comenzarán a usarlo y podrían pasar cualquier cosa a este método: cualquier clase estándar de Java o cualquier otra clase que escriban. La clase pasada puede tener cualquier número de variables y métodos. En otras palabras, nosotros (y nuestro programa) no tenemos idea de con qué clases trabajaremos. Pero aún así, tenemos que completar esta tarea. Y aquí es donde la API estándar de Java Reflection viene en nuestra ayuda. La API de Reflection es una poderosa herramienta del lenguaje. La documentación oficial de Oracle recomienda que este mecanismo solo debe ser utilizado por programadores experimentados que saben lo que están haciendo. Pronto comprenderá por qué estamos dando este tipo de advertencia por adelantado :) Aquí hay una lista de cosas que puede hacer con la API de Reflection:
- Identificar/determinar la clase de un objeto.
- Obtenga información sobre modificadores de clase, campos, métodos, constantes, constructores y superclases.
- Averigüe qué métodos pertenecen a una interfaz implementada.
- Cree una instancia de una clase cuyo nombre de clase no se conozca hasta que se ejecute el programa.
- Obtenga y establezca el valor de un campo de instancia por nombre.
- Llame a un método de instancia por su nombre.
Cómo identificar/determinar la clase de un objeto
Empecemos con lo básico. El punto de entrada al motor de reflexión de Java es laClass
clase. Sí, parece muy divertido, pero eso es lo que es la reflexión :) Usando la Class
clase, primero determinamos la clase de cualquier objeto pasado a nuestro método. Intentemos hacer esto:
import learn.codegym.Cat;
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
Class clazz = o.getClass();
System.out.println(clazz);
}
public static void main(String[] args) {
analyzeClass(new Cat("Fluffy", 6));
}
}
Salida de la consola:
class learn.codegym.Cat
Presta atención a dos cosas. Primero, colocamos deliberadamente la Cat
clase en un learn.codegym
paquete separado. Ahora puede ver que el getClass()
método devuelve el nombre completo de la clase. En segundo lugar, llamamos a nuestra variable clazz
. Eso se ve un poco extraño. Tendría sentido llamarlo "clase", pero "clase" es una palabra reservada en Java. El compilador no permitirá que las variables se llamen así. Teníamos que sortear eso de alguna manera :) ¡No está mal para empezar! ¿Qué más teníamos en esa lista de capacidades?
Cómo obtener información sobre modificadores de clase, campos, métodos, constantes, constructores y superclases.
¡Ahora las cosas se están poniendo más interesantes! En la clase actual, no tenemos constantes ni una clase principal. Vamos a agregarlos para crear una imagen completa. Cree laAnimal
clase principal más simple:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
Y haremos que nuestra Cat
clase herede Animal
y agreguemos una constante:
package learn.codegym;
public class Cat extends Animal {
private static final String ANIMAL_FAMILY = "Feline family";
private String name;
private int age;
// ...the rest of the class
}
¡Ahora tenemos la imagen completa! Veamos de qué es capaz la reflexión :)
import learn.codegym.Cat;
import java.util.Arrays;
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
Class clazz = o.getClass();
System.out.println("Class name: " + clazz);
System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
System.out.println("Parent class: " + clazz.getSuperclass());
System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
}
public static void main(String[] args) {
analyzeClass(new Cat("Fluffy", 6));
}
}
Esto es lo que vemos en la consola:
Class name: class learn.codegym.Cat
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age]
Parent class: class learn.codegym.Animal
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()]
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
¡Mira toda esa información detallada de clase que pudimos obtener! ¡Y no solo información pública, sino también información privada! Nota: private
las variables también se muestran en la lista. Nuestro "análisis" de la clase puede considerarse esencialmente completo: estamos usando el analyzeObject()
método para aprender todo lo que podamos. Pero esto no es todo lo que podemos hacer con la reflexión. No estamos limitados a la simple observación, ¡pasaremos a la acción! :)
Cómo crear una instancia de una clase cuyo nombre de clase no se conoce hasta que se ejecuta el programa.
Comencemos con el constructor predeterminado. NuestraCat
clase aún no tiene uno, así que vamos a agregarlo:
public Cat() {
}
Aquí está el código para crear un Cat
objeto usando la reflexión ( createCat()
método):
import learn.codegym.Cat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String className = reader.readLine();
Class clazz = Class.forName(className);
Cat cat = (Cat) clazz.newInstance();
return cat;
}
public static Object createObject() throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String className = reader.readLine();
Class clazz = Class.forName(className);
Object result = clazz.newInstance();
return result;
}
public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
System.out.println(createCat());
}
}
Entrada de la consola:
learn.codegym.Cat
Salida de la consola:
Cat{name='null', age=0}
Esto no es un error: los valores de name
y age
se muestran en la consola porque escribimos código para mostrarlos en el toString()
método de la Cat
clase. Aquí leemos el nombre de una clase cuyo objeto crearemos desde la consola. El programa reconoce el nombre de la clase cuyo objeto se va a crear. En aras de la brevedad, omitimos el código de manejo de excepciones adecuado, que ocuparía más espacio que el ejemplo en sí. En un programa real, por supuesto, debe manejar situaciones que involucren nombres ingresados incorrectamente, etc. El constructor predeterminado es bastante simple, así que como puede ver, es fácil de usar para crear una instancia de la clase :) Usando el newInstance()
método , creamos un nuevo objeto de esta clase. Otra cosa es si elCat
constructor toma argumentos como entrada. Eliminemos el constructor predeterminado de la clase e intentemos ejecutar nuestro código nuevamente.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
¡Algo salió mal! Obtuvimos un error porque llamamos a un método para crear un objeto usando el constructor predeterminado. Pero no tenemos tal constructor ahora. Entonces, cuando newInstance()
se ejecuta el método, el mecanismo de reflexión usa nuestro antiguo constructor con dos parámetros:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
Pero no hicimos nada con los parámetros, ¡como si nos hubiéramos olvidado por completo de ellos! Usar la reflexión para pasar argumentos al constructor requiere un poco de "creatividad":
import learn.codegym.Cat;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static Cat createCat() {
Class clazz = null;
Cat cat = null;
try {
clazz = Class.forName("learn.codegym.Cat");
Class[] catClassParams = {String.class, int.class};
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return cat;
}
public static void main(String[] args) {
System.out.println(createCat());
}
}
Salida de la consola:
Cat{name='Fluffy', age=6}
Echemos un vistazo más de cerca a lo que está sucediendo en nuestro programa. Creamos una matriz de Class
objetos.
Class[] catClassParams = {String.class, int.class};
Corresponden a los parámetros de nuestro constructor (que solo tiene String
y int
parámetros). Los pasamos al clazz.getConstructor()
método y obtenemos acceso al constructor deseado. Después de eso, todo lo que tenemos que hacer es llamar al newInstance()
método con los argumentos necesarios y no olvide convertir explícitamente el objeto al tipo deseado: Cat
.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
¡Ahora nuestro objeto se ha creado con éxito! Salida de la consola:
Cat{name='Fluffy', age=6}
Avanzando a lo largo :)
Cómo obtener y establecer el valor de un campo de instancia por nombre.
Imagina que estás usando una clase escrita por otro programador. Además, no tiene la capacidad de editarlo. Por ejemplo, una biblioteca de clases prefabricada empaquetada en un JAR. Puedes leer el código de las clases, pero no puedes cambiarlo. Supongamos que el programador que creó una de las clases en esta biblioteca (que sea nuestraCat
clase anterior), al no poder dormir lo suficiente la noche anterior a la finalización del diseño, eliminó el getter y el setter del age
campo. Ahora esta clase ha llegado a ti. Cumple con todas tus necesidades, ya que solo necesitas Cat
objetos en tu programa. ¡Pero los necesitas para tener un age
campo! Esto es un problema: no podemos llegar al campo, porque tiene laprivate
modificador, y el getter y setter fueron eliminados por el desarrollador privado de sueño que creó la clase:/ Bueno, ¡la reflexión puede ayudarnos en esta situación! Tenemos acceso al código de la Cat
clase, por lo que al menos podemos averiguar qué campos tiene y cómo se llaman. Armados con esta información, podemos resolver nuestro problema:
import learn.codegym.Cat;
import java.lang.reflect.Field;
public class Main {
public static Cat createCat() {
Class clazz = null;
Cat cat = null;
try {
clazz = Class.forName("learn.codegym.Cat");
cat = (Cat) clazz.newInstance();
// We got lucky with the name field, since it has a setter
cat.setName("Fluffy");
Field age = clazz.getDeclaredField("age");
age.setAccessible(true);
age.set(cat, 6);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return cat;
}
public static void main(String[] args) {
System.out.println(createCat());
}
}
Como se indica en los comentarios, todo lo relacionado con el name
campo es sencillo, ya que los desarrolladores de la clase proporcionaron un setter. Ya sabes cómo crear objetos a partir de constructores predeterminados: tenemos el newInstance()
para esto. Pero tendremos que hacer algunos retoques con el segundo campo. Vamos a averiguar qué está pasando aquí :)
Field age = clazz.getDeclaredField("age");
Aquí, usando nuestro Class clazz
objeto, accedemos al age
campo a través del getDeclaredField()
método. Nos permite obtener el campo de edad como Field age
objeto. Pero esto no es suficiente, porque no podemos simplemente asignar valores a private
los campos. Para hacer esto, necesitamos hacer que el campo sea accesible usando el setAccessible()
método:
age.setAccessible(true);
Una vez que hacemos esto a un campo, podemos asignar un valor:
age.set(cat, 6);
Como puede ver, nuestro Field age
objeto tiene una especie de setter de adentro hacia afuera al que le pasamos un valor int y el objeto cuyo campo se va a asignar. Ejecutamos nuestro main()
método y vemos:
Cat{name='Fluffy', age=6}
¡Excelente! ¡Lo hicimos! :) Veamos qué más podemos hacer...
Cómo llamar a un método de instancia por su nombre.
Cambiemos ligeramente la situación del ejemplo anterior. Digamos que elCat
desarrollador de la clase no se equivocó con los getters y setters. Todo está bien en ese sentido. Ahora el problema es diferente: hay un método que definitivamente necesitamos, pero el desarrollador lo hizo privado:
private void sayMeow() {
System.out.println("Meow!");
}
Esto significa que si creamos Cat
objetos en nuestro programa, no podremos llamar al sayMeow()
método sobre ellos. ¿Tendremos gatos que no maúllan? Qué raro :/ ¿Cómo arreglaríamos esto? ¡Una vez más, la API de Reflection nos ayuda! Sabemos el nombre del método que necesitamos. Todo lo demás es un tecnicismo:
import learn.codegym.Cat;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void invokeSayMeowMethod() {
Class clazz = null;
Cat cat = null;
try {
cat = new Cat("Fluffy", 6);
clazz = Class.forName(Cat.class.getName());
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
sayMeow.setAccessible(true);
sayMeow.invoke(cat);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
invokeSayMeowMethod();
}
}
Aquí hacemos casi lo mismo que hicimos al acceder a un campo privado. Primero, obtenemos el método que necesitamos. Está encapsulado en un Method
objeto:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
El getDeclaredMethod()
método nos permite acceder a métodos privados. A continuación, hacemos que el método sea invocable:
sayMeow.setAccessible(true);
Y finalmente, llamamos al método sobre el objeto deseado:
sayMeow.invoke(cat);
Aquí, nuestra llamada de método parece una "devolución de llamada": estamos acostumbrados a usar un punto para apuntar un objeto al método deseado ( cat.sayMeow()
), pero cuando trabajamos con la reflexión, le pasamos al método el objeto al que queremos llamar ese método ¿Qué hay en nuestra consola?
Meow!
¡Todo funcionó! :) Ahora puedes ver las vastas posibilidades que nos brinda el mecanismo de reflexión de Java. En situaciones difíciles e inesperadas (como nuestros ejemplos con una clase de una biblioteca cerrada), realmente nos puede ayudar mucho. Pero, como con cualquier gran poder, conlleva una gran responsabilidad. Las desventajas de la reflexión se describen en una sección especial en el sitio web de Oracle. Hay tres desventajas principales:
-
El rendimiento es peor. Los métodos llamados mediante reflexión tienen peor rendimiento que los métodos llamados de forma normal.
-
Hay restricciones de seguridad. El mecanismo de reflexión nos permite cambiar el comportamiento de un programa en tiempo de ejecución. Pero en su lugar de trabajo, al trabajar en un proyecto real, puede enfrentar limitaciones que no lo permitan.
-
Riesgo de exposición de información interna. Es importante entender que la reflexión es una violación directa del principio de encapsulación: nos permite acceder a campos privados, métodos, etc. No creo que sea necesario mencionar que se debe recurrir a una violación directa y flagrante de los principios de OOP. a sólo en los casos más extremos, cuando no hay otras formas de resolver un problema por razones fuera de su control.
GO TO FULL VERSION