Reflection API и JavaDoc в Java. Практические Задания
Тест на рефлексии упал, а в отчёте вместо причины — null. Так бывает, если поймать исключение из Method.invoke() и напечатать его сообщение:
// в классе 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()); // Вывод: null
System.out.println(e.getCause()); // Вывод: java.lang.ArithmeticException: / by zero
} Задачи по рефлексии в Java на этой странице закрепляют модуль «Reflection API и JavaDoc»: вы выведете структуру класса по его имени, напишете универсальный toString() и DTO-маппер, которые читают private-поля, загрузите плагины по имени класса из файла настроек, соберёте мини-тест-раннер на Method.invoke() и задокументируете класс так, чтобы команда javadoc сгенерировала HTML без ошибок doclint. К каждой задаче есть подсказка, а после задач собраны промахи, на которых такие решения ломаются чаще всего.
Что повторить перед решением
Для пяти задач по Reflection API нужно уметь получать объект Class по имени, перебирать поля, конструкторы и методы через getDeclared*() и работать с ними через newInstance(), invoke(), get() и set(). Для шестой задачи — знать теги Javadoc и опции команды javadoc. Каждое правило с примером разобрано в разделах уроков из таблицы.
| Урок | Что понадобится | Задачи |
|---|---|---|
| Три способа получить объект Class | Class.forName() и ClassNotFoundException | Структура класса, Фабрика плагинов, Тест-раннер |
| Суперкласс, пакет и модификаторы | getSuperclass(), getInterfaces(), Modifier.toString() | Структура класса |
| Конструкторы, методы и поля: getDeclared*() и get*() | Разница между getDeclaredFields() и getFields(), поиск члена по имени | Задачи 1–5 |
| Вызов методов и доступ к полям | getDeclaredConstructor().newInstance(), invoke(), setAccessible(true) | toString(), Фабрика плагинов, DTO-маппер, Тест-раннер |
| Теги Javadoc | @param, @return, @throws, {@code}, {@link}, {@value} | Javadoc |
| Команда javadoc | Опции -d, -author, -version, -encoding | Javadoc |
Структура класса по его имени
Интернет-магазин · часть 9 из 12 · ← часть 8 · часть 10 →
Напишите программу, которая читает с консоли полное имя класса (например, shop.Product) и печатает его структуру:
- модификаторы и полное имя класса; слово
classилиinterfaceвыберите поisInterface(); - суперкласс и реализованные интерфейсы;
- все объявленные в классе поля: модификаторы, тип, имя;
- все объявленные конструкторы: модификаторы и типы параметров;
- все объявленные методы: модификаторы, тип возвращаемого значения, имя и типы параметров.
Учитывайте и private-члены. Поля и методы выводите отсортированными по имени, конструкторы — по числу параметров, типы членов — простыми именами (getSimpleName()). Если класса нет, программа выводит Класс не найден: ..., а не stack trace.
Проверьте программу на классе:
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);
}
} Ожидаемый вывод для ввода shop.Product:
public class shop.Product
Суперкласс: java.lang.Object
Интерфейсы: java.lang.Comparable
Поля:
public static final int MAX_NAME
private String name
protected double price
Конструкторы:
public Product()
private Product(String, double)
Методы:
public int compareTo(Product)
public String getName()
private static void log(String) Для ввода shop.Prodcut: Класс не найден: shop.Prodcut
Неочевидный момент
Модификаторы приходят битовой маской — переводите их в текст через Modifier.toString(m.getModifiers()); типы параметров даёт getParameterTypes(), тип результата — getReturnType(). Если в выводе появился второй метод public volatile int compareTo(Object), программа не ошиблась: для Comparable<Product> компилятор генерирует синтетический мост-метод (bridge method), а его флаг совпадает по биту с volatile. Пропускайте такие методы проверкой m.isSynthetic(). Сортировка: Arrays.sort(methods, Comparator.comparing(Method::getName)).
Универсальный toString() через рефлексию
Напишите класс ReflectionToString со статическим методом String toString(Object obj), который для любого объекта строит строку вида ИмяКласса{поле1=значение1, поле2=значение2}:
- выводятся все поля объекта, включая
privateи унаследованные от суперклассов; поля суперкласса идут первыми; static-поля не выводятся — они принадлежат классу, а не объекту;- для
nullметод возвращает строкуnull; - значения полей превращаются в строку через
String.valueOf(), вложенные объекты рекурсивно не разбираются.
Проверьте метод на классах Student и Aspirant из задачи «Наследование Student, Aspirant». Если вы её решали, возьмите свои классы и добавьте в Aspirant поле с темой научной работы, а в Student — статический счётчик созданных объектов. Если нет — используйте эти:
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("Анна", "Иванова", "КН-11", 4.5),
new Aspirant("Иван", "Петров", "ФИ-21", 5.0, null)
};
for (Student s : students) {
System.out.println(ReflectionToString.toString(s));
}
System.out.println(ReflectionToString.toString(null)); Ожидаемый вывод:
Student{firstName=Анна, lastName=Иванова, group=КН-11, averageMark=4.5}
Aspirant{firstName=Иван, lastName=Петров, group=ФИ-21, averageMark=5.0, researchWork=null}
null Элемент массива объявлен как Student, но во второй строке имя класса — Aspirant: getClass() возвращает фактический тип объекта, а не тип переменной.
Затем переопределите toString() только в Student одной строкой return ReflectionToString.toString(this); и убедитесь, что System.out.println(s) в том же цикле печатает то же самое — Aspirant унаследует этот метод, и его новое поле всё равно попадёт в строку.
С чего начать
Поднимайтесь по иерархии циклом for (Class<?> c = obj.getClass(); c != Object.class; c = c.getSuperclass()), складывайте классы в список и обходите его с конца — так поля суперкласса окажутся первыми. Перед field.get(obj) вызовите field.setAccessible(true), иначе для private-поля будет IllegalAccessException; static-поля отсеивает Modifier.isStatic(field.getModifiers()). Порядок полей внутри класса спецификация getDeclaredFields() не гарантирует, но HotSpot обычно возвращает их в порядке объявления. Не вызывайте метод рекурсивно для значений полей: на объекте String он полезет во внутренние поля JDK и получит InaccessibleObjectException.
Фабрика плагинов: класс по имени из файла настроек
Приложение должно подключать обработчики строк (плагины), не зная их классов при компиляции: имена классов лежат в файле настроек. Создайте в пакете 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;
}
} Файл plugins.properties в корне проекта:
plugin.1=plugins.UpperCasePlugin
plugin.2=plugins.ReversePlugin
plugin.3=plugins.GreetingPlugin
plugin.4=plugins.Missing
plugin.5=java.lang.StringBuilder Напишите программу, которая:
- читает файл и обходит записи в порядке ключей;
- получает класс через
Class.forName()и проверяет, что он реализуетPlugin, ещё до создания объекта; - создаёт объект через
getDeclaredConstructor().newInstance(); - находит метод
executeчерезgetMethod("execute", String.class), вызывает его черезinvoke()со строкой"Hello"и печатает[ПростоеИмяКласса] результат; - на каждую проблему выводит своё сообщение и переходит к следующему плагину:
ClassNotFoundException,NoSuchMethodException(нет конструктора без параметров) и класс, не реализующийPlugin.
Ожидаемый вывод:
[UpperCasePlugin] HELLO
[ReversePlugin] olleH
Нет конструктора без параметров: plugins.GreetingPlugin
Класс не найден: plugins.Missing
Не является плагином: java.lang.StringBuilderПодсказка: файл настроек
Класс Properties разбирался в уроке Унаследованные классы (Legacy Classes) в Java: Properties props = new Properties();, затем try (Reader reader = new FileReader("plugins.properties", StandardCharsets.UTF_8)) { props.load(reader); }. Properties — наследник Hashtable, порядок ключей в нём не гарантирован, поэтому обходите new TreeSet<>(props.stringPropertyNames()). Тип проверяйте без создания объекта: Plugin.class.isAssignableFrom(c). Исключение, брошенное внутри конструктора или execute(), придёт обёрнутым в InvocationTargetException. В реальном коде после проверки объект просто приводят к Plugin и вызывают execute() напрямую — invoke() в задаче нужен для тренировки.
DTO-маппер: копирование одноимённых полей
Напишите класс FieldMapper с методом static <T> T copy(Object source, Class<T> targetClass), который создаёт объект класса targetClass и переносит в него значения полей из source:
- копируются только поля, которые есть в обоих классах с одинаковым именем и одинаковым типом;
- если имя совпадает, а тип нет, поле не копируется, а в консоль выводится
Пропущено: имя (тип -> тип); static- иfinal-поля не копируются;- остальные поля нового объекта сохраняют значения по умолчанию.
Проверьте маппер на классах:
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); Ожидаемый вывод: поле passwordHash в DTO не попадает, age пропущено из-за разных типов:
Пропущено: age (int -> String)
UserDto{id=7, login=anna, age=null, active=false}Где легко ошибиться
Типы сравнивайте через sourceField.getType() == targetField.getType(): int.class и Integer.class — разные объекты, поэтому int и Integer тоже окажутся в списке пропущенных. Поле цели по имени ищите через getDeclaredField(name): если его нет, будет NoSuchFieldException — поймайте его и пропустите поле молча (или заранее соберите Map<String, Field>). setAccessible(true) нужен на обоих полях. Чтобы не перечислять пять checked-исключений рефлексии, объявите у copy() throws ReflectiveOperationException — это их общий предок.
Мини-тест-раннер на рефлексии
Напишите программу, которая запускает тесты класса, имя которого читается с консоли. Тестом считается public, не static метод без параметров, возвращающий void, имя которого начинается с test. Остальные методы игнорируются.
- Каждый тест вызывается через
invoke()на новом экземпляре класса. - Тест прошёл (
PASSED), если метод завершился без исключения, и упал (FAILED), если бросил любое исключение, включаяAssertionError. Для упавшего теста выведите исключение, которое бросил сам тестовый метод. - Тесты выполняются в алфавитном порядке, в конце печатается итог.
Тест-класс для проверки:
public class CalculatorTest {
public void testAdd() {
check(2 + 2 == 4, "2 + 2 должно быть 4");
}
public void testSubtract() {
check(5 - 3 == 1, "5 - 3 должно быть 1");
}
public void testParse() {
Integer.parseInt("abc");
}
public void helper() { // не тест: имя не начинается с test
}
public void testWithParam(int x) { // не тест: есть параметр
}
public int testReturnsValue() { // не тест: возвращает int
return 1;
}
public static void testStatic() { // не тест: static
}
private void testPrivate() { // не тест: не public
}
private static void check(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
} Ожидаемый вывод для ввода CalculatorTest:
PASSED testAdd
FAILED testParse: java.lang.NumberFormatException: For input string: "abc"
FAILED testSubtract: java.lang.AssertionError: 5 - 3 должно быть 1
Итого: 3, passed: 1, failed: 2Как отличить упавший тест
invoke() оборачивает любое исключение тестового метода в InvocationTargetException; её getMessage() возвращает null, а исключение самого теста — e.getCause(). Отсутствие конструктора без параметров (NoSuchMethodException) — ошибка тест-класса, а не теста: сообщите о ней один раз и завершите программу. Фильтр тестов: getMethods() уже отсеивает не-public методы, остаётся проверить m.getName().startsWith("test"), m.getParameterCount() == 0, m.getReturnType() == void.class и !Modifier.isStatic(m.getModifiers()). Новый экземпляр на каждый тест нужен, чтобы поля, изменённые одним тестом, не влияли на следующий.
Документирование класса в Javadoc и генерация HTML
Класс банковского счёта задокументирован с ошибками: комментарий класса — обычный, имена в @param не совпадают с параметрами, в тексте есть знак < и дженерики, а описание withdraw() противоречит коду.
package com.example.bank;
import java.util.ArrayList;
import java.util.List;
/*
* Банковский счёт
*/
public class Account {
public static final double MIN_DEPOSIT = 10.0;
private double balance;
private final List<String> history = new ArrayList<>();
/**
* Создаёт счёт.
* @param initial начальный баланс
*/
public Account(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount < MIN_DEPOSIT) {
throw new IllegalArgumentException("Минимальная сумма: " + MIN_DEPOSIT);
}
balance += amount;
history.add("+" + amount);
}
/**
* Снимает деньги, если amount < balance.
* @param sum сумма
*/
public double withdraw(double amount) {
if (amount > balance) {
throw new IllegalStateException("Недостаточно средств");
}
balance -= amount;
history.add("-" + amount);
return balance;
}
/**
* Возвращает List<String> операций.
*/
public List<String> getHistory() {
return List.copyOf(history);
}
} - Положите класс в
src/com/example/bank/Account.javaи сгенерируйте документацию командойjavadoc -d docs -sourcepath src -subpackages com.example -author -version -encoding UTF-8. Сохраните список ошибок и предупреждений doclint. - Превратите комментарий класса в Javadoc: краткое первое предложение, теги
@author,@version,@since. - Задокументируйте константу
MIN_DEPOSITтак, чтобы её значение подставлялось в текст тегом{@value}. - Исправьте
@paramу конструктора иwithdraw(); добавьте@returnи@throwsвезде, где они нужны, включая unchecked-исключения. - Напишите комментарий для
deposit(), а в описанииgetHistory()сошлитесь наdeposit()иwithdraw()через{@link}и укажите, что возвращается неизменяемая копия. - Приведите описание
withdraw()в соответствие с кодом (снять можно и весь баланс), а условия и дженерики оформите через{@code}. - Запустите ту же команду ещё раз: в выводе не должно остаться строк
errorиwarning. Отключать проверку опцией-Xdoclint:noneнельзя. - Откройте
docs/index.htmlи найдите на страницеAccountавтора, версию, значение10.0в описании константы и разделы Parameters, Returns и Throws.
Чек-лист перед повторным запуском
Комментарий класса начинается с /**, а не с /* — иначе javadoc его просто не видит. У каждого параметра свой @param с именем ровно как в сигнатуре, у конструктора тоже. Новые JDK предупреждают и о public-элементах совсем без комментария (warning: no comment) — поэтому документируйте и deposit(), и MIN_DEPOSIT. Ссылка на метод с параметрами пишется с типами: {@link #withdraw(double)}. Приватные поля balance и history документировать не обязательно: без опции -private они в HTML не попадают. Если автор и версия на странице не видны, проверьте, что в команде есть -author и -version.
Где решения ломаются без предупреждения
Код на рефлексии компилируется почти всегда: имена методов и полей — это строки, и компилятор их не проверяет. Поэтому ошибки в таких задачах проявляются только при запуске — исключением или, хуже, неверным выводом без исключения. Таблица собирает типичные промахи из задач этой страницы.
| Ошибка | Что произойдёт | Как исправить |
|---|---|---|
getMethod("setAge", Integer.class) для метода setAge(int) | NoSuchMethodException: int.class и Integer.class — разные объекты Class | Передавайте точные типы параметров: int.class |
field.get(obj) для private-поля без подготовки | IllegalAccessException | Сначала field.setAccessible(true) |
| Копирование или вывод всех полей подряд | Попадают static-поля: общий счётчик класса выглядит как свойство объекта, а при копировании затирается | Отсеивайте Modifier.isStatic(field.getModifiers()) |
catch (InvocationTargetException e) и печать e.getMessage() | Выводится null, настоящая причина теряется | Берите исключение из метода через e.getCause() |
Вывод всех getDeclaredMethods() класса, реализующего Comparable<T> | Появляется лишний public volatile int compareTo(Object) — синтетический мост-метод компилятора | Пропускайте методы, у которых isSynthetic() возвращает true |
Class.forName("Product") для класса из пакета shop | ClassNotFoundException: нужно полное имя | Class.forName("shop.Product"); для вложенного класса — через $ |
Знак < или List<String> в тексте Javadoc | error: malformed HTML или error: unknown tag: String, сборка документации прерывается | Оборачивайте код в {@code ...} — см. ошибки в Javadoc |
Проверь свой код
Перед отправкой решения сверьтесь с рекомендациями по оформлению кода: пишите Class<?>, а не сырой Class, не глотайте исключения рефлексии пустым catch и не объявляйте throws Exception у main, если по условию ошибку нужно обработать и вывести сообщение.
Часто задаваемые вопросы
На какой версии Java решать эти задачи?
Подойдёт любая JDK начиная с 11: задачам нужны только java.lang.reflect и команда javadoc из JDK, сторонние библиотеки не требуются. Объекты создавайте через getDeclaredConstructor().newInstance() — Class.newInstance() устарел с Java 9. В Javadoc встроенный тег {@return} доступен с Java 16, {@snippet} — с Java 18. С JDK 18 исходники по умолчанию читаются в UTF-8, на более старых JDK под Windows добавляйте -encoding UTF-8.
Чем мини-тест-раннер из задачи отличается от JUnit?
Принцип тот же: найти тестовые методы через рефлексию, создать новый экземпляр класса на каждый тест и вызвать метод через Method.invoke(). Поиск по префиксу test — правило JUnit 3, где тест-класс наследовал TestCase. JUnit 4 и 5 находят тесты по аннотации @Test, сравнивают ожидаемое с фактическим через assertEquals() и строят отчёты. Условие — в задаче Мини-тест-раннер на рефлексии.
Почему поля и методы выводятся не в том порядке, что в исходнике?
Документация getDeclaredFields() и getDeclaredMethods() прямо говорит, что элементы массива не упорядочены. HotSpot обычно отдаёт поля в порядке объявления, а методы — в порядке, который может отличаться между версиями JDK. Если вывод должен совпадать с образцом, сортируйте: Arrays.sort(methods, Comparator.comparing(Method::getName)).
Можно ли через рефлексию узнать имена параметров метода?
Да, через Method.getParameters() и Parameter.getName() (Java 8+), но по умолчанию компилятор не сохраняет имена параметров в .class-файле, и вы получите arg0, arg1. Настоящие имена появятся, если компилировать с флагом javac -parameters; проверить это можно методом Parameter.isNamePresent(). Поэтому в задаче про структуру класса выводятся только типы параметров.
Комментарии