Что такое Reflection и как его использовать?
Reflection, рефлексия – это средства манипуляции данными на основе знания о структуре классов этих данных, инструменты метапрограммирования.
Экземпляр класса Class можно получить тремя способами:
Литералом .class ;
Статическим фабричным методом Class.forName() ;
Методом getClass() экземпляров класса.
Использование Reflection API медленное и небезопасное. Оно позволяет ломать инвариантность состояний экземпляра, нарушать инкапсуляцию, и даже менять финальные поля.
Использовать рефлексию естественно в тестовом коде, в инструментах разработки, в фреймворках (особенно в связке с runtime-аннотациями). Рефлекшн в ординарном бизнес-коде обычно говорит о больших проблемах проектирования.
Нередко на интервью просят продемонстрировать пример использования рефлекшна. Один из самых близких для backend-разработчика примеров – инициализация классов-конфигураций в Spring Framework. Фреймворк с помощью рефлекшна сканирует внутренности таких классов. Поля и методы, помеченные специальными аннотациями, воспринимаются как объявления элементов экосистемы фреймворка.
Reflection API. Рефлексия. Темная сторона Java
Рефлексия в Java осуществляется с помощью Java Reflection API. Что такое эта рефлексия? Существует короткое и точное, а также популярное на просторах интернета определение. Рефлексия (от позднелат. reflexio — обращение назад) — это механизм исследования данных о программе во время её выполнения. Рефлексия позволяет исследовать информацию о полях, методах и конструкторах классов. Сам же механизм рефлексии позволяет обрабатывать типы, отсутствующие при компиляции, но появившиеся во время выполнения программы. Рефлексия и наличие логически целостной модели выдачи информации об ошибках дает возможность создавать корректный динамический код. Иначе говоря, понимание принципов работы рефлексии в java открывает перед вами ряд удивительных возможностей. Вы буквально можете жонглировать классами и их составляющими.
- Узнать/определить класс объекта;
- Получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах;
- Выяснить, какие методы принадлежат реализуемому интерфейсу/интерфейсам;
- Создать экземпляр класса, причем имя класса неизвестно до момента выполнения программы;
- Получить и установить значение поля объекта по имени;
- Вызвать метод объекта по имени.
public class MyClass < private int number; private String name = "default"; // public MyClass(int number, String name) < // this.number = number; // this.name = name; // >public int getNumber() < return number; >public void setNumber(int number) < this.number = number; >public void setName(String name) < this.name = name; >private void printData() < System.out.println(number + name); >>
Как мы видим, это самый обычный класс. Конструктор с параметрами закомментирован не просто так, мы к этому еще вернемся. Если вы внимательно просмотрели содержимое класса, то наверняка увидели отсутствие getter ’a для поля name . Само поле name помечено модификатором доступа private , обратиться к нему вне самого класса у нас не выйдет => мы не можем получить его значение. “Так в чем проблема? — скажете вы. — Допиши getter или измени модификатор доступа”. И вы будете правы, но, что если MyClass находится в скомпилированной aar библиотеке или в другом закрытом модуле без доступа к редактированию, а на практике такое случается крайне часто. И какой-то невнимательный программист просто забыл написать getter . Самое время вспомнить о рефлексии! Попробуем добраться до private поля name класса MyClass :
public static void main(String[] args) < MyClass myClass = new MyClass(); int number = myClass.getNumber(); String name = null; //no getter =( System.out.println(number + name);//output 0null try < Field field = myClass.getClass().getDeclaredField("name"); field.setAccessible(true); name = (String) field.get(myClass); >catch (NoSuchFieldException | IllegalAccessException e) < e.printStackTrace(); >System.out.println(number + name);//output 0default >
Разберем что тут сейчас произошло. В java есть замечательный класс Class . Он представляет классы и интерфейсы в исполняемом приложении Java. Связь между Class и ClassLoader мы затрагивать не будем, т.к. это не есть тема статьи. Далее, чтобы получить поля этого класса нужно вызвать метод getFields() , этот метод вернет нам все доступные поля класса. Нам это не подходит, так как наше поле private , поэтому используем метод getDeclaredFields() , этот метод также возвращает массив полей класса, но теперь и private и protected . В нашей ситуации мы знаем имя поля, которое нас интересует, и можем использовать метод getDeclaredField(String) , где String — имя нужного поля. Примечание: getFields() и getDeclaredFields() не возвращают поля класса-родителя! Отлично, мы получили объект Field с ссылкой на наш name . Т.к. поле не было публичным (public) следует дать доступ для работы с ним. Метод setAccessible(true) разрешает нам дальнейшую работу. Теперь поле name полностью под нашим контролем! Получить его значение можно вызовом get(Object) у объекта Field , где Object — экземпляр нашего класса MyClass . Приводим к типу String и присваиваем нашей переменной name . На тот случай если у нас вдруг не оказалось setter ’a, для установки нового значения полю name можно использовать метод set :
field.set(myClass, (String) "new value");
Поздравляю! Вы только что овладели базовым механизмом рефлексии и смогли получить доступ к private полю! Обратите внимание на блок try/catch и типы обрабатываемых исключений. IDE сама укажет на их обязательное присутствие, но по их названию итак ясно зачем они здесь. Идем дальше! Как вы могли заметить, наш MyClass уже имеет метод для вывода информации о данных класса:
private void printData()
Но этот программист и тут наследил. Метод находится под модификатором доступа private , и нам пришлось самим каждый раз писать код вывода. Не порядок, где там наша рефлексия?… Напишем вот такую функцию:
public static void printData(Object myClass) < try < Method method = myClass.getClass().getDeclaredMethod("printData"); method.setAccessible(true); method.invoke(myClass); >catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) < e.printStackTrace(); >>
Здесь примерно такая же процедура как и с получением поля — получаем нужный метод по имени и даем доступ к нему. И для вызова объекта Method используем invoke(Оbject, Args) , где Оbject — все также экземпляр класса MyClass . Args — аргументы метода — наш таковых не имеет. Теперь для вывода информации мы используем функцию printData :
public static void main(String[] args) < MyClass myClass = new MyClass(); int number = myClass.getNumber(); String name = null; //? printData(myClass); // outout 0default try < Field field = myClass.getClass().getDeclaredField("name"); field.setAccessible(true); field.set(myClass, (String) "new value"); name = (String) field.get(myClass); >catch (NoSuchFieldException | IllegalAccessException e) < e.printStackTrace(); >printData(myClass);// output 0new value >
Ура, теперь у нас есть доступ к приватному методу класса. Но что делать если у метода все таки будут аргументы, и зачем тот закомментированный конструктор? Всему свое время. Из определения в начале ясно, что рефлексия позволяет создавать экземпляры класса в режиме runtime (во время выполнения программы)! Мы можем создать объект класса по полному имени этого класса. Полное имя класса — это имя класса, учитывая путь к нему в package .
В моей иерархии package полным именем MyClass будет “ reflection.MyClass ”. Также узнать имя класса можно простым способом (вернет имя класса в виде строки):
MyClass.class.getName()
Создадим экземпляр класса с помощью рефлексии:
public static void main(String[] args) < MyClass myClass = null; try < Class clazz = Class.forName(MyClass.class.getName()); myClass = (MyClass) clazz.newInstance(); >catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) < e.printStackTrace(); >System.out.println(myClass);//output created object reflection.MyClass@60e53b93 >
На момент старта java приложения далеко не все классы оказываются загруженными в JVM. Если в вашем коде нет обращения к классу MyClass , то тот, кто отвечает за загрузку классов в JVM, а им является ClassLoader , никогда его туда и не загрузит. Поэтому нужно заставить ClassLoader загрузить его и получить описание нашего класса в виде переменной типа Class . Для этой задачи существует метод forName(String) , где String — имя класса, описание которого нам требуется. Получив Сlass , вызов метода newInstance() вернет Object , который будет создан по тому самому описанию. Остается привести этот объект к нашему классу MyClass . Круто! Было сложно, но, надеюсь, понятно. Теперь мы умеем создавать экземпляр класса буквально из одной строки! К сожалению описанный способ будет работать только с конструктором по умолчанию (без параметров). Как же вызывать методы с аргументами и конструкторы с параметрами? Самое время раскомментировать наш конструктор. Как и ожидалось, newInstance() не находит конструктор по умолчанию и больше не работает. Перепишем создание экземпляра класса:
public static void main(String[] args) < MyClass myClass = null; try < Class clazz = Class.forName(MyClass.class.getName()); Class[] params = ; myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2"); > catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) < e.printStackTrace(); >System.out.println(myClass);//output created object reflection.MyClass@60e53b93 >
Для получения конструкторов класса следует у описания класса вызвать метод getConstructors() , а для получения параметров конструктора — getParameterTypes() :
Constructor[] constructors = clazz.getConstructors(); for (Constructor constructor : constructors) < Class[] paramTypes = constructor.getParameterTypes(); for (Class paramType : paramTypes) < System.out.print(paramType.getName() + " "); >System.out.println(); >
Таким образом получаем все конструкторы и все параметры к ним. В моем примере идет обращение к конкретному конструктору с конкретными уже известными параметрами. И для вызова этого конструктора используем метод newInstance , в котором указываем значения этим параметрам. Точно так же будет и с invoke для вызова методов. Возникает вопрос: где может пригодится рефлексивный вызов конструкторов? Современные технологии java, как уже говорилось в начале, не обходятся без Reflection API. Например, DI (Dependency Injection), где аннотации в сочетании с рефлексией методов и конструкторов образуют популярную в Android разработке библиотеку Dagger. После прочтения этой статьи вы с уверенностью можете считать себя просвещенным в механизмы Reflection API. Темной стороной java рефлексия называется не зря. Она напрочь ломает парадигму ООП. В java инкапсуляция служит для сокрытия и ограничения доступа одних компонентов программы к другим. Используя модификатор private мы подразумеваем, что доступ к этому полю будет только в пределах класса, где это поле существует, основываясь на этом мы строим дальнейшую архитектуру программы. В этой статье мы увидели, как с помощью рефлексии можно пробираться куда угодно. Хорошим примером в виде архитектурного решения является порождающий шаблон проектирования — Singleton . Основная его идея в том, чтобы на протяжении всей работы программы класс, реализующий этот шаблон был только в одном экземпляре. Осуществляется это при помощи установки конструктору по умолчанию private модификатор доступа. И будет очень нехорошо, если какой-то программист со своей рефлексией будет плодить такие классы. Кстати, есть очень интересный вопрос, который я недавно услышал от своего сотрудника: может ли быть у класса, реализующий шаблон Singleton , наследники? Неужели в этом случае бессильна даже рефлексия? Пишите ваши feedback’и по статье и ответ в коментарии, а также задавайте свои вопросы! Истинная Сила Reflection API раскрывается в комбинации c Runtime Annotations, о чем мы, возможно, поговорим в следующей статье про темную сторону Java. Спасибо за внимание!
Java Reflection API: методы и примеры использования
Рассказываем, что рефлексия Java и для чего она используется.
Артем Григор
Автор статьи
11 ноября 2022 в 15:01
Рассмотрим, что такое рефлексия в Java, какие возможности предоставляет API и как ее можно использовать в проектах. В статье приведем несколько примеров кода, которые можно запустить и посмотреть на результат, разберем схемы работы и особенности использования. Код проверенно работает с Java 11. Чтобы разобраться в методах рефлексии, потребуется практический опыт работы с Java.
Приобрести его можно на курсе Skypro «Java-разработчик». Программу составляли опытные разработчики, поэтому практических заданий там много, а теории только необходимый минимум. Результаты практических заданий можно использовать в качестве портфолио, чтобы быстрее найти работу по новой специальности.
Что такое Java Reflection API
Java Reflection — это особенный функционал, который позволяет программе получить доступ к приватным частям объектов или поменять поведение некоторых методов классов. Созданный таким образом код будет адаптироваться к входным данным и, например, не будет зависеть от типов, с которыми работает.
Это дает возможность писать код, который со временем будет эволюционировать, то есть не зависеть от текущих имплементаций методов или переменных. Главные преимущества рефлексии — свобода и адаптивность. При необходимости вызвать приватный метод класса можно не переписывать его, а вызвать через Java Reflection. Фактически рефлексия позволяет не следовать написанному коду, вводя новые правила. Можно пойти чуть дальше и начать перехватывать вызовы методов, подменяя их другой логикой, или создать программу, которая будет работать с еще не написанным классом.
Для чего используется рефлексия
Примеров, когда рефлексия становится полезной в проектах, множество. Рассмотрим несколько вариантов ее использования:
- При тестировании кода. Часто бывает нужно проверить корректность работы приватной функции, однако в тесте ее вызвать не получается именно из-за того, что она приватная. Вариантов решения задачи два — сделать ее на время публичной, а потом обратно приватной, или просто вызвать ее в тесте через рефлексию. Второй вариант намного проще и быстрее.
- При написании фреймворков и библиотек. В популярном Spring Framework рефлексия используется для создания бинов. Во время работы программы Spring Framework собирает данные о классах, помеченных аннотацией `@Component`, и создает для них экземпляры. Это позволяет создавать бины без явного указания их в конфигурационном файле.
- Для поиска и запуска тестов. Например, так применяет рефлексию библиотека JUnit. Опытные пользователи замечали, что тесты помечены аннотацией `@Test`. Это сделано как раз для того, чтобы во время работы JUnit прошелся по всем классам и запустил всё с этой аннотацией.
- Для сериализации и десериализации объектов. Например, библиотека Jackson использует рефлексию для сериализации и десериализации объектов в стандарте JSON. Без нее Jackson не смог бы прочитать значения приватных полей и корректно сохранить их в JSON-формате. То же касается и десериализации, когда Jackson должен восстановить значения всех полей, в том числе и приватных, — это было бы невозможно без рефлексии.
Больше узнать об этих библиотеках и научиться программировать поможет курс по Java онлайн-университета Skypro. Он включает 440 часов теории и практики, видеоуроки и вебинары с преподавателями — экспертами в области Java-разработки, а также мастер-классы с реальными рабочими задачами и поддержку наставников. После окончания курса студенты получают диплом о профессиональной переподготовке.
Java-разработчик: новая работа через 11 месяцев
Получится, даже если у вас нет опыта в IT
Особенности Java Reflection
Нужно понимать, что Java Reflection API — это часть языка, а не библиотеки. Это означает, что использовать рефлексию можно над любым классом, написанными на Java. Для этого достаточно импортировать пакет `java.lang.reflect` в свой код.
Стоит отметить, что рефлексия в Java является довольно медленной, поэтому ее стоит использовать при отсутствии других вариантов. Причина — большинство операций не определены до выполнения программы, что мешает оптимизации кода в ходе компиляции. Рефлексия использует динамическую загрузку классов, что также требует еще больше ресурсов.
Рефлексия в Java не поддерживается с примитивными типами данных, такими как, например, int. Чтобы использовать рефлексию с ними, придется создать классы-обертки вокруг них.
История рефлексии в Java
Само понятие рефлексии в Java было введено почти с самого начала существования этого языка программирования, в версии 1.1. До этого можно было работать только предопределенными классами.
В Java 1.1 появился класс `Class`. Именно он позволяет получить данные о классе, к которому принадлежит объект. Таким образом стало возможным узнать, какие методы существуют у класса, и вызвать их. Для этого не нужно инициировать `Class` с нужным классом.
```java Class<?> stringClass = String.class; Method[] methods = stringClass.getMethods(); ```
Пример инициации класса `String` и получения всех методов `String`
С тех пор Java постоянно улучшала рефлексию, добавляя новые возможности. Например, в Java 5 появился новый оператор `instanceof`, который позволяет проверить, является ли объект экземпляром класса. А в Java 8 появился метод `getDeclaredMethods()`, который позволяет получить информацию о методах класса включая приватные.
Динамические прокси
Версия рефлексии в Java обладает еще одним очень полезным свойством — позволяет создавать динамические прокси. А они, в свою очередь, позволяют перехватывать вызовы к методам выбранного класса. Это может пригодиться, например, если нужно логировать вызовы к методам класса.
Примеры использования Java Reflection
Рассмотрим несколько имплементаций того, как можно использовать рефлексию в Java. Все примеры будут использовать класс `Human`, в котором есть приватное поле `name` и публичный геттер `getName()`, а также приватная функция `generateSecret()`. Приватная функция берет слово Secret и добавляет к нему входную строку, которую мы называем `salt`. Затем генерируется соответствующее строке число и выдается как результат.
Класс `Human` приведен ниже:
```java public class Human < public String name; public Human(String name, String address) < this.name = name; this.address = address; >private int generateSecret(String salt) < return "secret".concat(salt).hashCode(); >public String getName() < return name; >> ```
Получение метаданных класса
Ключевым для рефлексии является класс `Class`, именно он хранит всю информацию о классе, для которого был инициирован. Эти данные называют метаданными класса, то есть это вся доступная с помощью рефлексии информация о нем. Например, в метаданные входят имя класса, его модификаторы, родительский класс, интерфейсы, конструкторы, поля, методы и так далее.
В качестве примера создания класса Class для Human создадим класс `ReflectionExample` и в методе `main()` создадим объект класса `Human`. Для того чтобы получить соответствующий Class, достаточно вызвать функцию `getClass()` на любом объекте:
```java public class ReflectionExample < public static void main(String[] args) < Human john = new Human("John", "London"); Class<?> humanClass = john.getClass(); >> ```
Теперь мы можем использовать переменную `humanClass` для того, чтобы получить конкретные данные о классе. Для этого можно вызвать следующие методы на объекте `humanClass`:
* `getName()` — возвращает имя и пакет класса;
* `getSimpleName()` — возвращает имя класса без пакета;
* `getModifiers()` — возвращает модификаторы класса;
* `getSuperclass()` — возвращает родительский класс;
* `getInterfaces()` — возвращает список интерфейсов, которые наследует класс;
* `getConstructors()` — возвращает список конструкторов класса;
* `getFields()` — возвращает список публичных полей класса;
* `getDeclaredFields()` — возвращает список всех полей класса, в том числе приватных;
* `getMethods()` — возвращает массив публичных методов класса;
* `getDeclaredMethods()` — возвращает массив всех методов класса, в том числе приватных;
* `getPackage()` — возвращает имя пакета класса.
В методах `getFields()` и `getMethods()` заключается основной функционал рефлексии. Именно они позволят нам в дальнейшем поменять приватные поля и вызвать приватные методы.
Узнать о других методах и классах в языке и научиться их использовать можно на курсе Skypro «Java-разработчик». Программа обучения разбита на тематические блоки, в конце которых студенты выполняют курсовую работу. Опытные наставники и кураторы всегда готовы ответить на вопросы и помочь разобрать сложный материал.
Получение метаданных переменной
Покажем, как получить доступ и поменять то, что хранится в приватной переменной `name` в классе `Human`, даже если у нее нет сеттера.
Так же, как и выше, создадим объект класса `Human` и соответствующий `Class`. Используем функцию `getDeclaredFields()` для получения всех, в том числе приватных полей класса. Далее пройдемся по массиву переменных и найдем ту, которую хотим поменять. Как только мы нашли переменную, можем сразу же поменять или прочитать ее значение. Для этого используем функцию `setAccessible()` для снятия ограничения доступа и `set()` для изменения или `get()` для чтения значения.
```java public class ReflectionVarExample < public static void main(String[] args) throws Exception < Human john = new Human("John", 25); Field[] flds = Human.class.getDeclaredFields(); for (Field fld : flds) < if (fld.getName().equals("name")) < fld.setAccessible(true); fld.set(john, "Ivan"); >> System.out.println(john.getName()); > > ```
Если запустить этот код, можно увидеть, что имя изменилось на `Bob`, хотя изначально было `John`.
Получение метаданных метода
Теперь посмотрим, как получить метаданные обо всех методах класса, в том числе и приватных, а также вызвать любой из них. В Java Reflection методы можно получить сходным образом — это значит, что примеры получения метаданных метода и переменной во многом будут похожи.
Как и раньше, создадим объект класса `Human` и получим его метаданные. Далее используем `getDeclaredMethods` и получим список всех методов в этом классе. Среди этого списка найдем нужный и вызовем этот метод с собственными данными. Используем функцию `setAccessible()` для снятия ограничения доступа и `invoke()` непосредственно для вызова.
```java public class ReflectionMethodExample < public static void main(String[] args) throws Exception < Human john = new Human("John", 25); Method[] mthds = Human.class.getDeclaredMethods(); for (Method mthd : mthds) < if (mthd.getName().equals("getSecret")) < mthd.setAccessible(true); String secret = (int) mthd.invoke(john, "broken"); System.out.println(secret); >> > > ```
После запуска данной программы мы получим число из метода `getSecret`, несмотря на то что он был обозначен как приватный и нигде в коде класса не использовался.
Что такое рефлекшн
Боже, хоть одна всеняемая статья про Рефлекшн для новичков. А то, другие авторы сразу выввливают кучу методов, без объяснений для нубов. Спасибо, автор!
very junior java developer Уровень 47
21 апреля 2023
Грамотно написанный класс в Java: — У меня всё как надо, всё что нужно скрыть, помечено Private, я вообще не вызываю багов вовремя работы программы. Reflection API: — Я вас умоляю, не смешите мои подковы методы).
Griboed Уровень 30
21 февраля 2023
На собеседовании : — Reflection API, для чего он нужен? — Он позволяет наиболее циничный способом попрать принцип инкапсуляции в java.
Алексей Куренков Уровень 29
30 января 2023
proxylunae Уровень 45
22 июля 2022
Вызов метода мяу обычным способом : Кот, мяукни! Вызов метода мяу через рефлексию : Мяу, прозвучи от того кота!
SoSed Уровень 51 Expert
11 апреля 2022
Понял всё, кроме одного. Почему
clazz = Class.forName(Cat.class.getName());
clazz = Cat.class;
Вместо того, чтобы сразу получить объект класса, мы сначала получаем имя класса (которое нам известно), а потом по нему получаем объект класса. Или это как-то связано с полным именем (learn.javarush.Cat) ?
Vadim Makarenko Уровень 28
4 ноября 2021
Я статью только начал читать, но уже появился вопрос, причём, видимо, элементарный: если класс Cat наследует класс Animal, то зачем в наследнике повторять те же поля private String name; private int age; Они же ему переходят по наследству? Вопрос второй: как понять эти строки Class clazz = Class.forName(className); Cat cat = (Cat) clazz.newInstance(); Если стоит задача создать объект класса, чьё имя на момент компиляции неизвестно, то откуда в тексте программы это самое неизвестное имя? Я бы мог представить такую строку: Object obj = clazz.newInstance(); Она была бы универсальной для любого типа. И она есть в этом же коде ниже, в методе createObject(), но он же не вызывается вроде. Вызывается createCat()
Kamoliddin Уровень 41
22 октября 2021
вернулся сюда из 34 уровня лекции 8 задачи 2, запомните как создать обж с конструктором который принимает аргумент.
Sergey Tsaregorodtsev Уровень 29
15 сентября 2021
На этом наша лекция подошла к концу! Она получилась довольно большой, но сегодня ты узнал много нового :)
И сегодня, и вчера, и позавчера. Смог прожевать только за три вечера. Не уверен, что прожевал тщательно, и не случится изжоги.