Programación Orientada a Objetos

Anotaciones y reflexión

Declara metadatos y descubre clases, campos, métodos y records en ejecución sin confundir flexibilidad con acceso ilimitado.

Desde Java 8

Metadatos que otro código interpreta

Una anotación añade información a una declaración o a un uso de tipo. No ejecuta por sí sola una validación, una consulta ni una prueba. Necesitas un consumidor: el compilador, un procesador de anotaciones o una biblioteca en ejecución. La reflexión permite a ese consumidor inspeccionar tipos y operar sobre objetos sin conocer todas sus clases al escribir el programa.

Ambos mecanismos existen desde antes de Java 8, mínimo de este capítulo. Los ejemplos clásicos funcionan en Java 8 y Java 21; el de records exige Java 16. Úsalos para integrar código que no controlas, no para sustituir llamadas normales.

Declara una anotación y léela

Guarda este programa completo en AnnotationExample.java:

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class AnnotationExample {
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Label {
        String value();
        int priority() default 1;
    }

    @Label("service")
    static class Service {}

    static class Child extends Service {}

    public static void main(String[] args) {
        Label label = Service.class.getAnnotation(Label.class);
        System.out.println(label.value() + ":" + label.priority());
        System.out.println(Child.class.getAnnotation(Label.class).value());
        System.out.println(Child.class.getDeclaredAnnotation(Label.class));
    }
}

Salida exacta:

texto
service:1
service
null

@interface declara el tipo de anotación. Sus elementos parecen métodos sin parámetros: value es obligatorio y priority tiene un valor predeterminado. @Label("service") abrevia @Label(value = "service"); si cambias la prioridad, escribe @Label(value = "service", priority = 3).

Los elementos admiten primitivos, String, Class, enums, otras anotaciones y arrays de esos tipos. No admiten null ni cualquier objeto arbitrario. No puedes calcular un texto con una llamada a un método para pasarlo como valor.

RetenciónQué conservaQuién puede leerla
SOURCESolo el código fuenteCompilador y procesadores de anotaciones
CLASSEl archivo .classHerramientas que analizan bytecode
RUNTIMETambién la información en ejecuciónAPI de reflexión

Sin @Retention, la política es CLASS, no RUNTIME. @Target(TYPE) limita Label a declaraciones de tipos; puedes combinar destinos con un array, como {ElementType.FIELD, ElementType.METHOD}. TYPE_USE permite anotar usos de tipos desde Java 8. El destino no decide cuánto tiempo se conserva la anotación.

getAnnotation devuelve null si no encuentra la anotación. Compruébalo antes de leer sus elementos cuando el contrato no te garantice su presencia. isAnnotationPresent sirve si solo necesitas saber si está.

La herencia tiene un alcance muy concreto

@Inherited permite encontrar una anotación de clase buscando en su superclase. Por eso Child encuentra Label, pero getDeclaredAnnotation devuelve null: no está escrita directamente sobre Child.

No transmite anotaciones de métodos sobrescritos ni de campos. Tampoco busca en interfaces implementadas. Sin @Inherited, getAnnotation no hereda esa anotación de clase. Un framework puede establecer otras reglas de búsqueda; no confundas su comportamiento con el contrato de la API de Java.

Del metadato al comportamiento

  1. 01 Declaración @Label("service") Describe una clase, no ejecuta una acción.
  2. 02 Descubrimiento getAnnotation Lee el metadato conservado en ejecución.
  3. 03 Interpretación label.value() El consumidor decide qué hacer con él.
La anotación describe; el código consumidor aporta el comportamiento.

Class, Field y Method: descubrir no es poder acceder

Obtienes un Class<?> con Type.class, object.getClass() o Class.forName("package.Type"). Esta última forma puede inicializar la clase; no cargues nombres arbitrarios procedentes de una entrada sin validar.

En ReflectionExample.java, busca miembros concretos antes de usarlos:

java
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;

public class ReflectionExample {
    public static class Base {
        public String inherited() { return "base"; }
    }

    public static class User extends Base {
        private String name = "Ada";
        public String greet(String prefix) { return prefix + name; }
        private void reset() { name = "unknown"; }
    }

    public static void main(String[] args) throws ReflectiveOperationException {
        Class<?> type = User.class;
        Object user = type.getDeclaredConstructor().newInstance();
        Field name = type.getDeclaredField("name");
        name.setAccessible(true);
        System.out.println(name.get(user));
        name.set(user, "Luz");
        Method greet = type.getMethod("greet", String.class);
        System.out.println(greet.invoke(user, "Hola, "));
        String[] declared = Arrays.stream(type.getDeclaredMethods())
                .map(Method::getName).sorted().toArray(String[]::new);
        System.out.println(Arrays.toString(declared));
        boolean inherited = Arrays.stream(type.getMethods())
                .anyMatch(method -> method.getName().equals("inherited"));
        System.out.println(inherited);
    }
}

Salida exacta:

texto
Ada
Hola, Luz
[greet, reset]
true

Field.get lee y Field.set escribe sobre la instancia indicada. Method.invoke recibe el receptor y los argumentos; para un método estático el receptor puede ser null. Los primitivos se envuelven al devolverlos como Object.

BúsquedaQué incluyeQué no garantiza
getDeclaredMethods()Métodos declarados en esa clase, privados incluidosAcceso permitido u orden de declaración
getMethods()Métodos públicos, también heredados de clases e interfacesUn orden estable entre ejecuciones
getDeclaredField(name)Campo declarado ahí, aunque sea privadoBúsqueda en superclases
getField(name)Campo público, también heredadoAcceso a campos privados

La JVM no garantiza el orden de los arrays de métodos ni de campos. Aquí ordenas los nombres declarados y solo compruebas la presencia del método heredado: no imprimas un listado sin ordenar y lo presentes como salida fija. Pueden aparecer métodos sintéticos o puente generados por el compilador; filtra con isSynthetic() o isBridge() si tu herramienta no debe tratarlos como negocio.

getMethod busca por nombre y tipos exactos de parámetros, no realiza la resolución de sobrecargas del compilador. Un nombre inexistente provoca NoSuchMethodException; un argumento incompatible puede provocar IllegalArgumentException. Si el método invocado lanza una excepción, invoke la envuelve en InvocationTargetException: inspecciona su causa.

Acceso privado y módulos: una frontera real

setAccessible(true) solicita omitir las comprobaciones de acceso de Java; no convierte un miembro privado en parte de una API pública. El ejemplo actúa sobre una clase propia en el classpath. Cambiar campos privados ajenos rompe encapsulamiento y puede dejar objetos en estados inválidos. No intentes modificar campos final para saltarte el contrato de inmutabilidad.

Si controlas un módulo, puedes declarar opens com.example.model to framework.core; en module-info.java. Una alternativa de despliegue es --add-opens app.core/com.example.model=ALL-UNNAMED para consumidores del classpath. Son permisos concretos, no una receta para abrir todo el JDK. En entornos antiguos con un gestor de seguridad, las comprobaciones de permisos también pueden impedir el acceso reflectivo.

Componentes de records: desde Java 16

Class.isRecord(), getRecordComponents() y RecordComponent son APIs definitivas desde Java 16. Guarda este ejemplo en RecordReflectionExample.java y usa Java 21:

java
import java.lang.reflect.RecordComponent;

public class RecordReflectionExample {
    public record Point(int x, int y) {}

    public static void main(String[] args) throws ReflectiveOperationException {
        Point point = new Point(2, 5);
        System.out.println(Point.class.isRecord());
        for (RecordComponent component : Point.class.getRecordComponents()) {
            System.out.println(component.getName() + ":"
                    + component.getType().getSimpleName() + "="
                    + component.getAccessor().invoke(point));
        }
    }
}

Salida: true, x:int=2 y y:int=5, cada valor en una línea. A diferencia de los métodos, los componentes se devuelven en el orden de la cabecera del record. En una clase que no sea un record, getRecordComponents() devuelve null. ElementType.RECORD_COMPONENT existe desde Java 16; las anotaciones de un componente pueden propagarse al campo, al accesor y al parámetro del constructor según sus destinos aplicables. No todas aparecen en todos esos lugares.

Por qué lo usan los frameworks y cuándo evitarlo

Un serializador descubre propiedades; un contenedor localiza constructores e inyecta dependencias; una herramienta de pruebas busca métodos anotados. La reflexión permite trabajar con clases que la biblioteca no conocía al compilarse. No es la única vía: procesadores de anotaciones pueden generar código al compilar.

No hay un multiplicador universal de coste. Buscar miembros cuesta trabajo; invoke puede añadir comprobaciones, adaptación de argumentos y boxing. El JIT y la versión del JDK influyen. Guarda descriptores ya resueltos cuando proceda, pero no confundas evitar búsquedas con eliminar el coste de cada invocación. Mide con un benchmark adecuado antes de optimizar una ruta caliente; considera MethodHandle o código generado solo si los datos y el diseño lo justifican.

Qué te llevas

  • RUNTIME hace visible una anotación a reflexión; el consumidor le da significado.
  • @Inherited afecta a anotaciones de clase, no a toda la jerarquía de miembros.
  • Descubrir un miembro no garantiza poder acceder a él ni un orden de enumeración.
  • Prefiere interfaces y llamadas directas cuando conoces el contrato al compilar.
  • Reserva la reflexión para integración y herramientas, con permisos y errores explícitos.

Repasa el contrato de los records y el encapsulamiento antes de abrir acceso privado.