Рефлексия кода, reflection
Рефлексия (от reflexio — обращение назад) — это механизм исследования данных о программе во время её выполнения. Рефлексия в Java осуществляется с помощью Java Reflection API, состоящий из классов пакетов java.lang и java.lang.reflect. В информатике рефлексия означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Java Reflection API позволяет получать информацию о конструкторах, методах и полях классов и выполнять следующие операции над полями и методами объекта/класса :
- определение класса объекта;
- получение информации о полях, методах, конструкторах и суперклассах;
- получение информации о модификаторах полей и методов;
- создание экземпляра класса, имя которого неизвестно до момента выполнения программы;
- определение и изменение значений свойств объекта/класса;
- вызов методов объекта/класса.
Примечание : в тексте используется объект/класс. При работе с объектом (реализацией класса) можно обращаться к полям и методам класса напрямую, если они доступны (не private). При работе с классом можно обращаться к методам класса с использованием Java Reflection API. Но класс необходимо получить из объекта.
Определение свойств класса
В работающем приложении для получения класса необходимо использовать метод forName (String className). Следующий код демонстрирует возможность создания класса без использования и с использованием Reflection :
// Без использования Reflection Foo foo = new Foo(); // С использованием Reflection Class foo = Class.forName("Foo"); // Загрузка JDBC-драйвера Class.forName("com.mysql.jdbc.Driver");
Метод класса forName(className) часто используется для загрузки JDBC-драйвера.
Методом getName() объекта Class можно получить наименование класса, включающего пакет (package) :
Class aclass = foo.getClass(); System.out.println (aclass.getName());
Для получения значения модификатора класса используется метод getModifiers(). Класс java.lang.reflect.Modifier содержит статические методы, возвращающие логическое значения проверки модификатора класса :
Class cls = foo.getClass(); int mods = cls.getModifiers(); if (Modifier.isPublic (mods)) < System.out.println("public"); >if (Modifier.isAbstract(mods)) < System.out.println("abstract");>if (Modifier.isFinal (mods))
Для получения суперкласса рефлексированного объекта (класса) необходимо использовать метод getSuperclass() :
Class cls = foo.getClass(); Class superCls = cls.getSuperClass();
Поскольку в Java отсутствует множественное наследование, то для получения всех предков следует рекурсивно вызвать метод getSuperclass() в цикле, пока не будет достигнут Object, являющийся родителем всех классов. Object не имеет родителей, поэтому вызов его метода getSuperclass() вернет null.
Определение интерфейсов и конструкторов класса
Для получения в режиме run-time списка реализующих классом интерфейсов, необходимо получить Class и использовать его метод getInterfaces(). В следующем примере извлекается список интерфейсов класса ArrayList :
Class cls = ArrayList.class; Class[] ifs = cls.getInterfaces(); System.out.println(«List of interfaces\n»); for(Class ifc : ifs)
Чтобы IDE (Eclipse) не предупреждала о необходимости определения типа класса
Class is a raw type. References to generic type Class should be parameterized
в коде были использованы generic’и. В консоль выводятся следующие интерфейсы, реализуемые классом ArrayList :
List of interfaces java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable
Метод класса getConstructors() позволяет получить массив открытых конструкторов типа java.lang.reflect.Constructor. После этого, можно извлекать информацию о типах параметров конструктора и генерируемых исключениях :
Class cls = obj.getClass(); Constructor[] constructors = cls.getConstructors(); for (Constructor constructor : constructors) < Class[] params = constructor.getParameterTypes(); for (Class param : params) < System.out.println(param.getName()); >>
Определение полей класса
Метод getFields() объекта Class возвращает массив открытых полей типа java.lang.reflect.Field, которые могут быть определены не только в данном классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Field позволяет получить имя поля, тип и модификаторы :
Class cls = obj.getClass(); Field[] fields = cls.getFields(); for (Field field : fields) < Classfld = field.getType(); System.out.println("Class name : " + field.getName()); System.out.println("Class type : " + fld.getName()); >
Если известно наименование поля, то можно получить о нем информацию с помощью метода getField() объекта Class.
Class cls = obj.getClass(); Field fld = cls.getField("fieldName");
Методы getField() и getFields() возвращают только открытые члены данных класса. Чтобы получить все поля класса, включая закрытые и защищенные, необходимо использовать методы getDeclaredField() и getDeclaredFields(). Данные методы работают точно так же, как и их аналоги getField() и getFields().
Определение значений полей класса
Класс Field содержит специализированные методы для получения значений примитивных типов: getInt(), getFloat(), getByte() и др. Для установки значения поля, используется метод set(). Для примитивных типов имеются методы setInt(), setFloat(), setByte() и др.
Class cls = obj.getClass(); Field field = cls.getField("fieldName"); String value = (String) field.get(obj); field.set(obj, "New value");
Ниже приведен пример изменения значения закрытого поля класса в режиме run-time.
Определение методов класса
Метод getMethods() объекта Class возвращает массив открытых методов типа java.lang.reflect.Method. Эти методы могут быть определены не только в классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Method позволяет получить имя метода, тип возвращаемого им значения, типы параметров метода, модификаторы и генерируемые исключения.
Class cls = obj.getClass(); Method[] methods = cls.getMethods(); for (Method method : methods) < System.out.println("Method name : " + method.getName()); System.out.println("Return type : " + method.getReturnType().getName()); Class[] params = method.getParameterTypes(); System.out.print("Parameters : "); for (Class paramType : params) < System.out.print(" " + paramType.getName()); >System.out.println(); >
Если известно имя метода и типы его параметров, то можно получить отдельный метод класса :
Class cls = obj.getClass(); Class[] params = new Class[] ; Method method = cls.getMethod("methodName", params);
Пример изменения значения закрытого поля класса
Чтобы изменить значение закрытого (private) поля класса необходимо получить это поле методом getDeclaredField () и вызвать метод setAccessible (true) объекта Field, чтобы открыть доступ к полю. После этого значение закрытого поля можно изменять, если оно не final. В следующем примере определен внутренний класс PrivateFinalFields с набором закрытых полей; одно из полей final. При создании объекта класса поля инициализируются. В методе main примера поочередно в закрытые поля вносятся изменения и свойства объекта выводятся в консоль.
import java.lang.reflect.Field; class PrivateFinalFields < private int i = 1; private final String s = "String S"; private String s2 = "String S2"; public String toString() < return "i = " + i + ", " + s + ", " + s2; >> public class ModifyngPrivateFields < public static void main(String[] args) throws Exception < PrivateFinalFields pf = new PrivateFinalFields(); Field f = pf.getClass().getDeclaredField("i"); f.setAccessible(true); f.setInt(pf, 47); System.out.println("1. " + pf); f = pf.getClass().getDeclaredField("s"); f.setAccessible(true); f.set(pf, "MODIFY S"); System.out.println("2. " + pf); f = pf.getClass().getDeclaredField("s2"); f.setAccessible(true); f.set(pf, "MODIFY S2"); f = pf.getClass().getDeclaredField("i"); f.setAccessible(true); f.setInt(pf, 35); System.out.println("3. " + pf); >>
В результате выполнения примера в консоль будут выведены следующие сообщения :
1. i = 47, String S, String S2 2. i = 47, String S, String S2 3. i = 35, String S, MODIFY S2
Из приведённого примера видно что поля private можно изменять. Для этого необходимо получить объект типа java.lang.reflect.Field с помощью метода getDeclaredField (), вызвать его метод setAccessible (true) и с помощью метода set () установить требуемое значение поля. Необходимо иметь в виду, что наличие модификатора final в закрытом текстовом поле не вызывает исключений при изменении значений, а само значение поля остаётся прежним, т.е. final поля остаются неизменные. Если не вызвать метод открытия доступа к полю setAccessible (true), то будет вызвано исключение java.lang.IllegalAccessException.
Пример вызова метода, invoke
Java Reflection Api позволяет вызвать метод класса. Рассмотрим пример, в котором определим класс Reflect, включающий поля и методы управления ими. В режиме run-time с помощью метода данного класса будем изменять значения полей и распечатывать их.
Листинг класса Reflect
Класс Reflect включает два закрытых поля (id, name) и методы управления их значениями set/get. Дополнительно в класс включим метод setData, который будем вызывать для изменения значений полей, и метод toString для печати их значений.
class Reflect < private String name; private int id; Reflect() < name = "Test"; >public int getId() < return id; >public void setId(int id) < this.id = id; >String getName() < return name; >public void setName(String name) < this.name = name; >public void setData(final int id, String name) < this.id = id; this.name = name; >@Override public String toString() < return "Reflect [ id : " + id + ", name : " + name + "]"; >>
Для тестирования объекта типа Reflect с помощью Java Reflection Api создадим класс ReflectionTest. В этот класс включим две процедуры getClassFields и getClassMethods, которые в режиме run-time распечатают всю информацию (описание полей и методов) о классе. Методы получают класс в качестве параметра. В процедурах сначала определяются массивы полей и методы; после этого их параметры распечатываются :
private void getClassFields(Class cls) < Field[] fields = cls.getDeclaredFields(); System.out.println("Class fields"); for (Field field : fields) < Classfld = field.getType(); System.out.println("Class name : " + field.getName()); System.out.println("Class type : " + fld.getName()); > > private void getClassMethods(Class cls) < Method[] methods = cls.getDeclaredMethods(); System.out.println("Class methods"); for (Method method : methods) < System.out.println("Method name : " + method.getName()); System.out.println("Return type : " + method.getReturnType().getName()); Class[] params = method.getParameterTypes(); System.out.print("Parameters : "); for (Class param : params) System.out.print(" " + param.getName()); System.out.println(); > >
В конструкторе класса ReflectionTest сначала вызываются процедуры определения полей и методов объекта/класса Reflect. После этого вызываются методы изменения значений и печати значений с использованием Reflection API. Для определения метода setData используется массив типов параметров. Вызов метода setData выполняется с передачей ему массива новых значений.
public class ReflectionTest < static Reflect reflect; public ReflectionTest() < getClassFields (reflect.getClass()); getClassMethods(reflect.getClass()); Classcls = reflect.getClass(); try < System.out.println("\n1. invoke method toString()\n"); Method method = cls.getMethod("toString"); System.out.println(method.invoke(reflect)); Class[] paramTypes; Object [] args; paramTypes = new Class[] ; method = cls.getMethod("setData", paramTypes); args = new Object[]; method.invoke(reflect, args); System.out.println("\n2. invoke method toString()\n"); method = cls.getMethod("toString"); System.out.println(method.invoke(reflect)); > catch (NoSuchMethodException e) < >catch (SecurityException e) < >catch (IllegalAccessException e) < >catch (IllegalArgumentException e) < >catch (InvocationTargetException e) < >> private void getClassFields(Class cls) < // код метода представлен выше >private void getClassMethods(Class cls) < // код метода представлен выше >public static void main(String[] args) < reflect = new Reflect(); new ReflectionTest(); System.exit(0); >>
В результате выполнения примера в консоль будут выведены представленные ниже сообщения. Методы setData и toString(), вызываемые с помощью Java Reflection API, вносят измнения в закрытые поля класса и распечатываются их значения.
Class fields Class name : name Class type : java.lang.String Class name : id Class type : int Class methods Method name : toString Return type : java.lang.String Parameters : Method name : getId Return type : int Parameters : Method name : setId Return type : void Parameters : int Method name : getName Return type : java.lang.String Parameters : Method name : setName Return type : void Parameters : java.lang.String Method name : setData Return type : void Parameters : int java.lang.String 1. invoke method toString() Reflect [ id : 999, name : Test] 2. invoke method toString() Reflect [ id : 123, name : New value]
Скачать пример
Исходный код рассмотренного примера вызова метода invoke с использованием Java Reflection API можно скачать здесь (989 байт).
Java Reflection API
Java Reflection API — это программный интерфейс в языке Java, который позволяет приложениям анализировать свои компоненты и программное окружение, изменять собственное поведение и структуру. Позволяет исследовать информацию о полях, методах и конструкторах классов.

Освойте профессию «Java-разработчик»
С помощью механизма рефлексии можно обрабатывать типы, которые отсутствовали при компиляции, но появились во время выполнения программы. Рефлексия и наличие логически целостной модели выдачи информации об ошибках позволяют создавать корректный динамический код.
Возможности
Помимо самомодификации, API способен проводить самопроверку и самоклонирование. Чаще всего рефлексию Java используют:

- для получения информации о классах, интерфейсах, функциях, конструкторах, методах и модулях;
- изменения имен функций и классов во время выполнения программы;
- создания новых экземпляров классов;
- анализа и исполнения кода, поступающего из программного окружения;
- преобразования классов из одного типа в другой;
- создания массивов данных и манипуляций с ними;
- установления значений полей объектов по именам;
- получения доступа к переменным и методам, включая приватные, и к внешним классам;
- вызова методов объектов по именам.
Профессия / 14 месяцев
Java-разработчик
Освойте востребованный язык

Особенности рефлексии в Java
Снижение производительности программы
Рефлексия работает медленнее, чем обычные приемы по обработке классов, методов и переменных. Это связано с тем, что во время динамического определения многих типов оптимизация производительности становится недоступной. Поэтому не следует применять Reflection API во фрагментах кода, которые часто используются приложением, в особенности если скорость выполнения программы — приоритет разработчика.
Блокировка диспетчером безопасности
Для запуска рефлексии в программировании требуется разрешение на выполнение, которое, как правило, не выдается при работе программного компонента под управлением Security Manager (диспетчера безопасности).

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке
Уязвимость
При неверном использовании API способен нарушать один из главных принципов объектно-ориентированного программирования — инкапсуляцию данных. Это может привести к появлению потенциальных уязвимостей в веб-приложениях. В период с 2013 по 2016 год в библиотеке Reflection существовала брешь, которая позволяла хакерам обходить «песочницу» (изолированную зону для выполнения программ).
Нарушение переносимости программы
Поскольку Reflection API позволяет коду выполнять операции, которые обычно находятся под запретом, например получать доступ к закрытым полям и методам, использование рефлексии может сделать код неработоспособным и нарушить переносимость с одной операционной системы на другую. Кроме того, рефлексивный код нарушает абстракции, поэтому может изменить поведение программы при обновлении платформы.
Пример работы Reflection API в Java
Чтобы использовать Java Reflection API, не нужно подключать сторонние библиотеки. Все расположено в пакете java.lang.reflect.
Продемонстрируем некоторые методы рефлексии в программировании на конкретных примерах.
// Демонстрация работы Рефлексии в Java
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
// Создание объектов для класса Sample
// Создаем приватное поле private
// Создаем публичный конструктор
// Создаем публичный метод без параметров
public void method1() System.out.println(«Информация в строке — » + s); >
// Создаем публичный метод с целым числом в качестве параметра
public void method2(int x) System.out.println(«Целое число — » + x);
>
// Создаем приватный метод
private void method3() System.out.println(«Вызов приватного метода»);
>
>
class Exercise
public static void main(String args[]) throws Exception
// Создаем объект для последующей проверки свойств
Sample obj = new Sample();
// Создаем новый объект класса из другого объекта
Class cls = obj.getClass();
System.out.println(«Имя класса — » +
cls.getName());
// Получаем имя конструктора класса с помощью объекта
Constructor constructor = cls.getConstructor();
System.out.println(«Имя конструктора — » +
constructor.getName());
System.out.println(«Это публичные методы классов: «);
// Получаем методы классов с помощью объектов
Method[] methods = cls.getMethods();
// Выводим имена методов
for (Method method:methods)
System.out.println(method.getName());
// Создаем объект нужного метода с помощью имени метода и параметра класса
Method methodcall1 = cls.getDeclaredMethod(«method2», int.class);
// Вызов метода во время исполнения
// Создаем объект нужного поля с помощью имени поля
Field field = cls.getDeclaredField(«s»);
// Открываем доступ к полю независимо от используемого в нем спецификатора доступа
// Устанавливаем новое значение поля
// Создаем объект метода с помощью имени метода
Method methodcall2 = cls.getDeclaredMethod(«method1»);
// Вызов метода во время исполнения
// Создаем третий объект метода с помощью имени метода
Method methodcall3 = cls.getDeclaredMethod(«method3»);
// Изменяем настройки доступа
// Вызов метода во время исполнения
Рефлексия — мощный инструмент, для правильного использования которого требуются высокая квалификация и взвешенный подход.
Java-разработчик
Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.
Аннотации и рефлексия в Java

Аннотации и рефлексия являются ключевыми концепциями в Java, предоставляя разработчикам мощные инструменты для создания более гибких, адаптивных и понятных приложений. Аннотации предоставляют способ добавить метаданные к классам, методам и полям, что позволяет компилятору и другим инструментам анализировать код более глубоко. Рефлексия, с другой стороны, позволяет программам анализировать и модифицировать свой собственный состав и поведение во время выполнения.
Аннотации в Java
Аннотации в Java представляют собой метаданные, которые можно добавлять к классам, методам, полям и другим элементам кода. Они обеспечивают дополнительную информацию о коде, которая может быть использована компилятором, средствами разработки или даже во время выполнения программы. Для создания аннотации в Java используется аннотированный интерфейс, который определяет структуру аннотации.
Пример объявления аннотации:
public @interface MyAnnotation < String value(); // Элемент аннотации >
В данном примере MyAnnotation — это пользовательская аннотация, содержащая один элемент value . Элементы аннотации могут иметь различные типы данных, такие как строки, числа или даже другие классы.
Встроенные аннотации (например, Override, Deprecated)
Java предоставляет набор встроенных аннотаций, которые имеют специальное значение и часто используются в разработке.
— @Override : Эта аннотация указывает, что метод переопределяет метод из суперкласса. Она помогает предотвратить ошибки в случае, если вы ошибочно не переопределили метод.
Пример использования @Override :
@Override public void someMethod() < // Код метода >
— @Deprecated : Эта аннотация помечает элемент (класс, метод, поле и т. д.) как устаревший. Она предупреждает разработчиков о том, что использование этого элемента не рекомендуется, и в будущих версиях Java может быть удалено.
Пример использования @Deprecated :
@Deprecated public void oldMethod() < // Устаревший код >
Использование встроенных аннотаций помогает улучшить читаемость и надежность кода, а также упрощает его документирование. Они также могут быть использованы средствами анализа кода или инструментами для генерации документации.
Создание пользовательских аннотаций
Пользовательские аннотации — это мощный механизм, который позволяет разработчикам внедрять собственные метаданные в код. Это открывает широкие возможности для улучшения читаемости, обеспечения безопасности и документирования кода, а также для создания собственных фреймворков и инструментов.
Документирование кода
Пользовательские аннотации могут использоваться для документирования вашего кода, добавляя дополнительные комментарии и описания. Например, вы можете создать аннотацию @Description , чтобы добавить краткое описание класса, метода или переменной, которое автоматически включается в сгенерированную документацию:
@Retention(RetentionPolicy.RUNTIME) @Target() public @interface Description
@Description("Этот класс представляет собой модель пользователя.") public class User < // Поля и методы класса >
Проверка валидности данных
Аннотации могут использоваться для проверки валидности данных на этапе компиляции или даже во время выполнения. Например, вы можете создать аннотацию @NotNull , которая гарантирует, что поле не может быть пустым:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface NotNull
public class User < @NotNull private String username; // Остальные поля и методы >
Автоматизация задач
Пользовательские аннотации могут использоваться для автоматизации различных задач. Например, вы можете создать аннотацию @Benchmark , которая помечает метод как метод, который нужно измерить на производительность.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Benchmark
public class PerformanceTester < @Benchmark public void testMethod() < // Код для тестирования производительности >// Другие методы >
Шаги создания пользовательской аннотации
Создание пользовательской аннотации в Java включает в себя несколько шагов:
1. Определение аннотации:
— Определите интерфейс с ключевым словом @interface . В этом интерфейсе определяются элементы аннотации, которые могут иметь различные типы данных.
2. Определение целевых элементов:
— Решите, на какие элементы вашего кода вы хотите применять аннотацию (классы, методы, поля и т. д.). Это определяется с помощью аннотации @Target .
3. Указание правил видимости:
— Определите, как долго аннотация будет храниться (зависит от вашей потребности): во время компиляции, во время выполнения или вообще не храниться. Это устанавливается с помощью аннотации @Retention .
4. Применение аннотации:
— Примените вашу пользовательскую аннотацию к соответствующим элементам вашего кода, используя синтаксис @НазваниеАннотации .
5. Обработка аннотаций (по желанию):
— Разработайте код или инструменты, которые будут анализировать и использовать информацию из ваших пользовательских аннотаций.
Создание пользовательских аннотаций — это мощное средство для улучшения структуры и функциональности вашего кода. Они позволяют вам добавлять семантику к вашему коду и делать его более читаемым и надежным.
Применение аннотаций
1. Маркировка элементов:
Аннотации могут быть использованы для маркировки классов, методов, полей и других элементов кода. Например, аннотация @Entity может быть применена к классу, чтобы указать, что это сущность базы данных.
@Entity public class User < // Поля и методы >
2. Дополнительная информация:
Аннотации могут содержать дополнительные элементы, которые предоставляют информацию или параметры. Например, аннотация @Column может указывать дополнительные настройки для столбца в базе данных.
@Column(name = "user_name", nullable = false) private String username;
3. Контроль компиляции:
Аннотации могут использоваться для контроля компиляции. Например, аннотация @Override гарантирует, что метод действительно переопределен из суперкласса.
@Override public void someMethod() < // Код метода >
4. Аннотации времени выполнения:
Некоторые аннотации могут использоваться во время выполнения. Например, аннотация @Autowired в Spring Framework используется для инъекции зависимостей во время выполнения.
@Autowired private UserService userService;
5. Обработка аннотаций:
Средства анализа кода и библиотеки могут анализировать аннотации и выполнять дополнительные действия в зависимости от их наличия. Например, библиотека JUnit использует аннотации для определения тестовых методов и их автоматического выполнения.
@Test public void testMethod() < // Код теста >
Java также предоставляет ряд встроенных аннотаций, которые имеют специальное значение и часто используются в разработке.
Рефлексия в Java
Рефлексия в Java представляет собой механизм, который позволяет программам анализировать и манипулировать собственной структурой, типами данных и поведением во время выполнения. Это означает, что приложение может динамически получать информацию о классах, методах, полях, интерфейсах и других элементах и даже вызывать их методы без заранее известной структуры.
Рефлексия имеет важное значение в различных аспектах разработки Java:
a. Анализ кода на этапе выполнения:
— Рефлексия позволяет приложению анализировать классы, методы и поля во время выполнения, что особенно полезно в контексте рефлексии для анализа аннотаций, создания макетов данных и т. д.
b. Интроспекция библиотек и фреймворков:
— Многие библиотеки и фреймворки Java используют рефлексию для сканирования и обработки классов и ресурсов. Это позволяет им создавать мощные и гибкие решения, такие как инверсия управления (Inversion of Control) и внедрение зависимостей (Dependency Injection).
c. Создание обобщенных утилит:
— Рефлексия позволяет создавать обобщенные утилиты, которые могут работать с различными классами и объектами, даже если их структура заранее неизвестна.
Как рефлексия отличается от статического кода
Рефлексия и статический код представляют собой два различных подхода к работе с данными и кодом в Java:
- Статический код определяется и компилируется на этапе разработки. Все типы данных, классы и методы известны заранее.
- Эффективность статического кода обычно выше, так как все оптимизации могут быть применены на этапе компиляции.
- Изменение структуры кода требует перекомпиляции приложения.
- Рефлексия позволяет анализировать и взаимодействовать с кодом во время выполнения, когда структура и типы данных могут меняться.
- Подход с рефлексией более гибок и может использоваться для создания обобщенных или расширяемых решений.
- Использование рефлексии может увеличить сложность кода и ухудшить производительность из-за дополнительных операций во время выполнения.
Класс Class и объекты Class
Класс Class в Java представляет собой метаданные о классе, интерфейсе или примитивном типе данных.
Существуют несколько способов получения объекта Class :
a. С использованием литерала класса:
Самый простой способ получить объект Class — это использовать литерал класса, который представляет собой имя класса, например, MyClass.class . Этот подход часто используется для получения Class известного класса на этапе компиляции.
Class myClassClass = MyClass.class;
b. С использованием метода getClass():
В Java у каждого объекта есть метод getClass() , который возвращает объект Class , представляющий тип этого объекта. Этот метод может быть полезен при работе с объектами, когда тип объекта известен только во время выполнения.
MyClass obj = new MyClass(); Class objClass = obj.getClass();
c. С использованием статического метода forName():
Метод Class.forName(String className) позволяет загрузить класс по его имени в виде строки. Этот метод полезен, когда имя класса известно во время выполнения и может быть задано динамически.
String className = "com.example.MyClass"; Class myClassClass = Class.forName(className);
Основные методы класса Class
Класс Class предоставляет множество методов для анализа и взаимодействия с типами данных. Вот некоторые из наиболее часто используемых методов:
a. getName()
— Метод getName() возвращает имя класса в виде строки.
Class myClassClass = MyClass.class; String className = myClassClass.getName(); // Возвращает "com.example.MyClass"
b. getSimpleName():
— Метод getSimpleName() возвращает простое имя класса (без пакета) в виде строки.
String simpleName = myClassClass.getSimpleName(); // Возвращает "MyClass"
c. isAssignableFrom(Class cls):
— Метод isAssignableFrom(Class cls) проверяет, может ли класс, представляемый текущим объектом Class , быть присвоен классу, представляемому объектом cls .
boolean isAssignable = Number.class.isAssignableFrom(Integer.class); // Возвращает true
d. getMethods(), getFields(), getConstructors():
— Методы getMethods() , getFields() , getConstructors() возвращают массив методов, полей и конструкторов соответственно, которые доступны в данном классе (включая унаследованные).
Method[] methods = myClassClass.getMethods(); Field[] fields = myClassClass.getFields(); Constructor[] constructors = myClassClass.getConstructors();
Эти методы и многие другие позволяют анализировать и взаимодействовать с классами и объектами во время выполнения.
Использование рефлексии
1. Получение информации о классе
Рефлексия в Java позволяет получать различную информацию о классе, такую как его имя, пакет, суперкласс и интерфейсы. Вот некоторые способы получения информации о классе с использованием рефлексии:
a. Получение имени класса:
— Вы можете получить имя класса с помощью метода getName() класса Class .
Class myClassClass = MyClass.class; String className = myClassClass.getName(); // Возвращает "com.example.MyClass"
b. Получение имени пакета:
— Можно извлечь имя пакета, в котором находится класс, с помощью метода getPackage() .
Package classPackage = myClassClass.getPackage(); String packageName = classPackage.getName(); // Возвращает "com.example"
c. Получение суперкласса:
— С помощью метода getSuperclass() можно получить суперкласс текущего класса.
Class superClass = myClassClass.getSuperclass();
d. Получение интерфейсов:
— Вы можете получить список интерфейсов, которые реализует класс, с помощью метода getInterfaces() .
Class[] interfaces = myClassClass.getInterfaces();
Создание объектов с использованием рефлексии
Рефлексия также позволяет создавать объекты классов динамически. Это может быть полезно, например, при создании экземпляров классов на основе конфигурации или пользовательского ввода.
a. Создание объекта без параметров:
— С помощью метода newInstance() класса Class можно создать объект класса без передачи параметров конструктору.
Class myClassClass = MyClass.class; MyClass instance = (MyClass) myClassClass.newInstance();
b. Создание объекта с параметрами:
— Если у класса есть конструктор с параметрами, вы можете получить этот конструктор и передать аргументы с помощью рефлексии.
Class myClassClass = MyClass.class; Constructor constructor = myClassClass.getConstructor(String.class, int.class); MyClass instance = (MyClass) constructor.newInstance("example", 42);
Вызов методов и чтение полей с помощью рефлексии
Рефлексия позволяет вызывать методы и читать поля классов во время выполнения. Это может быть полезно, например, при обработке данных, которые структура которых неизвестна на этапе компиляции.
a. Вызов метода:
— Вы можете получить метод класса с помощью метода getMethod() , а затем вызвать его, передав необходимые аргументы.
Class myClassClass = MyClass.class; Method method = myClassClass.getMethod("someMethod", int.class); MyClass instance = new MyClass(); int result = (int) method.invoke(instance, 42);
b. Чтение поля:
— Для чтения значения поля класса используйте метод getField() и метод get() объекта поля.
Class myClassClass = MyClass.class; Field field = myClassClass.getField("fieldName"); MyClass instance = new MyClass(); String value = (String) field.get(instance);
Рефлексия предоставляет мощные инструменты для анализа и манипуляции классами и объектами во время выполнения. Однако она также требует аккуратного использования и может снижать производительность, поэтому ее следует применять с осторожностью и только тогда, когда это необходимо для решения конкретных задач.
Аннотации и рефлексия в совместной работе
Аннотации и рефлексия могут взаимодействовать с целью создания гибких и универсальных приложений. Взаимодействие между ними позволяет писать код, который способен адаптироваться к изменяющимся условиям и требованиям. Вот как аннотации и рефлексия могут взаимодействовать:
1. Использование аннотаций для маркировки классов и методов
Аннотации могут служить маркерами для классов, методов и полей. Это может быть полезно, например, при создании собственных аннотаций для указания специфических свойств или поведения классов.
@MyCustomAnnotation public class MyClass < @MyCustomAnnotation public void myMethod() < // Реализация метода >>
2. Извлечение аннотаций и выполнение действий с помощью рефлексии
Рефлексия позволяет анализировать аннотации, примененные к классам, методам или полям, и выполнять действия на основе их присутствия. Например, вы можете создать код, который автоматически находит все методы с определенной аннотацией и вызывает их.
Class myClassClass = MyClass.class; Method[] methods = myClassClass.getMethods(); for (Method method : methods) < if (method.isAnnotationPresent(MyCustomAnnotation.class)) < // Вызываем метод, помеченный аннотацией MyCustomAnnotation method.invoke(instance, args); >>
Заключение
Использование аннотаций и рефлексии в совместной работе требует баланса между гибкостью и сложностью кода. Правильное применение этих инструментов может значительно улучшить архитектуру и расширяемость приложений, но также требует осторожности и хорошего понимания их преимуществ и ограничений.
Завершить статью хотел бы полезной рекомендацией. Уже скоро у моих коллег из OTUS пройдет бесплатный вебинар про TCP/IP сервер в Java. Регистрация абсолютно бесплатна, поэтому рекомендую к посещению!
- Блог компании OTUS
- Программирование
- Java
Глубокое погружение в Java: рефлексия и загрузчик классов. Часть 1
Парадигмы программирования можно сравнить с территориями. На каждой территории, от императивного до декларативного программирования, установлены строгие законы поведения, диктующие, что можно делать, а что нельзя. Но чаще всего разработчики оказываются на территории, где приходится осуществлять то, что не одобряется или запрещается. В таких случаях начинается поиск лазеек и способов применения системы для решения насущных задач. Иногда обнаруживаются законы, которые разрешают действия, противоречащие основным принципам системы.
В этой статье я покажу, как две уникальные особенности Java позволяют создать полезные антишаблоны на территории Java.
Территория ООП
Объектно-ориентированное программирование (ООП) является одной из наиболее распространенных моделей программирования. Оно предполагает, что логика должна строиться вокруг определенных программой типов данных. Java-разработчики сначала определяют основные типы данных, необходимые в программе, а затем включают логику в классы и определяют способы взаимодействия этих классов друг с другом.
Хотя такой подход обладает общепризнанными преимуществами, статически типизированному языку, каковым является Java, не хватает гибкости. Java требует строгого определения типов данных на этапе компиляции. Это не всегда выполнимо, поскольку часто возникают ситуации, в которых невозможно предугадать на момент разработки программы, с какими именно типами данных она будет иметь дело. Кроме того, Java-программы не обеспечивают эволюционирование классов во время выполнения. Во многих случаях это также может стать трудно разрешимой задачей. Рассмотрим некоторые проблемы проектирования.
Проектирование фреймворков
Говоря простыми словами, фреймворк — это кодовая база, которая облегчает разработку, предоставляя структуру компонентов, в некотором роде скелет, на основе которого пользователи фреймворка могут создавать проекты. Определяемые пользователем типы могут быть подключены к фреймворку для настройки.
Например, инженер-тестировщик может осознать, что чаще всего реализует одни и те же шаги по тестированию API сервиса. Причем различия в коде заключаются в основном в типах данных, представляющих сам сервис. Он понимает, что большая часть его кода пригодна для повторного использования и может быть обобщена в виде фреймворка, который будет использоваться в дальнейшем при тестировании сервиса.
Чистое ООП в данном случае создает проблему, которая заключается в том, что приходится строить логику для неоднозначных типов данных. Полезным является фреймворк, который является достаточно общим, чтобы его можно было использовать для тестирования широкого спектра сервисов. Это в корне противоречит статически типизированным языкам, таким как Java, которые требуют манипулировать типами данных через определенные классы.
Проектирование для горячей замены
Программное обеспечение с возможностью “горячей замены” позволяет изменять компоненты во время выполнения без паузы в исполнении. Например, для предприятия часто бывает затратно останавливать работу приложений для обновления кода. Поэтому оно будет стремиться разрабатывать приложения, API которых остаются модифицируемыми и после развертывания.
Первая трудность, возникающая при решении этой задачи, заключается в том, что, подобно фреймворкам, логика должна строиться на основе изменяемых компонентов и, следовательно, неоднозначных типов данных. Кроме того, эта проблема связана с дополнительной сложностью загрузки нового кода во время выполнения, что представляет собой отдельную дополнительную проблему.
Приведенные выше примеры демонстрируют две сложные задачи объектно-ориентированного программирования на языке Java.
- Возможность проектирования для неоднозначных типов данных, то есть типов данных, классы которых неизвестны и не могут быть определены во время разработки.
- Возможность эволюционирования типов данных с изменением их классов во время выполнения программы.
Посмотрим, как пара взаимосвязанных возможностей Java — рефлексия и загрузчики классов — позволяют решать две вышеперечисленные проблемы и создавать невероятно гибкие Java-программы.
Рефлексия в Java
Пожалуй, самой популярной особенностью Java является то, что это статически типизированный язык.
Статическая типизация и вызов
public class Foo
public void doSomething()
System.out.println("I did something");
>
Обычно метод doSomething() вызывается следующим образом:
Foo foo = new Foo();
foo.doSomething();
Приведенный выше код является примером статической типизации и вызова. Переменная foo считается статически типизированной, поскольку ее тип разрешается в тип Foo во время компиляции. Аналогично, компилятор связывает вызов метода doSomething() с методом экземпляра, определенным в классе Foo. При статической типизации и вызове такие языковые явления, как переменные и вызовы методов, можно рассматривать так, будто они фиксируют определенные типы перед выполнением. Для успешной компиляции необходимо, чтобы определения классов стояли на первом месте.
Естественно, что универсальные фреймворки, предназначенные для работы с любыми типами без ограничения интерфейсов, не могут использовать статическую типизацию и вызов. Вместо этого в Java для создания таких фреймворков используется малоизвестный стиль программирования, называемый рефлексивным программированием. Рефлексивное программирование позволяет нарушить статически типизированную природу Java-программ. Оно переносит присвоение типов на время выполнения, а не на время компиляции, что известно как динамическая типизация.
Рефлексивное программирование
Вот определение из учебника “Java Reflection in Action”:
“Рефлексия — это способность работающей программы исследовать себя и свое программное окружение и изменять свое поведение в зависимости от того, что она обнаружила”.
Основную деятельность разработчика можно условно разделить на две категории: творческий аспект в проектировании надежных сопровождаемых программ и программирование, эквивалентное механическому труду. Например, разработчики часто пишут код, который зависит от внешних модулей или компонентов. При изменении внешнего модуля разработчику нередко приходится копаться в коде и переписывать вызовы методов.
Рефлексивное программирование облегчает разработчику вторую задачу. Вместо того чтобы вручную выполнять такую утомительную работу, как рефакторинг кода, патчинг JAR и модификацию вызовов методов, рефлексия позволяет писать программы, которые могут делать выбор, обычно принимаемый человеком, например выбирать между классом X и классом Y или вызывать новый метод вместо старого.
Чтобы понять это, необходимо разобраться в том, как классы существуют внутри виртуальной машины Java (JVM). JVM — это изолированная среда, в которой выполняются Java-программы. По большей части она не знает о своем хост-устройстве и его файловой системе, то есть не знает о файлах .java и .class, которые создаются в процессе разработки и компиляции (подробнее об этом в разделе “Загрузчики классов”). JVM имеет собственную внутреннюю память, в которой хранит все необходимые данные, требующиеся ей во время выполнения программы, в том числе данные о классах программы. Ее память состоит из нескольких компонентов (область методов, область кучи, область стека, регистры ПК, область нативных методов).
Область кучи
Область кучи JVM (Heap Area) — это динамическое пространство памяти, в котором хранятся текущие объекты программы. Именно здесь объекты живут и существуют как программные единицы. Область кучи делится на три основные части.
- Young Generation (молодое поколение). Часть, где хранятся вновь созданные объекты.
- Old Generation (старшее поколение). Часть, где хранятся долгоживущие объекты, т. е. объекты, которые пережили определенное количество циклов сборки мусора в молодом поколении, прежде чем были переведены в старшее поколение.
- MetaSpace (метапространство). Часть, предназначенная для хранения специальных типов объектов, называемых метаобъектами (представляют метаданные программы). Здесь также хранится байт-код методов.
Чтобы язык Java вел себя ожидаемо, среда JVM должна отслеживать метаданные о программе, что позволяет ей корректно выполняться. Например, для конкретного класса JVM должна хранить такую информацию, как его модификаторы доступа, методы и их типы (статический метод или метод экземпляра). Для каждого метода хранится информация о количестве параметров и их типах, а также о возвращаемом типе метода. JVM хранит метаданные программы наиболее известным ей способом — в объектах, называемых метаобъектами. Создатели Java определили специальный набор классов, используемых JVM внутри программы, которые представляют компоненты программы и предоставляют доступ к ним. Классы, интерфейсы и методы программы моделируются как объекты, которые живут в метапространстве (в Java все является объектами, даже классы).
В пакете java.lang определен класс для типа Class:
public final class Class extends Object implements Serializable, GenericDeclaration, Type, AnnotatedElement
Этот класс определяет метаобъект, представляющий класс. Каждый пользовательский класс будет иметь соответствующий объект Class, инстанцированный для него в JVM. Класс Class определяет множество интересных методов, большинство из которых предоставляют информацию о структуре класса:
public Field[] getFields()
/* Возвращает массив, содержащий объекты Field,
представляющие все доступные публичные поля класса или интерфейса,
представленного данным объектом Class.*/
public Annotation[] getAnnotations()
/* Возвращает аннотации, присутствующие в классе. */
public Method[] getMethods()
/* Возвращает массив, содержащий объекты Method,
представляющие все публичные методы класса или интерфейса,
представленного данным объектом Class. */
public T newInstance()
/* Создает новый экземпляр класса, представленного данным объектом Class. */
Аналогично, в java.lang определен класс Method, который определяет метаобъекты, представляющие метод. Он также содержит несколько API, которые могут предоставить информацию о структуре конкретного метода, моделируемого объектом Method:
public final class Method extends Executable
public Annotation [] getDeclaredAnnotations()
/* Возвращает аннотации, присутствующие в данном методе. */
public Class getDeclaringClass()
/* Возвращает объект Class,
представляющий класс или интерфейс, который объявляет метод,
представленный данным объектом Method */
public Class[] getParameterTypes()
/* Возвращает массив объектов Class, представляющих типы параметров методов */
public Class getReturnType()
/* Возвращает объект Class,
представляющий тип возврата метода,
который представлен данным объектом Method. */
public Object invoke(Object obj, Object. args)
/* Вызывает базовый метод,
представленный данным объектом Method,
используя экземпляр класса метода и массив объектов в качестве аргументов */.
Рекомендую изучить в официальной документации API этих классов и других метаобъектов, таких как Field и Annotation. Совокупность метаобъектов в метапространстве представляет собой саму программу.
Тип Java является живым во время выполнения программы. Это не статическая сущность, а скорее живой дышащий объект в JVM.
Метаобъекты в метапространстве, хотя и имеют выделенную часть кучи, не ограничиваются только внутренним использованием JVM — они доступны для работающих программ. Программа может проводить самообследование, обращаясь к представляющим ее метаобъектам. Более того, метаобъекты открывают доступ к той части программы, которую они представляют. Как было показано выше, класс Class определяет метод newInstance() и с его помощью может вернуть объект-экземпляр класса, который он представляет. Аналогично, класс Method определяет метод invoke() , с помощью которого может быть вызван представляемый им метод.
Вернемся к определению рефлексии.
Рефлексия — это способность выполняющейся программы исследовать себя и свое программное окружение (путем обращения к метаобъектам, таким как объекты Class, представляющие ее классы, и обнаружения их внутренней структуры) и изменять свои действия в зависимости от того, что она обнаружила (поскольку можно выбрать вызов части программы, представленной определенным метаобъектом).
Но как именно это реализуется?
Динамическая типизация и вызов
Как следует из предыдущего раздела, необходимость в рефлексии в основном возникает при динамической типизации и вызове. Вызов метода doSomething() класса Foo может быть выполнен альтернативно с использованием рефлексии следующим образом:
String className ="org.example.Foo";
String methodName = "doSomething";
try // Получаем Class объекта foo (рефлексивно - объект класса Foo)
Class cls = Class.forName(className);
// Рефлексивно создаем новый экземпляр foo
Object obj = cls.getDeclaredConstructor().newInstance();
// Получаем метод doSomething
Method method= cls.getDeclaredMethod(methodName);
// Рефлексивно вызваем метод doSomething
method.invoke(obj);
>catch(Exception e)>
Приведенный выше код выполняет динамическую типизацию и вызов метода. При заданных переменных className и methodName он рефлексивно получает объект Class целевого класса и извлекает объект Method целевого метода, через который вызывает метод. Обратите внимание, что если задать в этих двух переменных разные имена классов и методов, то один и тот же код будет выполнять разные методы, поскольку во время компиляции он не привязан к какому-либо определенному типу.
Практический пример: JUnit
JUnit — это популярный фреймворк для автоматизации тестирования Java-приложений. Он позволяет автоматизировать многие аспекты тестирования, такие как отображение агрегированных отчетов о результатах тестирования или выполнение повторяющихся тестовых примеров.
Преимуществом JUnit является его универсальность: он может вызывать тестовые методы любого пользовательского класса, и пользователям даже не нужно придерживаться какого-либо интерфейса при разработке тестовых классов.
Для настройки JUnit в основном опирается на аннотации. Аннотации — это часть языка Java, которая используется для добавления метаданных или дополнительной информации к Java-программам без непосредственного влияния на аннотируемый код.
JUnit определяет аннотацию @Test для “маркировки” тестовых методов. Это означает, что JUnit знает, какие пользовательские методы следует запускать посредством этой аннотации. Например, пользователь может определить следующий класс:
public class TestsBatch
public int sum (int a, int b) return a+b;
>
@Test
public void test1() <
assertEquals(10, sum(3,7));
>
@Test
public void test2() assertEquals(100, sum(20,80));
>
JUnit запускает методы test1() и test2() , поскольку они снабжены аннотацией @Test . Работать так, как задумано, JUnit позволяет широкое использование рефлексии. С помощью рефлексии пользовательские классы обнаруживаются во время выполнения программы и проверяются на наличие методов с аннотацией @Test . Затем эти методы динамически вызываются. Базовая логика, на которой работает фреймворк, достаточно просто реализуется с помощью API рефлексии. Она заключается в следующем.
- Обнаружить/перебрать все определяемые пользователем классы.
- Для каждого класса получить его методы и проверить их.
- Для каждого метода проверить наличие аннотации @Test . Если она есть, то необходимо вызвать этот метод и обработать его результаты.
Все описанные выше действия показаны в приведенном ниже коде:
File[] files = new File("src/test/java/test").listFiles();
// перебор файлов по пути, в котором существуют определенные пользователем классы
for (File file : files) String fileName = file.getName();
// получение объекта Class, соответствующего текущему файлу
Class c = Class.forName("test." + fileName.substring(0, fileName.indexOf(".")));
// проверка того, что класс не является интерфейсом или перечислением
if (!c.isInterface() && !c.isEnum()) // получение методов, принадлежащих классу
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) // проверка наличия аннотации @test у метода
Annotation annotation= method.getAnnotation(Test.class);
if(annotation!=null) // создание нового экземпляра текущего класса
Object obj = c.getDeclaredConstructor().newInstance();
// вызов текущего метода
Object result = method.invoke(obj);
// выполнение необходимой обработки объекта результата
>
>
>
Примечание: этот пример написан мной и не относится к JUnit. Однако JUnit использует рефлексию в своих операциях.
Изначально воспринимаемая неинтуитивно (и, возможно, порождающая самый уродливый код), рефлексия оказывается очень полезной функцией, когда статически типизированная природа Java накладывает ограничения. Более того, она позволяет разработчикам создавать динамические системы и автоматизировать многие трудоемкие аспекты программирования.
В следующем разделе рассмотрим еще одну интересную особенность JVM — загрузчики классов, которые в паре с рефлексией позволяют создавать принципиально открытые расширяемые системы.
- Java 21: новый подход к созданию строк
- Заменят ли потоки данных циклы в Java?
- Кэширование Redis для максимальной производительности в Spring Boot и Java
Читайте нас в Telegram, VK и Дзен