☕ Вопросы интервью по Java

Вопросы 211-220 - Reflection API

Вопрос 211
Что такое Reflection API и для чего он нужен?

Ответ: Reflection API (пакет java.lang.reflect и класс java.lang.Class) позволяет программе во время выполнения исследовать и изменять собственную структуру: узнавать классы, поля, методы, конструкторы, аннотации, создавать объекты и вызывать методы, имена которых неизвестны на этапе компиляции.

Основные классы:
  • Class — описание класса или интерфейса
  • Field — поле
  • Method — метод
  • Constructor — конструктор
  • Modifier — модификаторы доступа (public, static, final...)
  • Proxy — динамические прокси
Где применяется:
  • Фреймворки: Spring (внедрение зависимостей), Hibernate (маппинг полей), Jackson (сериализация), JUnit (поиск тестов)
  • IDE и отладчики — автодополнение, инспекторы объектов
  • Плагины и загрузка классов по имени из конфигурации (Class.forName)
💡 Важно: в обычном прикладном коде рефлексия нужна редко. Это инструмент для библиотек и фреймворков.
Вопрос 212
Как получить объект Class?

Ответ: Есть три основных способа:

Способ Когда использовать
String.class Тип известен на этапе компиляции. Работает и для примитивов: int.class
obj.getClass() Есть экземпляр; вернёт фактический (рантайм) класс объекта
Class.forName("java.lang.String") Имя класса известно только как строка. Выбрасывает ClassNotFoundException
Пример:

Class<String> c1 = String.class;

Object obj = "hello";
Class<?> c2 = obj.getClass();           // class java.lang.String

Class<?> c3 = Class.forName("java.util.ArrayList");

System.out.println(c1 == c2);          // true: Class объект один на класс в загрузчике
                
💡 Важно: Class.forName() инициализирует класс (выполняются статические блоки), а X.class — нет.
Вопрос 213
Как получить поля, методы и конструкторы класса? В чём разница getMethods() и getDeclaredMethods()?

Ответ: У Class есть две группы методов — с префиксом getDeclared и без него.

Метод Что возвращает
getMethods(), getFields() Только public члены, включая унаследованные
getDeclaredMethods(), getDeclaredFields() Все члены, объявленные в этом классе (любой уровень доступа), без унаследованных
getConstructors() Только public конструкторы
getDeclaredConstructors() Все конструкторы класса
Пример:

class User {
    private String name;
    public int age;
    private void secret() { }
    public String getName() { return name; }
}

Class<User> c = User.class;

for (Field f : c.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType().getSimpleName()
        + " : " + Modifier.toString(f.getModifiers()));
}
// name : String : private
// age : int : public

c.getDeclaredMethods();   // secret, getName
c.getMethods();           // getName + публичные методы Object (toString, hashCode...)
                
💡 Важно: порядок элементов в возвращаемых массивах не гарантирован спецификацией.
Вопрос 214
Как создать объект через рефлексию?

Ответ: Нужно получить Constructor по типам параметров и вызвать newInstance().


class Person {
    private final String name;
    public Person() { this("unknown"); }
    public Person(String name) { this.name = name; }
}

Class<Person> c = Person.class;

Person p1 = c.getDeclaredConstructor().newInstance();                 // пустой конструктор
Person p2 = c.getDeclaredConstructor(String.class).newInstance("Ann"); // с параметром
                
Важные детали:
  • Class.newInstance() объявлен устаревшим с Java 9 — он пробрасывает любые исключения конструктора, в том числе проверяемые, не объявляя их
  • Исключение внутри конструктора приходит обёрнутым в InvocationTargetException
  • Для private конструктора нужен setAccessible(true) (так ломают паттерн Singleton)
  • Если нужного конструктора нет — NoSuchMethodException
Вопрос 215
Как вызвать метод через рефлексию?

Ответ: Находим Method по имени и типам параметров и вызываем invoke(объект, аргументы...).


class Calc {
    public int sum(int a, int b) { return a + b; }
    public static int twice(int x) { return x * 2; }
    private String secret() { return "hidden"; }
}

Calc calc = new Calc();

Method sum = Calc.class.getMethod("sum", int.class, int.class);
Object r = sum.invoke(calc, 2, 3);                       // 5 (Integer)

Method twice = Calc.class.getMethod("twice", int.class);
Object r2 = twice.invoke(null, 4);                       // static: объект = null

Method secret = Calc.class.getDeclaredMethod("secret");
secret.setAccessible(true);                              // доступ к private
Object r3 = secret.invoke(calc);                         // "hidden"
                
Исключения:
  • NoSuchMethodException — метод с такой сигнатурой не найден
  • IllegalAccessException — нет доступа (private без setAccessible)
  • InvocationTargetException — вызванный метод сам бросил исключение; оригинал доступен через getCause()
💡 Важно: примитивы нужно указывать как int.class, а не Integer.class — иначе метод не найдётся. Результат invoke всегда Object (с автоупаковкой).
Вопрос 216
Как прочитать или изменить поле, в том числе private?

Ответ: Через Field: методы get()/set() и типизированные getInt(), setInt() и т.д. Для private полей сначала вызывают setAccessible(true).


class Account {
    private int balance = 100;
}

Account acc = new Account();

Field f = Account.class.getDeclaredField("balance");
f.setAccessible(true);

int value = (int) f.get(acc);     // 100
f.set(acc, 500);                  // теперь balance == 500
                
Что нужно знать:
  • getField() видит только public поля, getDeclaredField() — любые поля этого класса
  • Поле суперкласса через getDeclaredField подкласса не найти — нужно подняться по getSuperclass()
  • Для static поля в get/set первым аргументом передают null
  • Не-static final поле после setAccessible(true) изменить можно, но JIT мог уже подставить константу, поэтому результат непредсказуем. У static final, а также у полей record и hidden-классов изменение вызовет исключение
Вопрос 217
Что делает setAccessible(true) и как на него влияют модули Java 9+?

Ответ: setAccessible(true) отключает проверку модификаторов доступа для конкретного Field, Method или Constructor, поэтому можно обращаться к private членам. Проверка идёт при каждом вызове — отключить её для всего класса нельзя.

Влияние модульной системы (JPMS):
  • Начиная с Java 9 доступ через рефлексию ограничен границами модулей
  • В Java 16 сильная инкапсуляция внутренних пакетов JDK стала включена по умолчанию, а в Java 17 флаг --illegal-access удалён
  • Попытка обратиться к private члену, например java.base, закончится InaccessibleObjectException
  • Пакет в своём модуле можно открыть через opens в module-info.java или флагом запуска --add-opens java.base/java.lang=ALL-UNNAMED

Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
// Java 17+: InaccessibleObjectException:
// Unable to make field private final byte[] java.lang.String.value accessible:
// module java.base does not "opens java.lang" to unnamed module
                
💡 Важно: классы вашего приложения на classpath находятся в unnamed module и открыты для рефлексии. Проблемы возникают при доступе к внутренностям JDK и к библиотекам, подключённым как именованные модули.
Вопрос 218
Каковы недостатки и риски рефлексии?

Ответ:

Недостаток Пояснение
Производительность Поиск члена, проверки доступа и упаковка аргументов дают накладные расходы; JIT хуже оптимизирует такие вызовы. Заметно в горячем коде
Нет проверки при компиляции Имена методов и полей — строки; опечатка или рефакторинг (переименование) приведёт к ошибке только в runtime
Нарушение инкапсуляции Доступ к private членам ломает инварианты класса
Безопасность Требуется разрешение ReflectPermission при включённом SecurityManager (он объявлен устаревшим с Java 17)
Сложность отладки Обёрнутые исключения (InvocationTargetException), неочевидные стек-трейсы
GraalVM Native Image Рефлексивные обращения нужно заранее описывать в конфигурации, так как анализ идёт на этапе сборки
Как снизить издержки:
  • Кэшировать найденные Method/Field и не искать их заново при каждом вызове
  • Использовать MethodHandle / VarHandle (java.lang.invoke) — они быстрее при многократном использовании
  • Применять генерацию кода на этапе компиляции (Annotation Processing, как у MapStruct), где это возможно
Вопрос 219
Что такое динамические прокси (java.lang.reflect.Proxy)?

Ответ: Динамический прокси — объект, который создаётся во время выполнения и реализует заданные интерфейсы. Все вызовы его методов перенаправляются в один метод InvocationHandler.invoke(), где можно добавить логику до и после вызова реального объекта.


interface Service {
    String hello(String name);
}

Service real = name -> "Hello, " + name;

Service proxy = (Service) Proxy.newProxyInstance(
    Service.class.getClassLoader(),
    new Class<?>[] { Service.class },
    (p, method, args) -> {
        System.out.println("before " + method.getName());
        Object result = method.invoke(real, args);
        System.out.println("after");
        return result;
    }
);

proxy.hello("Ann");
// before hello
// after
                
Ограничения и альтернативы:
  • JDK Proxy работает только с интерфейсами
  • Для прокси классов используют библиотеки CGLIB, ByteBuddy (генерируют подкласс); final классы и методы так проксировать нельзя
  • Spring AOP использует JDK Proxy, если у бина есть интерфейс, и CGLIB — если нет (@Transactional, @Cacheable, @Async)
💡 Важно: из-за прокси вызов метода через this внутри того же бина обходит прокси — поэтому @Transactional не срабатывает при самовызове.
Вопрос 220
Как фреймворки используют рефлексию?

Ответ: Рефлексия — основа большинства «магии» в Java-фреймворках.

Фреймворк Что делает через рефлексию
Spring Сканирует классы с @Component, создаёт бины через конструкторы, внедряет зависимости в поля и конструкторы (@Autowired), создаёт прокси
Hibernate / JPA Читает @Entity, @Column, заполняет поля сущностей при загрузке из БД, создаёт прокси для lazy-загрузки
Jackson Находит геттеры, сеттеры и поля для сериализации JSON в объект и обратно
JUnit Находит методы с @Test, @BeforeEach и вызывает их
Пример: упрощённое внедрение зависимостей

@Retention(RetentionPolicy.RUNTIME)
@interface Inject { }

class Controller {
    @Inject
    private Service service;
}

static void injectAll(Object target, Map<Class<?>, Object> beans) throws Exception {
    for (Field f : target.getClass().getDeclaredFields()) {
        if (f.isAnnotationPresent(Inject.class)) {
            f.setAccessible(true);
            f.set(target, beans.get(f.getType()));
        }
    }
}
                
💡 Тенденция: чтобы ускорить старт и уменьшить расход памяти, современные фреймворки (Quarkus, Micronaut, Spring AOT) переносят часть работы на этап сборки и меньше полагаются на рефлексию во время выполнения.