Reflection and annotations
Problem
The field that toString forgot
The order's toString is written by hand. Later, someone adds a field for the discount:
class Order { private int number = 1042; private String customer = "Maria Petrova"; private double total = 86.40; private int discount = 10; // added later
@Override public String toString() { return "Order[number=" + number + ", customer=" + customer + ", total=" + total + "]"; }}
System.out.println(new Order());Order[number=1042, customer=Maria Petrova, total=86.4]The discount isn't in the output, because nobody remembered to update toString. With ten classes, this happens all the time.
@Test, JUnit finds and runs those methods, even though it has never seen your class. How? Both things work through the same idea: a program can look at its own classes while it runs.The idea
The class as an object
For every class, the JVM keeps an object of type Class that describes it: name, parent, interfaces, fields, methods, constructors. Getting to it is called reflection (the program sees itself as in a mirror). There are three ways to get this object:
Order.class, when you know the class while you write the code;obj.getClass(), when you have an object (it gives the object's real class, not the type of the variable);Class.forName("java.util.ArrayList"), when you only have the name as text.
Class<?> type = Class.forName("java.util.ArrayList");System.out.println("Name: " + type.getName());System.out.println("Short name: " + type.getSimpleName());System.out.println("Parent: " + type.getSuperclass().getName());for (Class<?> parent : type.getInterfaces()) { System.out.println("Interface: " + parent.getName());}Name: java.util.ArrayListShort name: ArrayListParent: java.util.AbstractListInterface: java.util.ListInterface: java.util.RandomAccessInterface: java.lang.CloneableInterface: java.io.SerializableforName throws the checked ClassNotFoundException if there's no such class. That's why main declares it with throws.See it
The fields of any object
getDeclaredFields() gives the fields of the class as Field objects (from java.lang.reflect). Each one knows its name and type and can read its value from a given object: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) = Maria Petrova total (double) = 86.4 discount (int) = 10Room: number (int) = 214 seaView (boolean) = truediscount field is already there. The fields are private, so before get there's setAccessible(true). Without it, get throws 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)); }}Step by step, in words
- dump gets an Order object. Its fields are private: they can't be seen from outside.
- getClass() gives the Class object that describes the Order class.
- The first line is the short name of the class.
- getDeclaredFields() gives 4 Field objects: each one knows its name and type.
- setAccessible(true) opens the private field number, and field.get(obj) reads its value from the object: 1042.
- customer (String) = Maria Petrova.
- total (double) = 86.4.
- discount: the field that toString forgot shows up by itself, because reflection gets all the fields.
- The function knows nothing about Order. For Room it prints number and seaView the same way.
More precisely, if you are curious
getDeclaredFields()gives all the fields of the class itself, the private ones too, but not the parent's.getFields()gives only thepublicfields, the inherited ones too.- Java doesn't promise the order of the fields. HotSpot (the JVM of Temurin, Oracle and OpenJDK) returns them in the order of the class, but code that relies on the order is fragile.
- If the program is split into modules (
module-info.java),setAccessibleon private fields in another module isn't allowed. That's why, for example, you can't poke into the private fields ofString.
Try it
Whose class?
What will the program print?
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 is a variable of type Document, but getClass() gives the real class of the object: Invoice. Its parent is Document. Document doesn't say extends, so its parent is Object, the common parent from the inheritance module. For Object itself, getSuperclass() returns null: there's nothing above it.See it
Object, method and private field by name
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("Coffee", 10.0);
Method discount = type.getMethod("applyDiscount", int.class);discount.invoke(coffee, 20);
Field price = type.getDeclaredField("price");price.setAccessible(true);System.out.println("Price after discount: " + price.get(coffee));price.set(coffee, 9.5);
Method label = type.getDeclaredMethod("label");label.setAccessible(true);System.out.println(label.invoke(coffee));Price after discount: 8.0Coffee: €9.5getDeclaredConstructor(String.class, double.class)finds the constructor by the types of its parameters, andnewInstancecalls it.getMethod("applyDiscount", int.class)finds the public method, andinvoke(object, arguments)calls it oncoffee.getDeclaredFieldandgetDeclaredMethodfind private things too. AftersetAccessible(true)you can read, write and call.
main has throws Exception: each of these steps throws its own checked exception (no such method, no access and so on).
More precisely, if you are curious
ArrayList, created and filled without new ArrayList or .add appearing in the code:Object list = Class.forName("java.util.ArrayList").getDeclaredConstructor().newInstance();Method add = list.getClass().getMethod("add", Object.class);add.invoke(list, "Coffee");add.invoke(list, "Tea");System.out.println(list);[Coffee, Tea]add takes an Object, so we look it up with getMethod("add", Object.class).Try it
The price that isn't there
Pick the line with the bug.
price, but here it says "prise". To the compiler this is ordinary text, so it says nothing. The error only shows up at run time: java.lang.NoSuchFieldException: prise.This is the main price of reflection:
- the compiler doesn't check the names and types, and the editor doesn't find them when you rename something;
- the code is slower than an ordinary call and harder to read;
-
setAccessiblegets around encapsulation: you can break a rule that the class protects.
The idea
Your own annotations
@: @Override, @Test, @Entity. On their own, they do nothing. Code that reads them gives them meaning, often through reflection. You can make your own, too. This one puts a label on a field, and the method prints only the fields with a label:@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)@interface Label { String value();}class Booking { @Label("Number") private int number = 501;
@Label("Guest") private String guest = "Ivan Georgiev";
@Label("Nights") 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)); } }}Number: 501Guest: Ivan GeorgievNights: 3@interfacedeclares an annotation.String value();is its value:@Label("Guest").@Retention(RetentionPolicy.RUNTIME)says that the annotation stays around while the program runs. Without it, reflection doesn't see it.@Target(ElementType.FIELD)says where it can go: here, only on fields.field.getAnnotation(Label.class)returns the annotation, ornullif the field doesn't have it.internalNotehas no label and isn't printed.
More precisely, if you are curious
RetentionPolicyhas three values:SOURCE(only for the compiler, like@Override),CLASS(in the.classfile, but not at run time; this is the default) andRUNTIME.- An annotation can have several values and a default value:
int max() default 100;. Then you write@Range(min = 1, max = 5). Only a value namedvaluecan be written without its name. ElementTypealso hasMETHOD,TYPE(a class),PARAMETER,CONSTRUCTORand others.
See it
How JUnit finds the tests
@Check annotation is for methods, and the test class checks the discount from the first lesson of the module (with the old bug 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("expected " + expected + ", but was " + 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("PASS " + method.getName()); passed++; } catch (InvocationTargetException e) { System.out.println("FAIL " + method.getName() + ": " + e.getCause().getMessage()); failed++; } } System.out.println("Checks: " + (passed + failed) + ", passed: " + passed + ", failed: " + failed);}FAIL extraFivePercentFrom100: expected 85.0, but was 90.0PASS tenPercentOffChecks: 2, passed: 1, failed: 1JUnit does the same, only with many more details:
- it gets the methods of the class and sorts them, so the order is always the same;
- it skips the ones without the annotation;
- it makes a new object for each test, so the tests don't get in each other's way;
- it calls the method with
invoke. If the test throws,invokewraps the exception in anInvocationTargetException, and the real one is ingetCause().
assertEquals in JUnit is also a method that throws AssertionError when the values differ. Exactly like expect here.
Try it
The annotation that disappeared
@Check is declared without @Retention, and everything else is the same:What will runChecks print?
@Target(ElementType.METHOD)@interface Check {}
class DiscountChecks { @Check void tenPercentOff() { ... }
@Check void extraFivePercentFrom100() { ... }}
runChecks(DiscountChecks.class);@Retention(RetentionPolicy.RUNTIME) the annotation is CLASS: it's written to the .class file, but the JVM doesn't load it. isAnnotationPresent(Check.class) returns false for every method, and not one of them runs. The program compiles and runs, it just doesn't do anything. That's why every annotation that is read with reflection is RUNTIME.Exercise
Code that works with any class
Solve the tasks in your editor.
1. All the fields. WriteprintFields(Object obj), which prints every field as name = value, including the inherited ones. Start from obj.getClass() and go up with getSuperclass() until Object; the parent's fields come first.class Vehicle { private String plate = "CA 1234 BK"; private int year = 2019;}
class Truck extends Vehicle { private double loadTons = 12.5;}plate = CA 1234 BKyear = 2019loadTons = 12.5toJson(Object obj), which returns the fields as JSON: the names in quotes, text in quotes, numbers and true/false without quotes. Use StringBuilder and instanceof String.class Ticket { private int seat = 14; private String event = "Concert"; private double price = 45.0; private boolean vip = false;}{"seat": 14, "event": "Concert", "price": 45.0, "vip": false}{"number": 214, "seaView": true}@MyTest and @MyBeforeEach and a runner run(Class<?> type). For each @MyTest method: a new object, then the method with @MyBeforeEach (if there is one), then the test. If the test throws, print the kind and the message of the exception. Try it with these tests:@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("Short by €" + (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("expected 60, but was " + account.getBalance()); }
@MyTest void withdraw70() { account.withdraw(70); if (account.getBalance() != 30) throw new AssertionError("expected 30, but was " + account.getBalance()); }
@MyTest void withdrawTooMuch() { account.withdraw(130); }}PASS withdraw40PASS withdraw70FAIL withdrawTooMuch: IllegalArgumentException: Short by €30Tests: 3, passed: 2, failed: 1Test