Рефлексия (от reflexio - обращение назад) - это механизм исследования данных о программе во время её выполнения. Рефлексия в Java осуществляется с помощью Java Reflection API, состоящий из классов пакетов java.lang
и java.lang.reflect
. В информатике рефлексия означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Java Reflection API позволяет получать информацию о конструкторах, методах и полях классов и выполнять следующие операции над полями и методами объекта/класса:
-
определение класса объекта
-
получение информации о полях, методах, конструкторах и суперклассах
-
получение информации о модификаторах полей и методов
-
создание экземпляра класса, имя которого неизвестно до момента выполнения программы
-
определение и изменение значений свойств объекта/класса
-
вызов методов объекта/класса
В исходном коде используется объект/класс. При работе с объектом (реализацией класса) можно обращаться к полям и методам класса напрямую, если они доступны (не private). При работе с классом можно обращаться к методам класса с использованием Java Reflection API. Но класс необходимо получить из объекта.
1. Определение свойств класса
В работающем приложении для получения класса необходимо использовать метод 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)) {
System.out.println("final");
}
Для получения суперкласса рефлексированного объекта (класса) необходимо использовать метод getSuperclass()
:
Class cls = foo.getClass();
Class superCls = cls.getSuperClass();
Поскольку в Java отсутствует множественное наследование, то для получения всех предков следует рекурсивно вызвать метод getSuperclass()
в цикле, пока не будет достигнут Object
, являющийся родителем всех классов. Object
не имеет родителей, поэтому вызов его метода getSuperclass()
вернет null
.
2. Определение интерфейсов и конструкторов класса
Для получения в режиме runtime списка реализующих классом интерфейсов, необходимо получить Class
и использовать его метод getInterfaces()
. В следующем примере извлекается список интерфейсов класса ArrayList
:
Class<?> cls = ArrayList.class;
Class<?>[] ifs = cls.getInterfaces();
System.out.println("List of interfaces\n");
for(Class<?> ifc : ifs) {
System.out.println (ifc.getName());
}
Чтобы IDE не предупреждала, о необходимости определения типа класса, сообщением Class is a raw type. References to generic type Class<T> 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());
}
}
3. Определение полей класса
Метод getFields()
объекта Class
возвращает массив открытых полей типа java.lang.reflect.Field
, которые могут быть определены не только в данном классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Field
позволяет получить имя поля, тип и модификаторы:
Class<?> cls = obj.getClass();
Field[] fields = cls.getFields();
for (Field field : fields) {
Class<?> fld = 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()
.
4. Определение значений полей класса
Класс 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");
Ниже приведен пример изменения значения закрытого поля класса в runtime.
5. Определение методов класса
Метод 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[] {Integer.class, String.class};
Method method = cls.getMethod("methodName", params);
6. Пример изменения значения закрытого поля класса
Чтобы изменить значение закрытого (private
) поля класса, необходимо получить это поле методом getDeclaredField()
и вызвать метод setAccessible(true)
объекта Field
, чтобы открыть доступ к полю. После этого значение закрытого поля можно изменять, если оно не final
. В следующем примере определен внутренний класс PrivateFinalFields
с набором закрытых полей; одно из полей final
. При создании объекта класса поля инициализируются. В методе main()
примера поочередно в закрытые поля вносятся изменения и свойства объекта выводятся в консоль.
import java.lang.reflect.Field;
class PrivateFinalFields {
private final String s = "String S";
private int i = 1;
private String s2 = "String S2";
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
}
}
public class ModifyingPrivateFields {
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
.
7. Пример вызова метода invoke()
Java Reflection Api позволяет вызвать метод класса. Рассмотрим пример, в котором определим класс Reflect
, включающий поля и методы управления ими. В runtime с помощью метода данного класса будем изменять значения полей и распечатывать их.
Класс Reflect
включает два закрытых поля id
, name
и методы управления их значениями set/get. Дополнительно в класс включим метод setData()
, который будем вызывать для изменения значений полей, и метод toString()
для печати их значений.
class Reflect {
private String name;
private int id;
Reflect() {
this.name = "Test";
this.id = 999;
}
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()
, которые в runtime распечатают всю информацию (описание полей и методов) о классе. Методы получают класс в качестве параметра. В процедурах сначала определяются массивы полей и методы; после этого их параметры распечатываются:
private void getClassFields(Class<?> cls) {
Field[] fields = cls.getDeclaredFields();
System.out.println("Class fields");
for (Field field : fields) {
Class<?> fld = 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());
Class<?> cls = 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[] {int.class, String.class};
method = cls.getMethod("setData", paramTypes);
args = new Object[]{(int)123,new String("New value")};
method.invoke(reflect, args);
System.out.println("\n2. invoke method toString()\n");
method = cls.getMethod("toString");
System.out.println(method.invoke(reflect));
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
System.out.println(e);
}
}
private void getClassFields(Class<?> cls) {
Field[] fields = cls.getDeclaredFields();
System.out.println("Class fields");
for (Field field : fields) {
Class<?> fld = 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();
}
}
public static void main(String[] args) {
this.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]