JavaRush /Blog Java /Random-ES /API de reflexión. Reflexión. El lado oscuro de Java
Oleksandr Klymenko
Nivel 13
Харків

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

Publicado en el grupo Random-ES
Saludos, joven padawan. En este artículo, le contaré sobre la Fuerza, cuyo poder los programadores de Java utilizan solo en una situación aparentemente desesperada. Entonces, el lado oscuro de Java es...Reflection API
API de reflexión.  Reflexión.  El lado oscuro de Java - 1
La reflexión en Java se realiza utilizando la API Java Reflection. ¿Qué es esta reflexión? Existe una definición breve y precisa que también es popular en Internet. La reflexión (del latín tardío reflexio - volver) es un mecanismo para estudiar datos sobre un programa durante su ejecución. Reflection le permite examinar información sobre campos, métodos y constructores de clases. El mecanismo de reflexión en sí le permite procesar tipos que faltan durante la compilación, pero que aparecen durante la ejecución del programa. La reflexión y la presencia de un modelo lógicamente coherente para informar errores hace posible crear un código dinámico correcto. En otras palabras, comprender cómo funciona la reflexión en Java le abre una serie de oportunidades increíbles. Literalmente puedes hacer malabarismos con las clases y sus componentes.
API de reflexión.  Reflexión.  El lado oscuro de Java - 2
Aquí hay una lista básica de lo que permite la reflexión:
  • Averiguar/determinar la clase de un objeto;
  • Obtenga información sobre modificadores de clase, campos, métodos, constantes, constructores y superclases;
  • Descubra qué métodos pertenecen a la(s) interfaz(es) implementada(s);
  • Cree una instancia de una clase y el nombre de la clase se desconoce hasta que se ejecuta el programa;
  • Obtener y establecer el valor de un campo de objeto por nombre;
  • Llama al método de un objeto por su nombre.
La reflexión se utiliza en casi todas las tecnologías Java modernas. Es difícil imaginar si Java como plataforma podría haber logrado una adopción tan enorme sin reflexionar. Lo más probable es que no pudiera. Te has familiarizado con la idea teórica general de la reflexión, ¡ahora vayamos a su aplicación práctica! No estudiaremos todos los métodos de la API de Reflection, solo lo que realmente se encuentra en la práctica. Dado que el mecanismo de reflexión implica trabajar con clases, tendremos una clase simple 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 podemos ver, esta es la clase más común. El constructor con parámetros está comentado por una razón, volveremos a esto más adelante. Si observaste detenidamente el contenido de la clase, probablemente notaste la ausencia de getter'a para name. El campo en sí nameestá marcado con un modificador de acceso private; no podremos acceder a él fuera de la clase misma; =>no podemos obtener su valor. "¿Entonces, cuál es el problema? - tu dices. "Agregar gettero cambiar el modificador de acceso". Y tendrá razón, pero ¿qué pasa si MyClassestá en una biblioteca aar compilada o en otro módulo cerrado sin acceso de edición? Y en la práctica esto sucede con mucha frecuencia. Y algún programador distraído simplemente se olvidó de escribir getter. ¡Es hora de recordar la reflexión! Intentemos llegar al privatecampo namede clase MyClass:
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
}
Averigüemos qué pasó aquí ahora. Hay una clase maravillosa en Java Class. Representa clases e interfaces en una aplicación Java ejecutable. No tocaremos la conexión entre Classy ClassLoader. este no es el tema del artículo. A continuación, para obtener los campos de esta clase, debe llamar al método getFields(), este método nos devolverá todos los campos disponibles de la clase. Esto no es adecuado para nosotros, ya que nuestro campo es private, entonces usamos el método getDeclaredFields(), que también devuelve una matriz de campos de clase, pero ahora ambos privatey protected. En nuestra situación sabemos el nombre del campo que nos interesa y podemos utilizar el método getDeclaredField(String), donde Stringestá el nombre del campo deseado. Nota: getFields()¡Y getDeclaredFields()no devuelva los campos de la clase padre! Genial, recibimos un objeto Field con un enlace a nuestro name. Porque el campo no era публичным(público), se debía dar acceso para trabajar con él. El método setAccessible(true)nos permite seguir trabajando. ¡ Ahora el campo nameestá completamente bajo nuestro control! Puedes obtener su valor llamando get(Object)al objeto Field, donde Objecthay una instancia de nuestra clase MyClass. Lo lanzamos Stringy lo asignamos a nuestra variable name. En caso de que de repente no tengamos setter'a, podemos usar el método para establecer un nuevo valor para el campo de nombre set:
field.set(myClass, (String) "new value");
¡Felicidades! ¡Acabas de dominar el mecanismo básico de reflexión y pudiste acceder al privatecampo! Preste atención al bloque try/catchy a los tipos de excepciones manejadas. El propio IDE indicará su presencia obligatoria, pero su nombre deja claro por qué están aquí. ¡Adelante! Como habrás notado, el nuestro MyClassya 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ó un legado aquí. El método está bajo el modificador de acceso privatey tuvimos que escribir el código de salida nosotros mismos cada vez. No está en orden, ¿dónde está nuestro reflejo?... Escribamos 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();
   }
}
Aquí el procedimiento es aproximadamente el mismo que cuando se obtiene un campo: obtenemos el método deseado por su nombre y le damos acceso. Y para llamar al objeto Methodque usamos invoke(Оbject, Args), donde Оbjecttambién hay una instancia de la clase MyClass. Args- argumentos del método: el nuestro no tiene ninguno. Ahora usamos la función para mostrar información printData:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 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
}
Hurra, ahora tenemos acceso al método privado de la clase. Pero, ¿qué pasa si el método todavía tiene argumentos y por qué hay un constructor comentado? Todo tiene su tiempo. ¡De la definición al principio está claro que la reflexión le permite crear instancias de una clase en un modo runtime(mientras el programa se está ejecutando)! Podemos crear un objeto de una clase con el nombre completo de esa clase. El nombre de clase completo es el nombre de la clase, dada la ruta a ella en package.
API de reflexión.  Reflexión.  El lado oscuro de Java - 3
En mi jerarquía, packageel nombre completo MyClassserá " reflection.MyClass". También puedes encontrar el nombre de la clase de una manera sencilla (te devolverá el nombre de la clase como una cadena):
MyClass.class.getName()
Creemos una instancia de la clase usando la reflexión:
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
}
En el momento en que se inicia la aplicación Java, no todas las clases se cargan en la JVM. Si su código no hace referencia a la clase MyClass, entonces la persona responsable de cargar las clases en la JVM, es decir ClassLoader, nunca las cargará allí. Por lo tanto, debemos forzarlo ClassLoadera cargar y recibir una descripción de nuestra clase en forma de variable de tipo Class. Para esta tarea existe un método forName(String), donde Stringestá el nombre de la clase cuya descripción requerimos. Habiendo recibido , devolverá Сlassla llamada al método , que se creará de acuerdo con la misma descripción. Queda por traer este objeto a nuestra clase . ¡Fresco! Fue difícil, pero espero que sea comprensible. ¡Ahora podemos crear una instancia de una clase literalmente desde una línea! Desafortunadamente, el método descrito sólo funcionará con el constructor predeterminado (sin parámetros). ¿Cómo llamar a métodos con argumentos y constructores con parámetros? Es hora de descomentar nuestro constructor. Como era de esperar, no encuentra el constructor predeterminado y ya no funciona. Reescribamos la creación de una instancia de clase: newInstance()ObjectMyClassnewInstance()
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
}
Para obtener constructores de clase, llame al método desde la descripción de la clase getConstructors()y para obtener parámetros del constructor, llame a getParameterTypes():
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
De esta manera obtenemos todos los constructores y todos sus parámetros. En mi ejemplo, hay una llamada a un constructor específico con parámetros específicos ya conocidos. Y para llamar a este constructor usamos el método newInstance, en el que especificamos los valores para estos parámetros. Lo mismo sucederá invokecon los métodos de llamada. Surge la pregunta: ¿dónde puede resultar útil la llamada reflexiva a los constructores? Las tecnologías Java modernas, como se mencionó al principio, no pueden prescindir de la API Reflection. Por ejemplo, DI (Inyección de dependencia), donde las anotaciones combinadas con la reflexión de métodos y constructores forman la biblioteca Dagger, popular en el desarrollo de Android. Después de leer este artículo, podrá considerarse con seguridad un conocedor de los mecanismos de la API Reflection. No en vano se llama a la reflexión el lado oscuro de Java. Rompe completamente el paradigma de la programación orientada a objetos. En Java, la encapsulación sirve para ocultar y limitar el acceso de algunos componentes del programa a otros. Al usar el modificador privado queremos decir que el acceso a este campo solo será dentro de la clase donde existe este campo, en base a esto construimos la arquitectura adicional del programa. En este artículo, vimos cómo puedes utilizar la reflexión para llegar a cualquier parte. Un buen ejemplo en forma de solución arquitectónica es el patrón de diseño generativo - Singleton. Su idea principal es que durante todo el funcionamiento del programa, la clase que implemente esta plantilla debe tener una sola copia. Esto se hace estableciendo el modificador de acceso predeterminado en privado para el constructor. Y sería muy malo si algún programador con su propio reflejo creara tales clases. Por cierto, hay una pregunta muy interesante que escuché recientemente de mi empleado: ¿puede una clase que implementa una plantilla tener Singletonherederos? ¿Es posible que en este caso incluso la reflexión sea impotente? Escriba sus comentarios sobre el artículo y la respuesta en los comentarios, ¡y también haga sus preguntas! El verdadero poder de la API Reflection viene en combinación con las anotaciones en tiempo de ejecución, de las que probablemente hablaremos en un artículo futuro sobre el lado oscuro de Java. ¡Gracias por su atención!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION