¿Para qué sirve la API de Reflection?

El mecanismo de reflexión de Java permite a un desarrollador realizar cambios y obtener información sobre clases, interfaces, campos y métodos en tiempo de ejecución sin conocer sus nombres.

La API de Reflection también le permite crear nuevos objetos, llamar a métodos y obtener o establecer valores de campo.

Hagamos una lista de todo lo que puedes hacer usando la reflexión:

  • 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 la(s) interfaz(es) implementada(s)
  • Cree una instancia de una clase cuyo nombre de clase no se conozca hasta que se ejecute el programa
  • Obtener y establecer el valor de un campo de instancia por nombre
  • Llamar a un método de instancia por su nombre

Casi todas las tecnologías Java modernas utilizan la reflexión. Es la base de la mayoría de los marcos y bibliotecas de Java/Java EE actuales, por ejemplo:

  • Spring frameworks para construir aplicaciones web
  • el marco de prueba JUnit

Si nunca antes se ha encontrado con estos mecanismos, probablemente se esté preguntando por qué es necesario todo esto. La respuesta es bastante simple pero también muy vaga: la reflexión aumenta drásticamente la flexibilidad y la capacidad de personalizar nuestra aplicación y código.

Pero siempre hay pros y contras. Así que vamos a mencionar algunos contras:

  • Violaciones de la seguridad de la aplicación. Reflection nos permite acceder a código que no deberíamos (violación de encapsulación).
  • Restricciones de seguridad. Reflection requiere permisos de tiempo de ejecución que no están disponibles para los sistemas que ejecutan un administrador de seguridad.
  • Bajo rendimiento. Reflection en Java determina los tipos dinámicamente escaneando el classpath para encontrar la clase a cargar. Esto reduce el rendimiento del programa.
  • Difícil de mantener. El código que utiliza la reflexión es difícil de leer y depurar. Es menos flexible y más difícil de mantener.

Trabajar con clases usando la API de Reflection

Todas las operaciones de reflexión comienzan con un objeto java.lang.Class . Para cada tipo de objeto, se crea una instancia inmutable de java.lang.Class . Proporciona métodos para obtener propiedades de objetos, crear nuevos objetos y llamar a métodos.

Veamos la lista de métodos básicos para trabajar con java.lang.Class :

Método Acción
Cadena getNombre(); Devuelve el nombre de la clase.
int obtenerModificadores(); Devuelve modificadores de acceso
Paquete obtenerPaquete(); Devuelve información sobre un paquete.
Clase obtenerSuperclase(); Devuelve información sobre una clase padre
Clase[] getInterfaces(); Devuelve una matriz de interfaces.
Constructor[] obtenerConstructores(); Devuelve información sobre los constructores de clases.
Campos[] obtenerCampos(); Devuelve los campos de una clase.
Campo getField(String fieldName); Devuelve un campo específico de una clase por nombre
Método[] obtenerMétodos(); Devuelve una matriz de métodos.

Estos son los métodos más importantes para obtener datos sobre clases, interfaces, campos y métodos. También existen métodos que le permiten obtener o establecer valores de campo y acceder a campos privados . Los veremos un poco más tarde.

En este momento, hablaremos sobre cómo obtener java.lang.Class en sí. Tenemos tres maneras de hacer esto.

1. Usando Class.forName

En una aplicación en ejecución, debe usar el método forName(String className) para obtener una clase.

Este código demuestra cómo podemos crear clases usando la reflexión. Vamos a crear una clase Person con la que podamos trabajar:


package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Y la segunda parte de nuestro ejemplo es el código que usa la reflexión:


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Este enfoque es posible si se conoce el nombre completo de la clase. Luego puede obtener la clase correspondiente usando el método estático Class.forName() . Este método no se puede utilizar para tipos primitivos.

2. Usando .class

Si un tipo está disponible pero no hay ninguna instancia de él, puede obtener la clase agregando .class al nombre del tipo. Esta es la forma más fácil de obtener la clase de un tipo primitivo.


Class aClass = Person.class;

3. Usando .getClass()

Si un objeto está disponible, la forma más sencilla de obtener una clase es llamar a object.getClass() .


Person person = new Person();
Class aClass = person.getClass();

¿Cuál es la diferencia entre los dos últimos enfoques?

Use A.class si sabe qué objeto de clase le interesa en el momento de la codificación. Si no hay ninguna instancia disponible, debe usar .class .

Obtener los métodos de una clase.

Veamos los métodos que devuelven los métodos de nuestra clase: getDeclaredMethods() y getMethods() .

getDeclaredMethods() devuelve una matriz que contiene objetos de método para todos los métodos declarados de la clase o interfaz representada por el objeto de clase, incluidos los métodos públicos, privados, predeterminados y protegidos, pero no los métodos heredados.

getMethods() devuelve una matriz que contiene objetos Método para todos los métodos públicos de la clase o interfaz representada por el objeto Clase: los declarados por la clase o interfaz, así como los heredados de superclases y superinterfaces.

Echemos un vistazo a cómo funciona cada uno de ellos.

Comencemos con getDeclaredMethods() . Para ayudarnos nuevamente a comprender la diferencia entre los dos métodos, a continuación trabajaremos con la clase Abstract Numbers . Escribamos un método estático que convertirá nuestra matriz Method en List<String> :


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

Aquí está el resultado de ejecutar este código:

byteValue
shortValue
intValue
longValue
float floatValue;
valor doble

Estos son los métodos declarados dentro de la clase Number . ¿Qué devuelve getMethods() ? Cambiemos dos líneas en el ejemplo:


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

Haciendo esto, veremos el siguiente conjunto de métodos:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
esperar
esperar
esperar
es igual
a la cadena
hashCode
getClass
notificar
notificar a todos

Debido a que todas las clases heredan Object , nuestro método también devuelve los métodos públicos de la clase Object .

Obtener los campos de una clase.

Los métodos getFields y getDeclaredFields se utilizan para obtener los campos de una clase. Como ejemplo, veamos la clase LocalDateTime . Reescribiremos nuestro código:


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

Como resultado de ejecutar este código, obtenemos el conjunto de campos contenidos en la clase LocalDateTime.

MIN
MAX
serialVersionUID
fecha
hora

Por analogía con nuestro examen anterior de métodos, veamos qué sucede si cambiamos un poco el código:


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

Producción:

MÍN.
MÁX.

Ahora vamos a averiguar la diferencia entre estos métodos.

El método getDeclaredFields devuelve una matriz de objetos Field para todos los campos declarados por la clase o interfaz representada por este Class objeto.

El método getFields devuelve una matriz de objetos Field para todos los campos públicos de la clase o interfaz representada por el Class objeto.

Ahora miremos dentro de LocalDateTime .

la clase MÍN y MÁX los campos son públicos, lo que significa que serán visibles a través del método getFields . Por el contrario, el Date , Time , serialVersionUID Los métodos tienen el modificador privado , lo que significa que no serán visibles a través del método getFields , pero podemos obtenerlos usando getDeclaredFields . Así es como podemos acceder a los objetos Field para campos privados .

Descripciones de otros métodos

Ahora es el momento de hablar sobre algunos métodos de la clase Class , a saber:

Método Acción
obtenerModificadores Obtener los modificadores para nuestra clase.
obtenerPaquete Obtener el paquete que contiene nuestra clase.
obtenerSuperclase Obtener la clase padre
obtener interfaces Obtener una matriz de interfaces implementadas por una clase
obtenerNombre Obtener el nombre de clase completo
obtenerNombreSimple Obtener el nombre de una clase

obtenerModificadores()

Se puede acceder a los modificadores usando un Clase objeto.

Los modificadores son palabras clave como public , static , interface , etc. Obtenemos modificadores usando el método getModifiers() :


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

Este código establece el valor de un int variable que es un campo de bits. Cada modificador de acceso se puede activar o desactivar configurando o borrando el bit correspondiente. Podemos verificar los modificadores usando los métodos en la clase java.lang.reflect.Modifier :


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Recordemos cómo se ve la declaración de nuestra Persona :


public class Person {
   …
}

Obtenemos la siguiente salida:

Modificadores de clase: 1
Es público: verdadero
Es estático: falso
Es final: falso
Es abstracto: falso
Es interfaz: falso

Si hacemos nuestra clase abstracta, entonces tenemos:


public abstract class Person { … }

y esta salida:

Modificadores de clase: 1025
Es público: verdadero
Es estático: falso
Es final: falso
Es abstracto: verdadero
Es interfaz: falso

Cambiamos el modificador de acceso, lo que significa que también cambiamos los datos devueltos a través de los métodos estáticos de la clase Modifier .

obtenerPaquete()

Conociendo solo una clase, podemos obtener información sobre su paquete:


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

obtenerSuperclase()

Si tenemos un objeto Clase, entonces podemos acceder a su clase principal:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

Obtenemos la conocida clase Object :


class java.lang.Object

Pero si nuestra clase tiene otra clase principal, la veremos en su lugar:


package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

Aquí tenemos nuestra clase padre:


class com.company.Human

getInterfaces()

Así es como podemos obtener la lista de interfaces implementadas por la clase:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

Y no olvidemos cambiar nuestra clase Person :


public class Person implements Serializable { … }

Producción:

[interfaz java.io.Serializable]

Una clase puede implementar muchas interfaces. Es por eso que obtenemos una serie de Clase objetos. En la API de Java Reflection, las interfaces también están representadas por Clase objetos.

Tenga en cuenta: el método devuelve solo las interfaces implementadas por la clase especificada, no su clase principal. Para obtener una lista completa de las interfaces implementadas por la clase, debe consultar tanto la clase actual como todos sus ancestros en la cadena de herencia.

getNombre() & getNombreSimple() & getNombreCanonical()

Escribamos un ejemplo que involucre un primitivo, una clase anidada, una clase anónima y la clase String :


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

Resultado de nuestro programa:

int class (primitivo):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (clase ordinaria):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (clase anidada):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (clase interna anónima):
getName() ): TestReflection$1
getCanonicalName()): nulo
getSimpleName()):
getTypeName(): TestReflection$1

Ahora analicemos la salida de nuestro programa:

  • getName() devuelve el nombre de la entidad.

  • getCanonicalName() devuelve el nombre canónico de la clase base, tal como lo define la especificación del lenguaje Java. Devuelve nulo si la clase base no tiene un nombre canónico (es decir, si es una clase local o anónima o una matriz cuyo tipo de elemento no tiene un nombre canónico).

  • getSimpleName() devuelve el nombre simple de la clase base como se especifica en el código fuente. Devuelve una cadena vacía si la clase base es anónima.

  • getTypeName() devuelve una cadena informativa para el nombre de este tipo.