Reflection & JavaDoc ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-10-31

Рефлексия в Java: класс Class и метод getClass()

Переменная объявлена как Object, а o.getClass() отвечает class java.lang.String. Это не ошибка: getClass() возвращает реальный тип объекта во время выполнения, а не тип переменной. На этом и построена рефлексия — программа узнаёт о своих классах то, чего компилятор в точке вызова не знал.

Рефлексия (Reflection API, от позднелатинского reflexio — обращение назад) — это механизм исследования программы во время её выполнения. Через рефлексию можно узнать, какие у класса поля, методы и конструкторы, создать объект по имени класса, вызвать метод и прочитать или изменить поле, даже приватное. Точка входа в Reflection API — класс java.lang.Class, а инструменты для работы с членами класса лежат в пакете java.lang.reflect.

1. Что такое рефлексия (Reflection API)

Рефлексия в Java — это возможность программы во время выполнения получить описание любого загруженного класса (имя, суперкласс, модификаторы, поля, методы, конструкторы) и работать с его членами, не зная их имён на этапе компиляции. Для этого служат класс Class и классы Field, Method, Constructor из java.lang.reflect.

В прикладном коде рефлексию пишут редко, но на ней работают инструменты, которыми вы пользуетесь каждый день:

  • JUnit находит методы с аннотацией @Test и вызывает их;
  • Spring создаёт бины и внедряет зависимости в поля и конструкторы;
  • Hibernate и Jackson читают и заполняют поля объектов при сохранении в БД и при разборе JSON;
  • IDE и отладчик показывают структуру классов.

2. Класс Class

Класс java.lang.Class описывает тип: для каждого загруженного класса, интерфейса, перечисления, массива и примитивного типа JVM создаёт ровно один объект Class. Через него можно узнать:

  • является ли тип интерфейсом, массивом или примитивом;
  • каков суперкласс и какие интерфейсы реализованы;
  • каково полное и простое имя класса, в каком он пакете, с какими модификаторами объявлен;
  • какие в нём конструкторы, поля, методы и вложенные классы.

Публичных конструкторов у Class нет: объект создаёт JVM при загрузке класса. Класс параметризован — Class<String> описывает String; если тип заранее не известен, пишут Class<?>, а не сырой Class.

3. Три способа получить объект Class: getClass(), .class, Class.forName()

Метод getClass() — это final-метод класса Object, который возвращает объект Class, описывающий реальный тип объекта во время выполнения. Если объекта нет, тот же Class можно получить литералом Имя.class (тип известен при компиляции) или методом Class.forName("полное.Имя") (имя известно только во время выполнения).

Пример 1. Три способа получить Class для String

public class ClassExample1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Object s = "Это строка";
        Class<?> c1 = s.getClass();                      // по объекту
        Class<?> c2 = String.class;                      // литерал класса
        Class<?> c3 = Class.forName("java.lang.String"); // по полному имени

        System.out.println(c1);
        System.out.println(c1 == c2 && c2 == c3);
        System.out.println(c1.getName() + " / " + c1.getSimpleName());
    }
}
class java.lang.String
true
java.lang.String / String

Все три переменные ссылаются на один и тот же объект, поэтому == возвращает true. Тип переменной s — Object, но getClass() вернул String.

Способ Когда применять Инициализирует класс (static-блоки) Что будет при ошибке
obj.getClass() Есть объект, нужен его реальный тип во время выполнения Класс уже инициализирован — объект создан NullPointerException, если obj == null; для примитивной переменной не компилируется
Имя.class Тип известен при компиляции; работает и для примитивов и массивов: int.class, int[].class Нет Неизвестный класс — ошибка компиляции
Class.forName("pkg.Имя") Имя приходит из конфигурации, плагина, строки; классическая загрузка JDBC-драйвера Да Checked-исключение ClassNotFoundException; для примитивов ("int") тоже ClassNotFoundException

4. Проверка вида типа: isArray(), isInterface(), isPrimitive()

Логические методы isArray(), isInterface() и isPrimitive() отвечают, каким видом типа является описанный объектом Class тип: массивом, интерфейсом или одним из восьми примитивов (плюс void.class). Обёртки вроде Integer примитивами не считаются.

Пример 2. Массив, интерфейс, обёртка и примитив

import java.io.Serializable;

public class ClassExample2 {
    public static void main(String[] args) {
        int[] array = new int[4];

        printInfo(array.getClass());
        printInfo(Serializable.class);
        printInfo(Integer.class);
        printInfo(double.class);
    }

    private static void printInfo(Class<?> c) {
        System.out.println(c.getName() + " (" + c.getSimpleName() + ")");
        System.out.println("  isArray: " + c.isArray()
                + ", isInterface: " + c.isInterface()
                + ", isPrimitive: " + c.isPrimitive());
    }
}
[I (int[])
  isArray: true, isInterface: false, isPrimitive: false
java.io.Serializable (Serializable)
  isArray: false, isInterface: true, isPrimitive: false
java.lang.Integer (Integer)
  isArray: false, isInterface: false, isPrimitive: false
double (double)
  isArray: false, isInterface: false, isPrimitive: true

Обратите внимание на [I: так getName() записывает массив int[] (внутреннее имя JVM). Читаемое имя даёт getSimpleName() или getTypeName().

5. Суперкласс, пакет и модификаторы

Методы getSuperclass(), getPackageName() и getModifiers() возвращают родительский класс, имя пакета и модификаторы объявления. Модификаторы приходят битовой маской int, поэтому для чтения их передают в java.lang.reflect.Modifier.toString().

import java.lang.reflect.Modifier;

public class ClassInfoExample {
    public static void main(String[] args) {
        Class<?> c = Integer.class;
        System.out.println(c.getSuperclass());                    // class java.lang.Number
        System.out.println(c.getPackageName());                   // java.lang
        System.out.println(Modifier.toString(c.getModifiers()));  // public final

        System.out.println(Object.class.getSuperclass());         // null
        System.out.println(Runnable.class.getSuperclass());       // null
        System.out.println(int[].class.getSuperclass());          // class java.lang.Object
    }
}
Метод Что возвращает Граничный случай
getName() Полное (бинарное) имя: java.lang.String Для массива — [I, [Ljava.lang.String;; для вложенного класса — java.util.Map$Entry
getSimpleName() Имя без пакета: String, int[] Для анонимного класса — пустая строка
getSuperclass() Объект Class родительского класса null для Object, интерфейсов, примитивов и void
getInterfaces() Массив интерфейсов, указанных в implements (или extends у интерфейса) Интерфейсы суперклассов не входят; если интерфейсов нет — пустой массив
getPackageName() (Java 9+), getPackage() Имя пакета строкой / объект Package Для класса в пакете по умолчанию getPackageName() вернёт пустую строку
getModifiers() Битовая маска модификаторов (int) Читать через Modifier.toString() или Modifier.isFinal() и т.п.
isArray(), isInterface(), isPrimitive() true/false по виду типа isPrimitive() для Integer.class — false, для void.class — true

6. Конструкторы, методы и поля: getDeclared*() и get*()

У Class две группы методов для получения членов класса. getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors() возвращают всё, что объявлено в самом классе, включая private, но без унаследованного. getFields(), getMethods(), getConstructors() возвращают только public-члены, зато вместе с унаследованными.

Члены класса Все объявленные в классе (любой доступ) Только public, включая унаследованные
Поля getDeclaredFields() getFields()
Методы getDeclaredMethods() getMethods() — вместе с public-методами Object
Конструкторы getDeclaredConstructors() getConstructors() — только свои: конструкторы не наследуются
Вложенные классы getDeclaredClasses() getClasses()

Для одного члена по имени есть версии в единственном числе: getDeclaredField("name"), getMethod("name", Class<?>... параметры) и т.д. Если члена нет, они бросают NoSuchFieldException или NoSuchMethodException.

Пример 3. Чем getDeclared*() отличается от get*()

import java.lang.reflect.Member;
import java.util.Arrays;

class Box {
    public double width;
    protected double height;
    double depth;

    public Box() {
    }

    public double volume() {
        return width * height * depth;
    }
}

class HeavyBox extends Box {
    public double weight;
    private String label;

    public HeavyBox() {
    }

    private HeavyBox(double weight) {
        this.weight = weight;
    }

    public double getWeight() {
        return weight;
    }

    private void secret() {
    }
}

public class ClassExample3 {
    public static void main(String[] args) {
        Class<HeavyBox> c = HeavyBox.class;

        System.out.println("Declared constructors: " + c.getDeclaredConstructors().length);
        System.out.println("Public constructors: " + c.getConstructors().length);

        System.out.println("Declared methods: " + names(c.getDeclaredMethods()));
        System.out.println("Public methods: " + c.getMethods().length);

        System.out.println("Declared fields: " + names(c.getDeclaredFields()));
        System.out.println("Public fields: " + names(c.getFields()));
    }

    // Порядок элементов в массивах не гарантирован, поэтому сортируем имена
    private static String names(Member[] members) {
        return Arrays.toString(Arrays.stream(members).map(Member::getName).sorted().toArray());
    }
}
Declared constructors: 2
Public constructors: 1
Declared methods: [getWeight, secret]
Public methods: 11
Declared fields: [label, weight]
Public fields: [weight, width]

Разбор: getDeclaredMethods() видит приватный secret(), но не видит унаследованный volume(). getMethods() возвращает 11 методов: getWeight(), volume() из Box и 9 public-методов Object (equals, hashCode, toString, getClass, notify, notifyAll и три wait). В getFields() попало унаследованное width, а protected height и package-private depth — нет.

7. Вызов методов и доступ к полям через рефлексию

Объекты Constructor, Method и Field умеют не только описывать члены класса, но и работать с ними: newInstance() создаёт объект, invoke() вызывает метод, get()/set() читают и записывают поле. Доступ к private-членам открывает setAccessible(true).

Пример 4. Создать объект по имени класса, вызвать метод, изменить приватное поле (использует классы Box и HeavyBox из примера 3 — положите файлы в один каталог)

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionInvokeExample {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("HeavyBox");             // класс в пакете по умолчанию
        Object box = c.getDeclaredConstructor().newInstance();

        Field weight = c.getField("weight");
        weight.setDouble(box, 12.5);

        Method getWeight = c.getMethod("getWeight");
        System.out.println(getWeight.invoke(box));          // 12.5

        Field label = c.getDeclaredField("label");          // private-поле
        label.setAccessible(true);
        label.set(box, "Хрупкое");
        System.out.println(label.get(box));                 // Хрупкое

        c.getMethod("secret");                              // secret() - private
    }
}
12.5
Хрупкое
Exception in thread "main" java.lang.NoSuchMethodException: HeavyBox.secret()

Последняя строка падает, потому что getMethod() ищет только public-методы. Приватный метод находится через getDeclaredMethod("secret"), а вызвать его можно после setAccessible(true).

Устаревший способ

В старых примерах объект создают через c.newInstance(). С Java 9 этот метод помечен @Deprecated: он пробрасывает checked-исключения конструктора в обход компилятора. Используйте c.getDeclaredConstructor().newInstance() — исключения конструктора тогда приходят обёрнутыми в InvocationTargetException.

Модули и setAccessible()

Для своих классов setAccessible(true) работает без ограничений. Для внутренних полей классов JDK (например, value у String) с Java 16–17 он бросает InaccessibleObjectException: пакеты модуля java.base закрыты для глубокой рефлексии. Открыть их можно только явно, флагом JVM --add-opens.

8. Неочевидные моменты

Чаще всего с объектом Class ошибаются в трёх местах: путают getClass() с instanceof, ожидают, что дженерики различаются во время выполнения, и не учитывают, что Class.forName() запускает static-блоки класса.

getClass() сравнивает тип точно, instanceof учитывает наследников. Дженерики стираются при компиляции, поэтому ArrayList<String> и ArrayList<Integer> описывает один и тот же объект Class:

Box box = new HeavyBox();
System.out.println(box instanceof Box);            // true
System.out.println(box.getClass() == Box.class);   // false: реальный тип - HeavyBox

System.out.println(new ArrayList<String>().getClass()
        == new ArrayList<Integer>().getClass());    // true

Object o = null;
o.getClass();                                      // NullPointerException

Вложенный класс в Class.forName() пишется через $. Class.forName("java.util.Map.Entry") бросит ClassNotFoundException; правильное бинарное имя — "java.util.Map$Entry". Литерал Map.Entry.class при этом пишется через точку.

Что выведет этот код?

class Config {
    static {
        System.out.println("Config загружен");
    }
}

public class InitDemo {
    public static void main(String[] args) throws Exception {
        Class<?> a = Config.class;
        System.out.println("после .class");
        Class<?> b = Class.forName("Config");
        System.out.println(a == b);
    }
}
после .class
Config загружен
true

Литерал Config.class не инициализирует класс, поэтому static-блок не срабатывает. Class.forName(String) инициализирует класс — именно поэтому раньше драйвер JDBC подключали строкой Class.forName("com.mysql.cj.jdbc.Driver"): драйвер регистрировал себя в static-блоке. Если инициализация не нужна, есть перегрузка Class.forName(name, false, classLoader).

Часто задаваемые вопросы

Что использовать в equals(): getClass() или instanceof?

Оба варианта допустимы, но дают разную семантику. getClass() != o.getClass() считает объект наследника неравным объекту родителя и гарантирует симметричность equals() при наследовании. instanceof разрешает сравнивать с подклассами — это удобно, если подклассы не добавляют полей, участвующих в сравнении, и класс помечен final. Разница между проверками показана в разделе Неочевидные моменты.

Почему рефлексию не советуют использовать в обычном коде?

Ошибки, которые компилятор поймал бы сразу (опечатка в имени метода, неверный тип аргумента), при рефлексии всплывают только во время выполнения в виде NoSuchMethodException или IllegalArgumentException. Вызов через Method.invoke() медленнее прямого, а setAccessible(true) обходит инкапсуляцию. Рефлексия оправдана во фреймворках, библиотеках сериализации, тестах и инструментах, где типы заранее неизвестны.

Можно ли через рефлексию изменить private или final поле?

Private-поле своего класса — да, после setAccessible(true). Final-поле экземпляра обычного класса тоже можно перезаписать через Field.set(), но не у записей (record) и не у static final полей — там будет IllegalAccessException. Константы времени компиляции (static final int MAX = 10) компилятор подставляет в код, поэтому даже удачная запись не изменит уже скомпилированные обращения. В новых версиях JDK изменение final-полей через рефлексию постепенно ограничивают (JEP 500).

Зачем писать Class<?>, а не просто Class?

Сырой тип Class отключает проверку дженериков и даёт предупреждение компилятора raw type. Class<?> означает «объект Class неизвестного типа» и безопасен, а Class<HeavyBox> позволяет получить типизированный результат: HeavyBox.class.getDeclaredConstructor().newInstance() вернёт HeavyBox без приведения. Сам getClass() объявлен с типом Class<? extends X>, где X — тип переменной.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.