Reflexión y anotaciones
En resumen: La reflexión es el acceso del programa a sus propias clases mientras se ejecuta: campos y métodos por nombre, y las anotaciones son notas que puede leer.
Problema
El campo que toString olvidó
El toString del pedido está escrito a mano. Más tarde, alguien añade un campo para el descuento:
class Order { private int number = 1042; private String customer = "María López"; private double total = 86.40; private int discount = 10; // añadido después
@Override public String toString() { return "Order[number=" + number + ", customer=" + customer + ", total=" + total + "]"; }}
System.out.println(new Order());Order[number=1042, customer=María López, total=86.4]El descuento no está en la salida, porque nadie se acordó de actualizar toString. Con diez clases, esto pasa constantemente.
@Test, JUnit encuentra y ejecuta esos métodos, aunque nunca ha visto tu clase. ¿Cómo? Las dos cosas funcionan con la misma idea: un programa puede examinar sus propias clases mientras se ejecuta.La idea
La clase como objeto
Para cada clase, la JVM guarda un objeto de tipo Class que la describe: nombre, padre, interfaces, campos, métodos, constructores. El acceso a él se llama reflexión (en inglés, reflection: el programa se ve a sí mismo como en un espejo). Hay tres formas de obtener este objeto:
Order.class, cuando conoces la clase mientras escribes el código;obj.getClass(), cuando tienes un objeto (da la clase real del objeto, no el tipo de la variable);Class.forName("java.util.ArrayList"), cuando solo tienes el nombre como texto.
Class<?> type = Class.forName("java.util.ArrayList");System.out.println("Nombre: " + type.getName());System.out.println("Nombre corto: " + type.getSimpleName());System.out.println("Padre: " + type.getSuperclass().getName());for (Class<?> parent : type.getInterfaces()) { System.out.println("Interfaz: " + parent.getName());}Nombre: java.util.ArrayListNombre corto: ArrayListPadre: java.util.AbstractListInterfaz: java.util.ListInterfaz: java.util.RandomAccessInterfaz: java.lang.CloneableInterfaz: java.io.SerializableforName lanza la excepción comprobada ClassNotFoundException si no existe esa clase. Por eso main la declara con throws.Míralo
Los campos de cualquier objeto
getDeclaredFields() da los campos de la clase como objetos Field (de java.lang.reflect). Cada uno conoce su nombre y su tipo, y puede leer su valor de un objeto dado:static void dump(Object obj) throws IllegalAccessException { Class<?> type = obj.getClass(); System.out.println(type.getSimpleName() + ":"); for (Field field : type.getDeclaredFields()) { field.setAccessible(true); System.out.println(" " + field.getName() + " (" + field.getType().getSimpleName() + ") = " + field.get(obj)); }}class Room { private int number = 214; private boolean seaView = true;}
dump(new Order());dump(new Room());Order: number (int) = 1042 customer (String) = María López total (double) = 86.4 discount (int) = 10Room: number (int) = 214 seaView (boolean) = truediscount ya está ahí. Los campos son private, por eso antes de get está setAccessible(true). Sin él, get lanza IllegalAccessException.static void dump(Object obj) throws IllegalAccessException { Class<?> type = obj.getClass(); System.out.println(type.getSimpleName() + ":"); for (Field field : type.getDeclaredFields()) { field.setAccessible(true); System.out.println(" " + field.getName() + " (" + field.getType().getSimpleName() + ") = " + field.get(obj)); }}Paso a paso, en palabras
- dump recibe un objeto Order. Sus campos son private: desde fuera no se ven.
- getClass() da el objeto Class que describe la clase Order.
- La primera línea es el nombre corto de la clase.
- getDeclaredFields() da 4 objetos Field: cada uno conoce su nombre y su tipo.
- setAccessible(true) abre el campo privado number, y field.get(obj) lee su valor del objeto: 1042.
- customer (String) = María López.
- total (double) = 86.4.
- discount: el campo que toString olvidó aparece solo, porque la reflexión obtiene todos los campos.
- El método no sabe nada de Order. Para Room imprime number y seaView de la misma forma.
Más precisamente, para curiosos
getDeclaredFields()da todos los campos de la propia clase, también los privados, pero no los del padre.getFields()da solo los campospublic, también los heredados.- Java no garantiza el orden de los campos. HotSpot (la JVM de Temurin, Oracle y OpenJDK) los devuelve en el orden de la clase, pero el código que depende del orden es frágil.
- Si el programa está dividido en módulos (
module-info.java), no se permitesetAccessiblesobre campos privados de otro módulo. Por eso, por ejemplo, no puedes meter mano en los campos privados deString.
Pruébalo
¿De qué clase es?
¿Qué imprimirá el programa?
class Document {}
class Invoice extends Document {}
Document doc = new Invoice();System.out.println(doc.getClass().getSimpleName());System.out.println(doc.getClass().getSuperclass().getSimpleName());System.out.println(Document.class.getSuperclass().getSimpleName());doc es una variable de tipo Document, pero getClass() da la clase real del objeto: Invoice. Su padre es Document. Document no pone extends, así que su padre es Object, el padre común del módulo de herencia. Para el propio Object, getSuperclass() devuelve null: no hay nada por encima.Míralo
Objeto, método y campo privado por nombre
class Product { private String name; private double price;
public Product(String name, double price) { this.name = name; this.price = price; }
public void applyDiscount(int percent) { price = price * (100 - percent) / 100; }
private String label() { return name + ": " + price + " €"; }}Class<Product> type = Product.class;Product coffee = type.getDeclaredConstructor(String.class, double.class).newInstance("Café", 10.0);
Method discount = type.getMethod("applyDiscount", int.class);discount.invoke(coffee, 20);
Field price = type.getDeclaredField("price");price.setAccessible(true);System.out.println("Precio con descuento: " + price.get(coffee));price.set(coffee, 9.5);
Method label = type.getDeclaredMethod("label");label.setAccessible(true);System.out.println(label.invoke(coffee));Precio con descuento: 8.0Café: 9.5 €getDeclaredConstructor(String.class, double.class)encuentra el constructor por los tipos de sus parámetros, ynewInstancelo llama.getMethod("applyDiscount", int.class)encuentra el método público, einvoke(objeto, argumentos)lo llama sobrecoffee.getDeclaredFieldygetDeclaredMethodencuentran también cosas privadas. Después desetAccessible(true)puedes leer, escribir y llamar.
main lleva throws Exception: cada uno de estos pasos lanza su propia excepción comprobada (no existe ese método, no hay acceso, etc.).
Más precisamente, para curiosos
ArrayList, creado y rellenado sin que en el código aparezca new ArrayList ni .add:Object list = Class.forName("java.util.ArrayList").getDeclaredConstructor().newInstance();Method add = list.getClass().getMethod("add", Object.class);add.invoke(list, "Café");add.invoke(list, "Té");System.out.println(list);[Café, Té]add recibe un Object, por eso lo buscamos con getMethod("add", Object.class).Pruébalo
El precio que no está
Elige la línea con el error.
price, pero aquí pone "prise". Para el compilador es texto normal, así que no dice nada. El error aparece solo durante la ejecución: java.lang.NoSuchFieldException: prise.Este es el precio principal de la reflexión:
- el compilador no comprueba los nombres ni los tipos, y el editor no los encuentra cuando cambias un nombre;
- el código es más lento que una llamada normal y más difícil de leer;
-
setAccessiblese salta la encapsulación: puedes romper una regla que la clase protege.
La idea
Tus propias anotaciones
@: @Override, @Test, @Entity. Por sí solas no hacen nada. Les da sentido el código que las lee, a menudo con reflexión. También puedes crear las tuyas. Esta pone una etiqueta a un campo, y el método imprime solo los campos con etiqueta:@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)@interface Label { String value();}class Booking { @Label("Número") private int number = 501;
@Label("Huésped") private String guest = "Carlos Gómez";
@Label("Noches") private int nights = 3;
private String internalNote = "VIP";}static void printLabeled(Object obj) throws IllegalAccessException { for (Field field : obj.getClass().getDeclaredFields()) { Label label = field.getAnnotation(Label.class); if (label != null) { field.setAccessible(true); System.out.println(label.value() + ": " + field.get(obj)); } }}Número: 501Huésped: Carlos GómezNoches: 3@interfacedeclara una anotación.String value();es su valor:@Label("Huésped").@Retention(RetentionPolicy.RUNTIME)dice que la anotación se conserva también mientras el programa se ejecuta. Sin ella, la reflexión no la ve.@Target(ElementType.FIELD)dice dónde se puede poner: aquí, solo en campos.field.getAnnotation(Label.class)devuelve la anotación, onullsi el campo no la tiene.internalNoteno tiene etiqueta y no se imprime.
Más precisamente, para curiosos
RetentionPolicytiene tres valores:SOURCE(solo para el compilador, como@Override),CLASS(en el archivo.class, pero no durante la ejecución; es el valor por defecto) yRUNTIME.- Una anotación puede tener varios valores y un valor por defecto:
int max() default 100;. Entonces se escribe@Range(min = 1, max = 5). Solo un valor llamadovaluese puede escribir sin su nombre. ElementTypetiene ademásMETHOD,TYPE(una clase),PARAMETER,CONSTRUCTORy otros.
Míralo
Cómo encuentra JUnit las pruebas
@Check es para métodos, y la clase de pruebas comprueba el descuento de la primera lección del módulo (con el viejo error price > 100):@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface Check {}
class DiscountChecks { @Check void tenPercentOff() { expect(45.0, Discount.apply(50, 10)); }
@Check void extraFivePercentFrom100() { expect(85.0, Discount.apply(100, 10)); }
void expect(double expected, double actual) { if (expected != actual) { throw new AssertionError("esperaba " + expected + ", pero es " + actual); } }}static void runChecks(Class<?> type) throws ReflectiveOperationException { Method[] methods = type.getDeclaredMethods(); Arrays.sort(methods, Comparator.comparing(Method::getName)); int passed = 0; int failed = 0; for (Method method : methods) { if (!method.isAnnotationPresent(Check.class)) { continue; } Object instance = type.getDeclaredConstructor().newInstance(); try { method.invoke(instance); System.out.println("OK " + method.getName()); passed++; } catch (InvocationTargetException e) { System.out.println("FALLA " + method.getName() + ": " + e.getCause().getMessage()); failed++; } } System.out.println("Comprobaciones: " + (passed + failed) + ", pasan: " + passed + ", fallan: " + failed);}FALLA extraFivePercentFrom100: esperaba 85.0, pero es 90.0OK tenPercentOffComprobaciones: 2, pasan: 1, fallan: 1JUnit hace lo mismo, solo que con muchos más detalles:
- obtiene los métodos de la clase y los ordena, para que el orden sea siempre el mismo;
- se salta los que no tienen la anotación;
- crea un objeto nuevo para cada prueba, así las pruebas no se estorban entre sí;
- llama al método con
invoke. Si la prueba lanza una excepción,invokela envuelve en unaInvocationTargetException, y la verdadera está engetCause().
assertEquals en JUnit también es un método que lanza AssertionError cuando los valores no coinciden. Igual que expect aquí.
Pruébalo
La anotación que desapareció
@Check está declarada sin @Retention, y todo lo demás es igual:¿Qué imprimirá runChecks?
@Target(ElementType.METHOD)@interface Check {}
class DiscountChecks { @Check void tenPercentOff() { ... }
@Check void extraFivePercentFrom100() { ... }}
runChecks(DiscountChecks.class);@Retention(RetentionPolicy.RUNTIME), la anotación es CLASS: está escrita en el archivo .class, pero la JVM no la carga. isAnnotationPresent(Check.class) devuelve false para todos los métodos, y no se ejecuta ninguno. El programa compila y funciona, simplemente no hace nada. Por eso toda anotación que se lee con reflexión es RUNTIME.Ejercicio
Código que funciona con cualquier clase
Resuelve las tareas en tu editor.
1. Todos los campos. EscribeprintFields(Object obj), que imprima cada campo como nombre = valor, incluidos los heredados. Empieza por obj.getClass() y sube con getSuperclass() hasta Object; los campos del padre van primero.class Vehicle { private String plate = "1234 BCD"; private int year = 2019;}
class Truck extends Vehicle { private double loadTons = 12.5;}plate = 1234 BCDyear = 2019loadTons = 12.5toJson(Object obj), que devuelva los campos como JSON: los nombres entre comillas, el texto entre comillas, los números y true/false sin comillas. Usa StringBuilder e instanceof String.class Ticket { private int seat = 14; private String event = "Concierto"; private double price = 45.0; private boolean vip = false;}{"seat": 14, "event": "Concierto", "price": 45.0, "vip": false}{"number": 214, "seaView": true}@MyTest y @MyBeforeEach y un ejecutor run(Class<?> type). Para cada método @MyTest: un objeto nuevo, luego el método con @MyBeforeEach (si lo hay) y luego la prueba. Si la prueba lanza una excepción, imprime el tipo y el mensaje de la excepción. Pruébalo con estas pruebas:@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface MyTest {}
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@interface MyBeforeEach {}
class Account { private int balance;
Account(int balance) { this.balance = balance; }
int getBalance() { return balance; }
void withdraw(int amount) { if (amount > balance) { throw new IllegalArgumentException("Faltan " + (amount - balance) + " €"); } balance -= amount; }}
class AccountChecks { private Account account;
@MyBeforeEach void fresh() { account = new Account(100); }
@MyTest void withdraw40() { account.withdraw(40); if (account.getBalance() != 60) throw new AssertionError("esperaba 60, pero es " + account.getBalance()); }
@MyTest void withdraw70() { account.withdraw(70); if (account.getBalance() != 30) throw new AssertionError("esperaba 30, pero es " + account.getBalance()); }
@MyTest void withdrawTooMuch() { account.withdraw(130); }}OK withdraw40OK withdraw70FALLA withdrawTooMuch: IllegalArgumentException: Faltan 30 €Tests: 3, pasan: 2, fallan: 1Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Listas · Clases anidadas · Lambdas · Tests con JUnit
- Ejercicios sobre el tema: Programas en el mundo real