W3docs

Java Reflection: Инспекция полей

Инспекция, чтение и изменение полей на этапе выполнения в Java с помощью reflection API.

Объект Field описывает одно поле класса: его имя, тип, модификаторы и — при наличии экземпляра — его значение. Reflection позволяет перечислять поля класса, читать и записывать их, даже если они private и не имеют геттеров или сеттеров. Именно так JSON-десериализаторы заполняют объекты, а ORM-фреймворки гидратируют сущности. В этой главе рассматривается получение объектов Field, чтение и запись значений, «шлюз» setAccessible, а также особый случай final-полей.

Эта глава опирается на введение в reflection. Смежные API см. в разделах инспекция методов и инспекция конструкторов.

Получение объектов Field

Применяется то же разделение public/declared, что описано во введении:

Class<?> c = User.class;

Field f1 = c.getField("name");             // public field, incl. inherited — else NoSuchFieldException
Field f2 = c.getDeclaredField("name");     // any access level, this class only

Field[] all   = c.getFields();             // public fields, incl. inherited
Field[] mine  = c.getDeclaredFields();     // all access levels, this class only

getField/getFields видят только public-поля, но следуют по цепочке наследования. getDeclaredField/getDeclaredFields видят private/protected/package-поля тоже, но только те, что буквально объявлены на запрашиваемом классе. Чтобы собрать все поля, включая унаследованные приватные, нужно обойти getSuperclass() и объединить результаты.

Метаданные поля: имя, тип, модификаторы, generics

Объект Field отвечает на вопросы о себе без необходимости иметь какой-либо экземпляр:

Field f = User.class.getDeclaredField("age");

f.getName();           // "age"
f.getType();           // int.class           — the erased type
f.getGenericType();    // int                 — Type, keeps generic info
f.getModifiers();      // int bitset
Modifier.isPrivate(f.getModifiers());   // true/false
Modifier.isStatic(f.getModifiers());
Modifier.isFinal(f.getModifiers());
f.getDeclaringClass(); // class …User

getType() возвращает стёртый Class (List); getGenericType() возвращает Type, который для поля List<String> можно привести к ParameterizedType, чтобы восстановить String. Это работает потому, что сигнатуры generic-полей сохраняются в байткоде класса, даже несмотря на стирание типов на уровне экземпляров.

Чтение и запись значений

Для чтения или записи нужен экземпляр (или null для static-поля), и необходимо пройти проверку доступа:

User u = new User("ada", 36);
Field age = User.class.getDeclaredField("age");
age.setAccessible(true);               // bypass the access check for private

int current = age.getInt(u);           // typed getter for primitives → 36
age.setInt(u, 37);                     // typed setter
Object boxed = age.get(u);             // generic getter, autoboxes → Integer 37
age.set(u, 40);                        // generic setter, autounboxes

Для примитивных полей существуют типизированные акцессоры — getInt, getBoolean, getDouble, setLong, … — а также универсальные get(Object)/set(Object,Object) для любых полей (с боксингом примитивов). Для static-поля передайте null в качестве цели: staticField.get(null).

«Шлюз» setAccessible

По умолчанию Field применяет правила доступа Java: попытка рефлективно прочитать private-поле выбрасывает IllegalAccessException. Метод field.setAccessible(true) подавляет эту проверку для конкретного объекта Field. Именно это позволяет reflection добираться до внутренних состояний — и именно поэтому это опасно.

Два предостережения, актуальных начиная с Java 9:

  • Границы модулей. Если целевой тип находится в модуле, который не открыл свой пакет через opens, вызов setAccessible(true) выбросит InaccessibleObjectException. Библиотеки просят добавить --add-opens или чтобы модуль открыл пакет через opens.
  • Это per-object. Вызов setAccessible(true) влияет только на тот объект Field, на котором он был вызван, но не на поле глобально. Заново полученный Field для того же члена снова будет заблокирован.

Запись в final-поля

final-поля — особый и непростой случай. Для нестатического final-поля иногда всё ещё можно выполнить запись после setAccessible(true):

Field f = Config.class.getDeclaredField("name");   // private final String
f.setAccessible(true);
f.set(config, "changed");                            // may work…

Но здесь есть серьёзные оговорки:

  • Это не работает для static final-констант примитивного типа или String — они вставляются компилятором прямо в места использования, поэтому даже при изменении поля уже скомпилированные чтения этого не заметят.
  • JVM и JIT исходят из того, что final-поля никогда не меняются; их мутация является неопределённым поведением с точки зрения видимости и может быть оптимизирована.
  • Современные JDK всё чаще запрещают это вовсе.

Честное правило: не мутируйте final-поля рефлективно в production-коде. Фреймворки сериализации, которые это делают (для реконструкции неизменяемых объектов), используют низкоуровневые механизмы Unsafe/VarHandle и осознанно принимают этот риск. Пример ниже демонстрирует случай с instance-final полем, работающим для иллюстрации механизма, а не в качестве рекомендации.

Практический пример: минимальный маппер на основе полей

Программа рефлективно обходит объявленные поля объекта, чтобы построить Map<String,Object> (мини-сериализатор), затем берёт эту карту и записывает её значения обратно в свежий экземпляр (мини-десериализатор) — работая с private-полями напрямую, без единого геттера или сеттера.

java— editable, runs on the server

Что можно извлечь из этого запуска:

  • toMap сформировал снимок каждого поля экземпляра без единого геттера — getDeclaredFields() в сочетании с setAccessible(true) напрямую получил доступ к private-состоянию. Именно это — механически — делают Jackson и Gson при настройке доступа к полям. Класс не требует никакого специального API; reflection предоставляет универсальный.
  • Статическое поле count было исключено, потому что цикл проверял Modifier.isStatic. Сериализаторы стандартно пропускают static, transient и синтетические поля; битовый набор модификаторов позволяет принимать такие решения единообразно, не прописывая имена полей жёстко.
  • fromMap записал в private final-поле currency после setAccessible(true), и запись вступила в силу — демонстрируя механизм instance-final. Это сработало только потому, что currency — нестатическое final-поле, перезаписанное до того, как оптимизатор успел принять его за константу; полагаться на это в реальном коде ненадёжно, а static final-константы таким способом не изменить.
  • Чтение метаданных (bal.getType(), Modifier.toString(...), isFinal(...)) вообще не требовало экземпляра AccountField описывает объявление, которое одинаково для всех объектов класса. Для значений нужен экземпляр; для формы — нет.
  • Типизированный getInt(rebuilt) вернул примитив напрямую без боксинга, а чтение static-поля использовало cnt.get(null) — передача null в качестве цели является соглашением для статиков. Использование типизированного акцессора для примитивов позволяет избежать лишней аллокации при каждом чтении, что важно в горячих путях сериализации.

Практика

Практика
JSON-библиотека десериализует данные в класс, у которого есть поля 'private' и нет сеттеров. Используя reflection, она вызывает 'getDeclaredField(name)', а затем 'field.set(obj, value)', но на первом же private-поле получает 'IllegalAccessException'. Добавление какого одного вызова это исправит и почему он нужен?
JSON-библиотека десериализует данные в класс, у которого есть поля 'private' и нет сеттеров. Используя reflection, она вызывает 'getDeclaredField(name)', а затем 'field.set(obj, value)', но на первом же private-поле получает 'IllegalAccessException'. Добавление какого одного вызова это исправит и почему он нужен?
Was this page helpful?