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 onlygetField/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 …UsergetType() возвращает стёртый 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-полями напрямую, без единого геттера или сеттера.
Что можно извлечь из этого запуска:
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(...)) вообще не требовало экземпляраAccount—Fieldописывает объявление, которое одинаково для всех объектов класса. Для значений нужен экземпляр; для формы — нет. - Типизированный
getInt(rebuilt)вернул примитив напрямую без боксинга, а чтениеstatic-поля использовалоcnt.get(null)— передачаnullв качестве цели является соглашением для статиков. Использование типизированного акцессора для примитивов позволяет избежать лишней аллокации при каждом чтении, что важно в горячих путях сериализации.