Skip to content
wannacode

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());
Output
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.

There's another question from the start of the module, too. When you write @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());
}
Output
Name: java.util.ArrayList
Short name: ArrayList
Parent: java.util.AbstractList
Interface: java.util.List
Interface: java.util.RandomAccess
Interface: java.lang.Cloneable
Interface: java.io.Serializable
forName 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());
Output
Order:
number (int) = 1042
customer (String) = Maria Petrova
total (double) = 86.4
discount (int) = 10
Room:
number (int) = 214
seaView (boolean) = true
One method prints any object, and the new discount 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
  1. dump gets an Order object. Its fields are private: they can't be seen from outside.
  2. getClass() gives the Class object that describes the Order class.
  3. The first line is the short name of the class.
  4. getDeclaredFields() gives 4 Field objects: each one knows its name and type.
  5. setAccessible(true) opens the private field number, and field.get(obj) reads its value from the object: 1042.
  6. customer (String) = Maria Petrova.
  7. total (double) = 86.4.
  8. discount: the field that toString forgot shows up by itself, because reflection gets all the fields.
  9. 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 the public fields, 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), setAccessible on private fields in another module isn't allowed. That's why, for example, you can't poke into the private fields of String.

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());
Answer

See it

Object, method and private field by name

Through reflection you can create an object, call a method and change a field, knowing only their names:
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));
Output
Price after discount: 8.0
Coffee: €9.5
  • getDeclaredConstructor(String.class, double.class) finds the constructor by the types of its parameters, and newInstance calls it.
  • getMethod("applyDiscount", int.class) finds the public method, and invoke(object, arguments) calls it on coffee.
  • getDeclaredField and getDeclaredMethod find private things too. After setAccessible(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
The example from the old material: an 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);
Output
[Coffee, Tea]
add takes an Object, so we look it up with getMethod("add", Object.class).

Try it

The price that isn't there

The program compiles, but it stops. Which line has the bug?

Pick the line with the bug.

The idea

Your own annotations

Annotations are labels with @: @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));
}
}
}
Output
Number: 501
Guest: Ivan Georgiev
Nights: 3
  • @interface declares 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, or null if the field doesn't have it. internalNote has no label and isn't printed.
More precisely, if you are curious
  • RetentionPolicy has three values: SOURCE (only for the compiler, like @Override), CLASS (in the .class file, but not at run time; this is the default) and RUNTIME.
  • 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 named value can be written without its name.
  • ElementType also has METHOD, TYPE (a class), PARAMETER, CONSTRUCTOR and others.

See it

How JUnit finds the tests

Here's a tiny JUnit. The @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);
}
Output
FAIL extraFivePercentFrom100: expected 85.0, but was 90.0
PASS tenPercentOff
Checks: 2, passed: 1, failed: 1

JUnit does the same, only with many more details:

  1. it gets the methods of the class and sorts them, so the order is always the same;
  2. it skips the ones without the annotation;
  3. it makes a new object for each test, so the tests don't get in each other's way;
  4. it calls the method with invoke. If the test throws, invoke wraps the exception in an InvocationTargetException, and the real one is in getCause().

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);
Answer

Exercise

Code that works with any class

Solve the tasks in your editor.

1. All the fields. Write printFields(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;
}
Output for new Truck()
plate = CA 1234 BK
year = 2019
loadTons = 12.5
2. Mini JSON. Write toJson(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;
}
Output for new Ticket() and new Room()
{"seat": 14, "event": "Concert", "price": 45.0, "vip": false}
{"number": 214, "seaView": true}
3. Your own test runner. Make the annotations @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);
}
}
Output
PASS withdraw40
PASS withdraw70
FAIL withdrawTooMuch: IllegalArgumentException: Short by €30
Tests: 3, passed: 2, failed: 1

Test

Check yourself

1. What does obj.getClass() give you?
2. What do you have to call before you read a private field with field.get(obj)?
3. There is a typo in getDeclaredField("prise"). When will you find out?
4. Why does your own annotation have @Retention(RetentionPolicy.RUNTIME)?
5. How does JUnit find the methods with @Test?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·