JavaRush /Blog Java /Random-ES /Reflexión en Java: ejemplos de uso

Reflexión en Java: ejemplos de uso

Publicado en el grupo Random-ES
Es posible que te hayas encontrado con el concepto de “reflexión” en la vida cotidiana. Generalmente esta palabra se refiere al proceso de estudiarse a uno mismo. En programación, tiene un significado similar: es un mecanismo para examinar datos sobre un programa, así como para cambiar la estructura y el comportamiento del programa durante su ejecución. Lo importante aquí es que se haga en tiempo de ejecución, no en tiempo de compilación. Pero ¿por qué examinar el código en tiempo de ejecución? Ya lo ves :/ Ejemplos de uso de Reflexión - 1La idea de reflexión puede no quedar clara de inmediato por una razón: hasta ese momento, siempre sabías con qué clases estabas trabajando. Bueno, por ejemplo, podrías escribir una clase Cat:
package learn.javarush;

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, ves qué campos y métodos tiene. Seguramente puedes crear un sistema de herencia con una clase común por conveniencia Animal, si de repente el programa necesita otras clases de animales. Anteriormente, incluso creamos una clase de clínica veterinaria en la que se podía pasar un objeto padre Animaly el programa trataba al animal dependiendo de si era un perro o un gato. Aunque estas tareas no son muy sencillas, el programa aprende toda la información que necesita sobre las clases en tiempo de compilación. Por lo tanto, cuando main()pasas un objeto en un método Cata los métodos de la clase clínica veterinaria, el programa ya sabe que se trata de un gato, no de un perro. Ahora imaginemos que nos enfrentamos a otra tarea. Nuestro objetivo es escribir un analizador de código. Necesitamos crear una clase CodeAnalyzercon un solo método: void analyzeClass(Object o). Este método debería:
  • determinar qué clase se le pasó el objeto y mostrar el nombre de la clase en la consola;
  • determinar los nombres de todos los campos de esta clase, incluidos los privados, y mostrarlos en la consola;
  • determine los nombres de todos los métodos de esta clase, incluidos los privados, y muéstrelos en la consola.
Se verá algo como esto:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит un objeto o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
Ahora es visible la diferencia entre este problema y el resto de problemas que resolviste antes. En este caso, la dificultad radica en el hecho de que ni usted ni el programa saben exactamente qué se pasará al método analyzeClass(). Usted escribe un programa, otros programadores comenzarán a usarlo y podrán pasar cualquier cosa a este método: cualquier clase Java estándar o cualquier clase que hayan escrito. Esta clase puede tener cualquier número de variables y métodos. En otras palabras, en este caso nosotros (y nuestro programa) no tenemos idea de con qué clases trabajaremos. Y, sin embargo, debemos resolver este problema. Y aquí viene en nuestra ayuda la biblioteca estándar de Java: la API Java Reflection. La API de Reflection es una poderosa característica del lenguaje. La documentación oficial de Oracle indica que se recomienda que este mecanismo lo utilicen únicamente programadores experimentados que entiendan muy bien lo que están haciendo. Pronto comprenderá por qué de repente recibimos tales advertencias por adelantado :) Aquí hay una lista de lo que se puede hacer usando la API de Reflection:
  1. Averiguar/determinar la clase de un objeto.
  2. Obtenga información sobre modificadores de clase, campos, métodos, constantes, constructores y superclases.
  3. Descubra qué métodos pertenecen a la(s) interfaz(es) implementada(s).
  4. Cree una instancia de una clase cuando el nombre de la clase sea desconocido hasta que se ejecute el programa.
  5. Obtenga y establezca el valor de un campo de objeto por nombre.
  6. Llama al método de un objeto por su nombre.
Impresionante lista, ¿eh? :) Prestar atención:¡El mecanismo de reflexión es capaz de hacer todo esto “sobre la marcha” sin importar qué objeto de clase pasemos a nuestro analizador de código! Veamos las capacidades de la API Reflection con ejemplos.

Cómo averiguar/determinar la clase de un objeto

Empecemos con lo básico. El punto de entrada al mecanismo de reflexión de Java es el Class. Sí, parece muy divertido, pero para eso está la reflexión :) Usando una clase Class, primero que nada determinamos la clase de cualquier objeto pasado a nuestro método. Intentemos esto:
import learn.javarush.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("Barsik", 6));
   }
}
Salida de consola:

class learn.javarush.Cat
Presta atención a dos cosas. En primer lugar, colocamos deliberadamente la clase Caten un paquete separado y learn.javarush;ahora puede ver que getClass()devuelve el nombre completo de la clase. En segundo lugar, nombramos nuestra variable clazz. Parece un poco extraño. Por supuesto, debería llamarse "clase", pero "clase" es una palabra reservada en el lenguaje Java y el compilador no permitirá que las variables se llamen de esa manera. Tuve que salir de esto :) Bueno, ¡no es un mal comienzo! ¿Qué más teníamos en la lista de posibilidades?

Cómo obtener información sobre modificadores de clase, campos, métodos, constantes, constructores y superclases

¡Esto ya es más interesante! En la clase actual no tenemos constantes ni clase principal. Agreguémoslos para que estén completos. Creemos la clase principal más simple Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
Y agreguemos Catherencia desde Animaly una constante a nuestra clase:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
¡Ahora tenemos un juego completo! Probemos las posibilidades de la reflexión :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Nombre класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Esto es lo que obtenemos en la consola:
Nombre класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
¡Recibimos mucha información detallada sobre la clase! Y no sólo de lo público, sino también de lo privado. Prestar atención: private-Las variables también se muestran en la lista. En realidad, el “análisis” de la clase se puede considerar completo en este punto: ahora, usando el método, analyzeClass()aprenderemos todo lo que sea posible. Pero estas no son todas las posibilidades que tenemos al trabajar con la reflexión. ¡No nos limitemos a la simple observación y pasemos a la acción activa! :)

Cómo crear una instancia de una clase si se desconoce el nombre de la clase antes de ejecutar el programa

Comencemos con el constructor predeterminado. Aún no está en nuestra clase Cat, así que agreguémoslo:
public Cat() {

}
Así es como se vería el código para crear un objeto Catusando la reflexión (método createCat()):
import learn.javarush.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());
   }
}
Ingrese a la consola:

learn.javarush.Cat
Salida de consola:

Cat{name='null', age=0}
Esto no es un error: los valores namey agese muestran en la consola porque programamos su salida en el método de la toString()clase Cat. Aquí leemos el nombre de la clase cuyo objeto crearemos desde la consola. El programa en ejecución aprende el nombre de la clase cuyo objeto creará. Ejemplos de uso de Reflexión - 3En aras de la brevedad, hemos omitido el código para un manejo adecuado de excepciones para que no ocupe más espacio que el ejemplo mismo. En un programa real, por supuesto, definitivamente vale la pena manejar situaciones en las que se ingresan nombres incorrectos, etc. El constructor predeterminado es algo bastante simple, por lo que crear una instancia de una clase usándolo, como puede ver, no es difícil :) Y usando el método, newInstance()creamos un nuevo objeto de esta clase. Otra cuestión es si el constructor de la clase Cattoma parámetros como entrada. Eliminemos el constructor predeterminado de la clase e intentemos ejecutar nuestro código nuevamente.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
¡Algo salió mal! Recibimos un error porque llamamos a un método para crear un objeto a través del constructor predeterminado. Pero ahora no tenemos ese diseñador. Esto significa que cuando el método funcione, newInstance()el mecanismo de reflexión utilizará 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 de ellos por completo! Para pasarlos al constructor usando la reflexión, tendrás que modificarlo un poco:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 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 consola:

Cat{name='Barsik', age=6}
Echemos un vistazo más de cerca a lo que está sucediendo en nuestro programa. Hemos creado una serie de objetos Class.
Class[] catClassParams = {String.class, int.class};
Corresponden a los parámetros de nuestro constructor (solo tenemos los parámetros Stringy int). Los pasamos al método clazz.getConstructor()y obtenemos acceso al constructor requerido. Después de esto, todo lo que queda es llamar al método newInstance()con los parámetros necesarios y no olvidar convertir explícitamente el objeto a la clase que necesitamos - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
Como resultado, ¡nuestro objeto se creará con éxito! Salida de consola:

Cat{name='Barsik', age=6}
Vamonos :)

Cómo obtener y establecer el valor de un campo de objeto por nombre

Imagine que está utilizando una clase escrita por otro programador. Sin embargo, no tiene la oportunidad de editarlo. Por ejemplo, una biblioteca de clases lista para usar empaquetada en un JAR. Puedes leer el código de clase, pero no puedes cambiarlo. El programador que creó la clase en esta biblioteca (que sea nuestra clase anterior Cat) no durmió lo suficiente antes del diseño final y eliminó los captadores y definidores del campo age. Ahora esta clase ha llegado a ti. Satisface plenamente tus necesidades, porque sólo necesitas objetos en el programa Cat. ¡Pero los necesitas con ese mismo campo age! Esto es un problema: no podemos acceder al campo porque tiene un modificador privatey el posible desarrollador de esta clase eliminó los captadores y definidores :/ Bueno, ¡la reflexión también puede ayudarnos en esta situación! CatTenemos acceso al código de la clase : al menos podemos saber qué campos tiene y cómo se llaman. Armados con esta información, solucionamos nuestro problema:
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           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 el comentario, nametodo es simple con el campo: los desarrolladores de la clase le proporcionaron un configurador. También ya sabes cómo crear objetos a partir de constructores predeterminados: existe un método para esto newInstance(). Pero tendrás que jugar con el segundo campo. Averigüemos qué está pasando aquí :)
Field age = clazz.getDeclaredField("age");
Aquí nosotros, usando nuestro objeto Class clazz, accedemos al campo ageusando el archivo getDeclaredField(). Nos da la posibilidad de obtener el campo de edad como un objeto Field age. Pero esto aún no es suficiente, porque privatea los campos no se les pueden asignar valores simplemente. Para hacer esto, debe hacer que el campo esté “disponible” usando el método setAccessible():
age.setAccessible(true);
A aquellos campos para los que se hace esto se les pueden asignar valores:
age.set(cat, 6);
Como puede ver, tenemos una especie de setter al revés: asignamos al campo Field agesu valor y también le pasamos el objeto al que se le debe asignar este campo. Ejecutemos nuestro método main()y veamos:

Cat{name='Barsik', age=6}
¡Genial, lo hicimos todo! :) Veamos qué otras posibilidades tenemos...

Cómo llamar al método de un objeto por su nombre

Cambiemos ligeramente la situación del ejemplo anterior. Digamos que el desarrollador de la clase Catcometió un error con los campos: ambos están disponibles, hay captadores y definidores para ellos, todo está bien. El problema es diferente: hizo privado un método que definitivamente necesitamos:
private void sayMeow() {

   System.out.println("Meow!");
}
Como resultado, crearemos objetos Caten nuestro programa, pero no podremos llamar a su método sayMeow(). ¿Tendremos gatos que no maullen? Bastante extraño :/ ¿Cómo puedo solucionar esto? ¡Una vez más, la API Reflection viene al rescate! Sabemos el nombre del método requerido. El resto es cuestión de técnica:
import learn.javarush.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("Barsik", 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í actuamos de la misma manera que en la situación del acceso a un campo privado. Primero obtenemos el método que necesitamos, que está encapsulado en un objeto de clase Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Con ayuda, getDeclaredMethod()puede "contactar" a métodos privados. A continuación hacemos que el método sea invocable:
sayMeow.setAccessible(true);
Y finalmente, llamamos al método en el objeto deseado:
sayMeow.invoke(cat);
Llamar a un método también parece una "llamada a la inversa": estamos acostumbrados a señalar un objeto al método requerido usando un punto ( cat.sayMeow()), y cuando trabajamos con la reflexión, pasamos al método el objeto desde el cual se debe llamar. . ¿Qué tenemos en la consola?

Meow!
¡Todo salió bien! :) Ahora ves las amplias posibilidades que nos ofrece el mecanismo de reflexión en Java. En situaciones difíciles e inesperadas (como en los ejemplos con una clase de una biblioteca cerrada), realmente puede ayudarnos mucho. Sin embargo, como toda gran potencia, también implica una gran responsabilidad. Las desventajas de la reflexión se describen en una sección especial del sitio web de Oracle. Hay tres desventajas principales:
  1. La productividad disminuye. Los métodos que se llaman mediante reflexión tienen un rendimiento menor que los métodos que se llaman normalmente.

  2. Hay restricciones de seguridad. El mecanismo de reflexión le permite cambiar el comportamiento del programa durante el tiempo de ejecución. Pero en su entorno de trabajo en un proyecto real puede haber restricciones que no le permitan hacer esto.

  3. Riesgo de divulgación de información privilegiada. Es importante entender que el uso de la reflexión viola directamente el principio de encapsulación: nos permite acceder a campos, métodos, etc. privados. Creo que no es necesario explicar que se debe recurrir a la violación directa y grave de los principios de la programación orientada a objetos solo en los casos más extremos, cuando no hay otras formas de resolver el problema por razones que escapan a su control.

Utilice el mecanismo de reflexión con prudencia y sólo en situaciones en las que no se pueda evitar, y no se olvide de sus defectos. ¡Esto concluye nuestra conferencia! Resultó bastante grande, pero hoy aprendiste muchas cosas nuevas :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION