CodeGym /Blog Java /Random-ES /API de reflexión: reflexión. El lado oscuro de Java
Autor
Pavlo Plynko
Java Developer at CodeGym

API de reflexión: reflexión. El lado oscuro de Java

Publicado en el grupo Random-ES
Saludos, joven padawan. En este artículo, les hablaré sobre la Fuerza, un poder que los programadores de Java solo usan en situaciones aparentemente imposibles. El lado oscuro de Java es la API Reflection. En Java, la reflexión se implementa mediante la API de Java Reflection.

¿Qué es la reflexión de Java?

Hay una definición breve, precisa y popular en Internet. La reflexión ( del latín tardío reflexio - volver atrás ) es un mecanismo para explorar datos sobre un programa mientras se está ejecutando. Reflection le permite explorar información sobre campos, métodos y constructores de clases. Reflection le permite trabajar con tipos que no estaban presentes en tiempo de compilación, pero que estuvieron disponibles durante el tiempo de ejecución. La reflexión y un modelo lógicamente consistente para emitir información de error hacen posible crear un código dinámico correcto. En otras palabras, comprender cómo funciona la reflexión en Java le abrirá una serie de oportunidades increíbles. Literalmente puedes hacer malabarismos con las clases y sus componentes. Aquí hay una lista básica de lo que permite la reflexión:
  • Aprender/determinar la clase de un objeto;
  • Obtenga información sobre los modificadores, campos, métodos, constantes, constructores y superclases de una clase;
  • Averigüe qué métodos pertenecen a la(s) interfaz(es) implementada(s);
  • Cree una instancia de una clase cuyo nombre de clase se desconozca hasta el momento de la ejecución;
  • Obtener y establecer valores de los campos de un objeto por nombre;
  • Llame al método de un objeto por su nombre.
Reflection se utiliza en casi todas las tecnologías modernas de Java. Es difícil imaginar que Java, como plataforma, podría haber logrado una adopción tan generalizada sin reflexionar. Lo más probable es que no lo hubiera hecho. Ahora que ya está familiarizado con la reflexión como concepto teórico, ¡pasemos a su aplicación práctica! No aprenderemos todos los métodos de la API de Reflection, solo los que realmente encontrará en la práctica. Dado que la reflexión implica trabajar con clases, comenzaremos con una clase simple llamada MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Como puede ver, esta es una clase muy básica. El constructor con parámetros se comenta deliberadamente. Volveremos a eso más tarde. Si observó detenidamente el contenido de la clase, probablemente notó la ausencia de un getter para el campo de nombre . El campo de nombre en sí está marcado con el modificador de acceso privado : no podemos acceder a él fuera de la clase en sí, lo que significa que no podemos recuperar su valor. " Entonces, ¿cuál es el problema ?" tu dices. "Agregue un getter o cambie el modificador de acceso". Y tendrías razón, a menos queMyClassestaba en una biblioteca AAR compilada o en otro módulo privado sin posibilidad de realizar cambios. En la práctica, esto sucede todo el tiempo. Y algún programador descuidado simplemente se olvidó de escribir un captador . ¡Este es el momento de recordar la reflexión! Intentemos llegar al campo de nombre privado de la MyClassclase:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Analicemos lo que acaba de pasar. En Java, hay una clase maravillosa llamada Class. Representa clases e interfaces en una aplicación Java ejecutable. No cubriremos la relación entre Classy ClassLoader, ya que ese no es el tema de este artículo. A continuación, para recuperar los campos de esta clase, debe llamar al getFields()método. Este método devolverá todos los campos accesibles de esta clase. Esto no funciona para nosotros, porque nuestro campo es privado , entonces usamos el getDeclaredFields()método. Este método también devuelve una matriz de campos de clase, pero ahora incluye campos privados y protegidos . En este caso, conocemos el nombre del campo que nos interesa, por lo que podemos usar el getDeclaredField(String)método, dondeStringes el nombre del campo deseado. Nota: getFields()¡y getDeclaredFields()no devuelva los campos de una clase principal! Excelente. Tenemos un Fieldobjeto que hace referencia a nuestro nombre . Dado que el campo no era público , debemos otorgar acceso para trabajar con él. El setAccessible(true)método nos permite continuar. ¡ Ahora el campo de nombre está bajo nuestro control completo! Puede recuperar su valor llamando al método Fielddel objeto , donde es una instancia de nuestra clase. Convertimos el tipo y asignamos el valor a nuestra variable de nombre . Si no podemos encontrar un setter para establecer un nuevo valor en el campo de nombre, puede usar el método set :get(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
¡Felicidades! ¡Acabas de dominar los conceptos básicos de la reflexión y accediste a un campo privado ! Preste atención al try/catchbloque y a los tipos de excepciones que se manejan. El IDE le dirá que se requiere su presencia por sí solo, pero puede saber claramente por sus nombres por qué están aquí. ¡Hacia adelante! Como habrás notado, nuestra MyClassclase ya tiene un método para mostrar información sobre los datos de la clase:

private void printData(){
       System.out.println(number + name);
   }
Pero este programador también dejó aquí sus huellas dactilares. El método tiene un modificador de acceso privado y tenemos que escribir nuestro propio código para mostrar datos cada vez. Que desastre. ¿Adónde fue nuestro reflejo? Escribe la siguiente función:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
El procedimiento aquí es casi el mismo que el utilizado para recuperar un campo. Accedemos al método deseado por su nombre y damos acceso al mismo. Y sobre el Methodobjeto llamamos invoke(Object, Args)método, donde Objecttambién es una instancia de la MyClassclase. Argsson los argumentos del método, aunque el nuestro no tiene ninguno. Ahora usamos la printDatafunción para mostrar información:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
¡Viva! Ahora tenemos acceso al método privado de la clase. Pero, ¿qué pasa si el método tiene argumentos y por qué el constructor está comentado? Todo a su debido tiempo. ¡De la definición al principio queda claro que la reflexión le permite crear instancias de una clase en tiempo de ejecución (mientras el programa se está ejecutando)! Podemos crear un objeto usando el nombre completo de la clase. El nombre completo de la clase es el nombre de la clase, incluida la ruta de su paquete .
API de reflexión: reflexión.  El lado oscuro de Java - 2
En mi jerarquía de paquetes , el nombre completo de MyClass sería "reflection.MyClass". También hay una forma sencilla de aprender el nombre de una clase (devuelve el nombre de la clase como una cadena):

MyClass.class.getName()
Usemos la reflexión de Java para crear una instancia de la clase:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
Cuando se inicia una aplicación Java, no todas las clases se cargan en la JVM. Si su código no hace referencia a la MyClassclase, entonces ClassLoader, que es responsable de cargar las clases en la JVM, nunca cargará la clase. Eso significa que debe forzar ClassLoaderla carga y obtener una descripción de clase en forma de Classvariable. Por eso tenemos el forName(String)método, donde Stringestá el nombre de la clase cuya descripción necesitamos. Después de obtener el Сlassobjeto, llamar al método newInstance()devolverá un Objectobjeto creado con esa descripción. Todo lo que queda es suministrar este objeto a nuestroMyClassclase. ¡Fresco! Eso fue difícil, pero comprensible, espero. ¡Ahora podemos crear una instancia de una clase literalmente en una línea! Desafortunadamente, el enfoque descrito solo funcionará con el constructor predeterminado (sin parámetros). ¿Cómo llamas a métodos y constructores con parámetros? Es hora de descomentar nuestro constructor. Como era de esperar, newInstance()no se puede encontrar el constructor predeterminado y ya no funciona. Reescribamos la instanciación de la clase:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
Se debe llamar al getConstructors()método en la definición de clase para obtener constructores de clase y luego getParameterTypes()se debe llamar para obtener los parámetros de un constructor:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Eso nos da todos los constructores y sus parámetros. En mi ejemplo, me refiero a un constructor específico con parámetros específicos previamente conocidos. Y para llamar a este constructor, usamos el newInstancemétodo, al que le pasamos los valores de estos parámetros. Será lo mismo cuando se use invokepara llamar a métodos. Esto plantea la pregunta: ¿cuándo es útil llamar a los constructores a través de la reflexión? Como ya se mencionó al principio, las tecnologías modernas de Java no pueden funcionar sin la API de Java Reflection. Por ejemplo, Inyección de dependencia (DI), que combina anotaciones con reflejo de métodos y constructores para formar el popular Darerbiblioteca para el desarrollo de Android. Después de leer este artículo, puede considerar con confianza que está informado sobre las formas de la API de Java Reflection. No llaman a la reflexión el lado oscuro de Java por nada. Rompe por completo el paradigma OOP. En Java, la encapsulación oculta y restringe el acceso de otros a ciertos componentes del programa. Cuando usamos el modificador privado, pretendemos que solo se acceda a ese campo desde la clase donde existe. Y construimos la arquitectura posterior del programa en base a este principio. En este artículo, hemos visto cómo puede usar la reflexión para forzar su camino en cualquier lugar. El patrón de diseño creacional Singletones un buen ejemplo de esto como solución arquitectónica. La idea básica es que una clase que implemente este patrón solo tendrá una instancia durante la ejecución de todo el programa. Esto se logra agregando el modificador de acceso privado al constructor predeterminado. Y sería muy malo si un programador usara la reflexión para crear más instancias de tales clases. Por cierto, hace poco escuché a un compañero de trabajo hacer una pregunta muy interesante: ¿se puede heredar una clase que implementa el patrón Singleton? ¿Será que, en este caso, incluso la reflexión sería impotente? ¡Deje sus comentarios sobre el artículo y su respuesta en los comentarios a continuación, y haga sus propias preguntas allí!

Más lectura:

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION