Refelection & Java Doc. Practical Tasks
A reflection-based test fails, and the report shows null instead of the reason. That is exactly what you get when you catch the exception from Method.invoke() and print its message:
// in class Calculator: public int divide(int a, int b) { return a / b; }
Method divide = Calculator.class.getMethod("divide", int.class, int.class);
try {
divide.invoke(new Calculator(), 1, 0);
} catch (InvocationTargetException e) {
System.out.println(e.getMessage()); // Output: null
System.out.println(e.getCause()); // Output: java.lang.ArithmeticException: / by zero
} These Java reflection exercises practice the module «Reflection API and Javadoc in Java»: you will print the structure of a class by its name, write a universal toString() and a DTO mapper that read private fields, load plugins by class name from a properties file, build a mini test runner on top of Method.invoke(), and document a class so that the javadoc command generates HTML without doclint errors. Every task comes with a hint, and the section after the tasks collects the slips that break such solutions most often.
What to Review Before You Start
The five Reflection API tasks need four skills: getting a Class object by name, iterating over fields, constructors and methods with getDeclared*(), and using them through newInstance(), invoke(), get() and set(). The sixth task needs the Javadoc tags and the options of the javadoc command. Each rule is explained with an example in the lesson sections below.
| Lesson | What you need from it | Tasks |
|---|---|---|
| getClass() vs .class vs Class.forName() | Class.forName() and ClassNotFoundException | Class Inspector, Plugin Factory, Test Runner |
| Superclass, package and modifiers | getSuperclass(), getInterfaces(), Modifier.toString() | Class Inspector |
| Constructors, methods and fields: getDeclared*() vs get*() | The difference between getDeclaredFields() and getFields(), finding a member by name | All reflection tasks |
| Invoking methods and accessing fields | getDeclaredConstructor().newInstance(), invoke(), setAccessible(true) | toString(), Plugin Factory, DTO Mapper, Test Runner |
| Javadoc tags | @param, @return, @throws, {@code}, {@link}, {@value} | Javadoc |
| The javadoc command | Options -d, -author, -version, -encoding | Javadoc |
Inspect a Class Structure by Its Name
Write a program that reads a fully qualified class name from the console (for example, shop.Product) and prints the structure of the class:
- the modifiers and the fully qualified name of the class; choose the word
classorinterfacewithisInterface(); - the superclass and the implemented interfaces;
- all fields declared in the class: modifiers, type, name;
- all declared constructors: modifiers and parameter types;
- all declared methods: modifiers, return type, name and parameter types.
Include private members. Sort fields and methods by name and constructors by the number of parameters; print member types by their simple names (getSimpleName()). If the class does not exist, the program prints Class not found: ... instead of a stack trace.
Test the program on this class:
package shop;
public class Product implements Comparable<Product> {
public static final int MAX_NAME = 100;
private String name;
protected double price;
public Product() {
}
private Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
@Override
public int compareTo(Product other) {
return Double.compare(price, other.price);
}
private static void log(String message) {
System.out.println(message);
}
} Expected output for the input shop.Product:
public class shop.Product
Superclass: java.lang.Object
Interfaces: java.lang.Comparable
Fields:
public static final int MAX_NAME
private String name
protected double price
Constructors:
public Product()
private Product(String, double)
Methods:
public int compareTo(Product)
public String getName()
private static void log(String) For the input shop.Prodcut: Class not found: shop.Prodcut
Not obvious at first
Modifiers come as a bit mask — turn them into text with Modifier.toString(m.getModifiers()); parameter types come from getParameterTypes(), the return type from getReturnType(). If a second method public volatile int compareTo(Object) shows up in the output, the program is not wrong: for Comparable<Product> the compiler generates a synthetic bridge method, and its flag shares a bit with volatile. Skip such methods with m.isSynthetic(). Sorting: Arrays.sort(methods, Comparator.comparing(Method::getName)).
Universal toString() with Reflection
Write a class ReflectionToString with a static method String toString(Object obj) that builds a string of the form ClassName{field1=value1, field2=value2} for any object:
- all fields of the object are printed, including
privateones and those inherited from superclasses; superclass fields come first; staticfields are not printed — they belong to the class, not to the object;- for
nullthe method returns the stringnull; - field values are turned into strings with
String.valueOf(); nested objects are not inspected recursively.
Test the method on the Student and Aspirant classes from the exercise «Inheritance: Student and Aspirant». If you solved it, take your own classes, add a research topic field to Aspirant and a static counter of created objects to Student. If not, use these:
class Student {
private static int count;
private String firstName;
private String lastName;
private String group;
protected double averageMark;
Student(String firstName, String lastName, String group, double averageMark) {
this.firstName = firstName;
this.lastName = lastName;
this.group = group;
this.averageMark = averageMark;
count++;
}
double getScholarship() {
return averageMark == 5 ? 2000 : 1900;
}
}
class Aspirant extends Student {
private String researchWork;
Aspirant(String firstName, String lastName, String group, double averageMark, String researchWork) {
super(firstName, lastName, group, averageMark);
this.researchWork = researchWork;
}
@Override
double getScholarship() {
return averageMark == 5 ? 2500 : 2200;
}
} Student[] students = {
new Student("Anna", "Smith", "CS-11", 4.5),
new Aspirant("John", "Brown", "PH-21", 5.0, null)
};
for (Student s : students) {
System.out.println(ReflectionToString.toString(s));
}
System.out.println(ReflectionToString.toString(null)); Expected output:
Student{firstName=Anna, lastName=Smith, group=CS-11, averageMark=4.5}
Aspirant{firstName=John, lastName=Brown, group=PH-21, averageMark=5.0, researchWork=null}
null The array element is declared as Student, yet the second line says Aspirant: getClass() returns the actual type of the object, not the type of the variable.
Then override toString() in Student only, with a single line return ReflectionToString.toString(this);, and make sure that System.out.println(s) in the same loop prints the same — Aspirant inherits the method, and its new field still ends up in the string.
Where to start
Walk up the hierarchy with a loop for (Class<?> c = obj.getClass(); c != Object.class; c = c.getSuperclass()), collect the classes into a list and iterate over it from the end — this way superclass fields come first. Call field.setAccessible(true) before field.get(obj), otherwise a private field gives IllegalAccessException; Modifier.isStatic(field.getModifiers()) filters out static fields. The specification of getDeclaredFields() does not guarantee the order of fields within a class, but HotSpot usually returns them in declaration order. Do not call the method recursively on field values: on a String object it would reach into internal JDK fields and get InaccessibleObjectException.
Plugin Factory: Load a Class by Name from a Properties File
An application has to plug in string processors (plugins) without knowing their classes at compile time: the class names are stored in a properties file. Create an interface and three classes in the package plugins:
package plugins;
public interface Plugin {
String execute(String input);
} package plugins;
public class UpperCasePlugin implements Plugin {
@Override
public String execute(String input) {
return input.toUpperCase();
}
} package plugins;
public class ReversePlugin implements Plugin {
@Override
public String execute(String input) {
return new StringBuilder(input).reverse().toString();
}
} package plugins;
public class GreetingPlugin implements Plugin {
private final String greeting;
public GreetingPlugin(String greeting) {
this.greeting = greeting;
}
@Override
public String execute(String input) {
return greeting + ", " + input;
}
} The file plugins.properties in the project root:
plugin.1=plugins.UpperCasePlugin
plugin.2=plugins.ReversePlugin
plugin.3=plugins.GreetingPlugin
plugin.4=plugins.Missing
plugin.5=java.lang.StringBuilder Write a program that:
- reads the file and goes through the entries in key order;
- gets the class with
Class.forName()and checks that it implementsPluginbefore creating any object; - creates an object with
getDeclaredConstructor().newInstance(); - finds the
executemethod withgetMethod("execute", String.class), calls it throughinvoke()with the string"Hello"and prints[SimpleClassName] result; - prints a separate message for each problem and moves on to the next plugin:
ClassNotFoundException,NoSuchMethodException(no constructor without parameters) and a class that does not implementPlugin.
Expected output:
[UpperCasePlugin] HELLO
[ReversePlugin] olleH
No no-arg constructor: plugins.GreetingPlugin
Class not found: plugins.Missing
Not a plugin: java.lang.StringBuilderHint: the properties file
The Properties class is covered in Legacy Classes in Java: Properties props = new Properties();, then try (Reader reader = new FileReader("plugins.properties", StandardCharsets.UTF_8)) { props.load(reader); }. Properties extends Hashtable, so the key order is not guaranteed — iterate over new TreeSet<>(props.stringPropertyNames()). Check the type without creating an object: Plugin.class.isAssignableFrom(c). An exception thrown inside the constructor or execute() arrives wrapped in InvocationTargetException. In real code, after the check you simply cast the object to Plugin and call execute() directly — invoke() is here for practice.
DTO Mapper: Copy Fields with Matching Names
Write a class FieldMapper with a method static <T> T copy(Object source, Class<T> targetClass) that creates an object of targetClass and copies field values from source into it:
- only fields that exist in both classes with the same name and the same type are copied;
- if the name matches but the type does not, the field is not copied, and the console shows
Skipped: name (type -> type); staticandfinalfields are not copied;- all other fields of the new object keep their default values.
Test the mapper on these classes:
class User {
private long id;
private String login;
private String passwordHash;
private int age;
User(long id, String login, String passwordHash, int age) {
this.id = id;
this.login = login;
this.passwordHash = passwordHash;
this.age = age;
}
}
class UserDto {
private long id;
private String login;
private String age;
private boolean active;
@Override
public String toString() {
return "UserDto{id=" + id + ", login=" + login + ", age=" + age + ", active=" + active + "}";
}
} UserDto dto = FieldMapper.copy(new User(7, "anna", "5f4dcc3b", 30), UserDto.class);
System.out.println(dto); Expected output: passwordHash does not get into the DTO, and age is skipped because the types differ:
Skipped: age (int -> String)
UserDto{id=7, login=anna, age=null, active=false}Easy to get wrong
Compare types with sourceField.getType() == targetField.getType(): int.class and Integer.class are different objects, so int and Integer also end up among the skipped fields. Look up the target field by name with getDeclaredField(name): if it does not exist, you get NoSuchFieldException — catch it and skip the field silently (or build a Map<String, Field> in advance). setAccessible(true) is needed on both fields. To avoid listing five checked reflection exceptions, declare throws ReflectiveOperationException on copy() — it is their common supertype.
Mini Test Runner with Reflection
Write a program that runs the tests of a class whose name is read from the console. A test is a public, non-static method without parameters that returns void and whose name starts with test. All other methods are ignored.
- Every test is called through
invoke()on a new instance of the class. - A test passes (
PASSED) if the method completes without an exception, and fails (FAILED) if it throws any exception, includingAssertionError. For a failed test, print the exception thrown by the test method itself. - Tests run in alphabetical order, and a summary is printed at the end.
A test class to check the runner:
public class CalculatorTest {
public void testAdd() {
check(2 + 2 == 4, "2 + 2 should be 4");
}
public void testSubtract() {
check(5 - 3 == 1, "5 - 3 should be 1");
}
public void testParse() {
Integer.parseInt("abc");
}
public void helper() { // not a test: the name does not start with test
}
public void testWithParam(int x) { // not a test: has a parameter
}
public int testReturnsValue() { // not a test: returns int
return 1;
}
public static void testStatic() { // not a test: static
}
private void testPrivate() { // not a test: not public
}
private static void check(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
} Expected output for the input CalculatorTest:
PASSED testAdd
FAILED testParse: java.lang.NumberFormatException: For input string: "abc"
FAILED testSubtract: java.lang.AssertionError: 5 - 3 should be 1
Total: 3, passed: 1, failed: 2How to tell a failed test
invoke() wraps any exception of the test method in InvocationTargetException; its getMessage() returns null, and the exception of the test itself is e.getCause(). A missing constructor without parameters (NoSuchMethodException) is an error of the test class, not of a test: report it once and stop the program. The test filter: getMethods() already drops non-public methods, so it remains to check m.getName().startsWith("test"), m.getParameterCount() == 0, m.getReturnType() == void.class and !Modifier.isStatic(m.getModifiers()). A new instance for every test keeps fields changed by one test from affecting the next one.
Document a Class with Javadoc and Generate HTML
This bank account class is documented with mistakes: the class comment is a regular comment, the names in @param do not match the parameters, the text contains a bare < sign and generics, and the description of withdraw() contradicts the code.
package com.example.bank;
import java.util.ArrayList;
import java.util.List;
/*
* Bank account
*/
public class Account {
public static final double MIN_DEPOSIT = 10.0;
private double balance;
private final List<String> history = new ArrayList<>();
/**
* Creates an account.
* @param initial the initial balance
*/
public Account(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount < MIN_DEPOSIT) {
throw new IllegalArgumentException("Minimum amount: " + MIN_DEPOSIT);
}
balance += amount;
history.add("+" + amount);
}
/**
* Withdraws money if amount < balance.
* @param sum the amount
*/
public double withdraw(double amount) {
if (amount > balance) {
throw new IllegalStateException("Insufficient funds");
}
balance -= amount;
history.add("-" + amount);
return balance;
}
/**
* Returns a List<String> of operations.
*/
public List<String> getHistory() {
return List.copyOf(history);
}
} - Put the class into
src/com/example/bank/Account.javaand generate the documentation withjavadoc -d docs -sourcepath src -subpackages com.example -author -version -encoding UTF-8. Save the list of doclint errors and warnings. - Turn the class comment into Javadoc: a short first sentence and the tags
@author,@version,@since. - Document the constant
MIN_DEPOSITso that its value is inserted into the text with the{@value}tag. - Fix
@paramin the constructor and inwithdraw(); add@returnand@throwswherever they are needed, including unchecked exceptions. - Write a comment for
deposit(), and in the description ofgetHistory()link todeposit()andwithdraw()with{@link}and state that an unmodifiable copy is returned. - Make the description of
withdraw()match the code (the whole balance can be withdrawn), and wrap conditions and generics in{@code}. - Run the same command again: no
errororwarninglines must remain in the output. Switching the check off with-Xdoclint:noneis not allowed. - Open
docs/index.htmland find on theAccountpage the author, the version, the value10.0in the description of the constant, and the Parameters, Returns and Throws sections.
Checklist before the second run
The class comment starts with /**, not /* — otherwise javadoc simply does not see it. Every parameter has its own @param with the name exactly as in the signature, the constructor too. Newer JDKs also warn about public elements without any comment (warning: no comment) — so document both deposit() and MIN_DEPOSIT. A link to a method with parameters includes the types: {@link #withdraw(double)}. The private fields balance and history do not need documenting: without the -private option they do not get into the HTML. If the author and version are not shown on the page, check that the command contains -author and -version.
Where Solutions Break Without a Warning
Reflection code almost always compiles: method and field names are plain strings, and the compiler does not check them. That is why mistakes in these tasks show up only at runtime — as an exception or, worse, as wrong output with no exception at all. The table lists the typical slips from the tasks on this page.
| Mistake | What happens | How to fix it |
|---|---|---|
getMethod("setAge", Integer.class) for the method setAge(int) | NoSuchMethodException: int.class and Integer.class are different Class objects | Pass the exact parameter types: int.class |
field.get(obj) on a private field right away | IllegalAccessException | Call field.setAccessible(true) first |
| Printing or copying every field in a row | static fields slip in: a class-wide counter looks like a property of the object and gets overwritten on copying | Filter them out with Modifier.isStatic(field.getModifiers()) |
catch (InvocationTargetException e) and printing e.getMessage() | Prints null, the real cause is lost | Take the exception thrown by the method from e.getCause() |
Printing all getDeclaredMethods() of a class that implements Comparable<T> | An extra public volatile int compareTo(Object) appears — a synthetic bridge method generated by the compiler | Skip methods for which isSynthetic() returns true |
Class.forName("Product") for a class in the package shop | ClassNotFoundException: the fully qualified name is required | Class.forName("shop.Product"); a nested class is written with $ — see Where developers get tripped up |
A bare < or List<String> in Javadoc text | error: malformed HTML or error: unknown tag: String, the documentation build stops | Wrap code in {@code ...} — see Common Javadoc mistakes |
Check your code
Before you call a task done, go through the code style checklist: write Class<?> instead of the raw Class, never swallow reflection exceptions in an empty catch, and do not declare throws Exception on main when the task asks you to handle the error and print a message.
Frequently Asked Questions
Which Java version do I need for these exercises?
Any JDK from 11 on: the tasks use only java.lang.reflect and the javadoc command from the JDK, no third-party libraries. Create objects with getDeclaredConstructor().newInstance() — Class.newInstance() has been deprecated since Java 9. In Javadoc the inline tag {@return} is available since Java 16 and {@snippet} since Java 18. Since JDK 18 source files are read as UTF-8 by default; on older JDKs on Windows add -encoding UTF-8.
How is the mini test runner different from JUnit?
The idea is the same: find the test methods through reflection, create a new instance of the class for every test and call the method with Method.invoke(). Finding tests by the test prefix is the JUnit 3 convention, where a test class extended TestCase. JUnit 4 and 5 find tests by the @Test annotation, compare expected and actual values with assertEquals() and build reports. The task itself is Mini Test Runner with Reflection.
Why are fields and methods not printed in source order?
The documentation of getDeclaredFields() and getDeclaredMethods() states that the elements of the returned array are not in any particular order. HotSpot usually returns fields in declaration order, but methods may come in an order that differs between JDK versions. If your output has to match a sample, sort it: Arrays.sort(methods, Comparator.comparing(Method::getName)).
Can reflection read the names of method parameters?
Yes, with Method.getParameters() and Parameter.getName() (Java 8+), but by default the compiler does not store parameter names in the .class file, so you get arg0, arg1. The real names appear only if you compile with javac -parameters; Parameter.isNamePresent() tells you which case you have. That is why the class inspector task prints parameter types only.
Comments