Перейти к основному содержимому

Руководство по отражению Java

· 15 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим отражение Java, которое позволяет нам проверять и/или изменять атрибуты времени выполнения классов, интерфейсов, полей и методов. Это особенно удобно, когда мы не знаем их имен во время компиляции.

Кроме того, мы можем создавать новые объекты, вызывать методы и получать или устанавливать значения полей с помощью отражения.

2. Настройка проекта

Чтобы использовать отражение Java, нам не нужно включать какие-либо специальные jar-файлы , какую-либо специальную конфигурацию или зависимости Maven. JDK поставляется с группой классов, объединенных в пакете java.lang.reflect специально для этой цели.

Итак, все, что нам нужно сделать, это сделать следующий импорт в наш код:

import java.lang.reflect.*;

И мы готовы идти.

Чтобы получить доступ к информации о классе, методе и поле экземпляра, мы вызываем метод getClass , который возвращает представление класса во время выполнения объекта. Возвращенный объект класса предоставляет методы для доступа к информации о классе.

3. Простой пример

Чтобы намочить ноги, мы рассмотрим очень простой пример, который проверяет поля простого объекта Java во время выполнения.

Давайте создадим простой класс Person с полями только имени и возраста и вообще без методов.

Вот класс Person:

public class Person {
private String name;
private int age;
}

Теперь мы будем использовать отражение Java, чтобы обнаружить имена всех полей этого класса.

Чтобы оценить силу отражения, давайте создадим объект Person и будем использовать Object в качестве ссылочного типа:

@Test
public void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() {
Object person = new Person();
Field[] fields = person.getClass().getDeclaredFields();

List<String> actualFieldNames = getFieldNames(fields);

assertTrue(Arrays.asList("name", "age")
.containsAll(actualFieldNames));
}

Этот тест показывает нам, что мы можем получить массив объектов Field из нашего объекта person , даже если ссылка на объект является родительским типом этого объекта.

В приведенном выше примере нас интересовали только имена этих полей. Но можно сделать гораздо больше, и мы увидим примеры этого в следующих разделах.

Обратите внимание, как мы используем вспомогательный метод для извлечения фактических имен полей.

Это очень простой код:

private static List<String> getFieldNames(Field[] fields) {
List<String> fieldNames = new ArrayList<>();
for (Field field : fields)
fieldNames.add(field.getName());
return fieldNames;
}

4. Варианты использования отражения Java

Прежде чем мы перейдем к различным функциям отражения в Java, мы обсудим некоторые распространенные способы его использования. Рефлексия Java чрезвычайно эффективна и может оказаться очень удобной во многих отношениях.

Например, во многих случаях у нас есть соглашение об именах для таблиц базы данных. Мы можем добавить согласованности, добавив к именам наших таблиц префикс tbl_ , чтобы таблица с данными об учениках называлась tbl_student_data .

В таких случаях мы могли бы назвать объект Java, содержащий данные о студентах, как Student или StudentData . Затем, используя парадигму CRUD, у нас есть одна точка входа для каждой операции, поэтому операции Create получают только параметр Object .

Затем мы используем отражение для получения имени объекта и имен полей. На этом этапе мы можем сопоставить эти данные с таблицей БД и присвоить значения полей объекта соответствующим именам полей БД.

5. Проверка классов Java

В этом разделе мы рассмотрим наиболее фундаментальный компонент Java Reflection API. Объекты класса Java, как мы упоминали ранее, дают нам доступ к внутренним деталям любого объекта.

Мы собираемся изучить внутренние детали, такие как имя класса объекта, модификаторы, поля, методы, реализованные интерфейсы и т. д.

5.1. Готовиться

Чтобы получить четкое представление об API отражения применительно к классам Java и получить разнообразные примеры, давайте создадим абстрактный класс Animal , который реализует интерфейс Eating . Этот интерфейс определяет пищевое поведение любого конкретного объекта Animal , который мы создаем.

Во-первых, вот интерфейс Eating :

public interface Eating {
String eats();
}

А вот конкретная реализация Animal интерфейса Eating :

public abstract class Animal implements Eating {

public static String CATEGORY = "domestic";
private String name;

protected abstract String getSound();

// constructor, standard getters and setters omitted
}

Давайте также создадим еще один интерфейс под названием Locomotion , который описывает, как движется животное:

public interface Locomotion {
String getLocomotion();
}

Теперь мы создадим конкретный класс под названием Goat , который расширяет Animal и реализует Locomotion .

Поскольку суперкласс реализует Eating , Goat также должен будет реализовать методы этого интерфейса:

public class Goat extends Animal implements Locomotion {

@Override
protected String getSound() {
return "bleat";
}

@Override
public String getLocomotion() {
return "walks";
}

@Override
public String eats() {
return "grass";
}

// constructor omitted
}

С этого момента мы будем использовать отражение Java для проверки аспектов объектов Java, которые появляются в классах и интерфейсах выше.

5.2. Имена классов

Начнем с получения имени объекта из класса :

@Test
public void givenObject_whenGetsClassName_thenCorrect() {
Object goat = new Goat("goat");
Class<?> clazz = goat.getClass();

assertEquals("Goat", clazz.getSimpleName());
assertEquals("com.foreach.reflection.Goat", clazz.getName());
assertEquals("com.foreach.reflection.Goat", clazz.getCanonicalName());
}

Обратите внимание, что метод getSimpleName класса Class возвращает базовое имя объекта, как оно будет отображаться в его объявлении. Затем два других метода возвращают полное имя класса, включая объявление пакета.

Давайте также посмотрим, как мы можем создать объект класса Goat , если мы знаем только его полное имя класса:

@Test
public void givenClassName_whenCreatesObject_thenCorrect(){
Class<?> clazz = Class.forName("com.foreach.reflection.Goat");

assertEquals("Goat", clazz.getSimpleName());
assertEquals("com.foreach.reflection.Goat", clazz.getName());
assertEquals("com.foreach.reflection.Goat", clazz.getCanonicalName());
}

Обратите внимание, что имя, которое мы передаем статическому методу forName , должно включать информацию о пакете. В противном случае мы получим ClassNotFoundException .

5.3. Модификаторы класса

Мы можем определить модификаторы, используемые в классе, вызвав метод getModifiers , который возвращает Integer . Каждый модификатор представляет собой бит флага, который либо устанавливается, либо сбрасывается.

Класс java.lang.reflect.Modifier предлагает статические методы, которые анализируют возвращенное целое число на наличие или отсутствие определенного модификатора.

Давайте подтвердим модификаторы некоторых классов, которые мы определили выше:

@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
Class<?> goatClass = Class.forName("com.foreach.reflection.Goat");
Class<?> animalClass = Class.forName("com.foreach.reflection.Animal");

int goatMods = goatClass.getModifiers();
int animalMods = animalClass.getModifiers();

assertTrue(Modifier.isPublic(goatMods));
assertTrue(Modifier.isAbstract(animalMods));
assertTrue(Modifier.isPublic(animalMods));
}

Мы можем проверять модификаторы любого класса, расположенные в библиотечном банке, который мы импортируем в наш проект.

В большинстве случаев нам может понадобиться использовать подход forName , а не полномасштабное создание экземпляров, поскольку это было бы дорогостоящим процессом в случае классов с большим объемом памяти.

5.4. Информация о пакете

Используя отражение Java, мы также можем получить информацию о пакете любого класса или объекта. Эти данные объединяются внутри класса Package , который возвращается вызовом метода getPackage для объекта класса.

Давайте запустим тест, чтобы получить имя пакета:

@Test
public void givenClass_whenGetsPackageInfo_thenCorrect() {
Goat goat = new Goat("goat");
Class<?> goatClass = goat.getClass();
Package pkg = goatClass.getPackage();

assertEquals("com.foreach.reflection", pkg.getName());
}

5.5. Суперкласс

Мы также можем получить суперкласс любого класса Java, используя отражение Java.

Во многих случаях, особенно при использовании библиотечных классов или встроенных классов Java, мы можем заранее не знать суперкласс используемого объекта. В этом подразделе показано, как получить эту информацию.

Давайте продолжим и определим надкласс Goat .

Кроме того, мы также показываем, что класс java.lang.String является подклассом класса java.lang.Object :

@Test
public void givenClass_whenGetsSuperClass_thenCorrect() {
Goat goat = new Goat("goat");
String str = "any string";

Class<?> goatClass = goat.getClass();
Class<?> goatSuperClass = goatClass.getSuperclass();

assertEquals("Animal", goatSuperClass.getSimpleName());
assertEquals("Object", str.getClass().getSuperclass().getSimpleName());
}

5.6. Реализованные интерфейсы

Используя отражение Java, мы также можем получить список интерфейсов, реализованных данным классом.

Давайте получим типы классов интерфейсов, реализованных классом Goat и абстрактным классом Animal :

@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
Class<?> goatClass = Class.forName("com.foreach.reflection.Goat");
Class<?> animalClass = Class.forName("com.foreach.reflection.Animal");

Class<?>[] goatInterfaces = goatClass.getInterfaces();
Class<?>[] animalInterfaces = animalClass.getInterfaces();

assertEquals(1, goatInterfaces.length);
assertEquals(1, animalInterfaces.length);
assertEquals("Locomotion", goatInterfaces[0].getSimpleName());
assertEquals("Eating", animalInterfaces[0].getSimpleName());
}

Обратите внимание на утверждения, что каждый класс реализует только один интерфейс. Изучая имена этих интерфейсов, мы обнаруживаем, что Goat реализует Locomotion , а Animal реализует Eating , точно так же, как это появляется в нашем коде.

Мы видим, что Goat является подклассом абстрактного класса Animal и реализует метод интерфейса eats() . Затем Goat также реализует интерфейс Eating .

Поэтому стоит отметить, что в возвращаемом массиве отображаются только те интерфейсы, которые класс явно объявляет реализованными с помощью ключевого слова Implements .

Таким образом, даже если класс реализует методы интерфейса, потому что его суперкласс реализует этот интерфейс, но подкласс не объявляет этот интерфейс напрямую с помощью ключевого слова Implements, этот интерфейс не будет отображаться в массиве интерфейсов.

5.7. Конструкторы, методы и поля

С отражением Java мы можем проверять конструкторы любого класса объекта, а также методы и поля.

Позже мы сможем увидеть более глубокие проверки каждого из этих компонентов класса. Но пока достаточно просто получить их имена и сравнить их с тем, что мы ожидаем.

Давайте посмотрим, как получить конструктор класса Goat :

@Test
public void givenClass_whenGetsConstructor_thenCorrect(){
Class<?> goatClass = Class.forName("com.foreach.reflection.Goat");

Constructor<?>[] constructors = goatClass.getConstructors();

assertEquals(1, constructors.length);
assertEquals("com.foreach.reflection.Goat", constructors[0].getName());
}

Мы также можем проверить поля класса Animal :

@Test
public void givenClass_whenGetsFields_thenCorrect(){
Class<?> animalClass = Class.forName("com.foreach.reflection.Animal");
Field[] fields = animalClass.getDeclaredFields();

List<String> actualFields = getFieldNames(fields);

assertEquals(2, actualFields.size());
assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
}

И мы можем аналогичным образом проверить методы класса Animal :

@Test
public void givenClass_whenGetsMethods_thenCorrect(){
Class<?> animalClass = Class.forName("com.foreach.reflection.Animal");
Method[] methods = animalClass.getDeclaredMethods();
List<String> actualMethods = getMethodNames(methods);

assertEquals(4, actualMethods.size());
assertTrue(actualMethods.containsAll(Arrays.asList("getName",
"setName", "getSound")));
}

Как и в случае с getFieldNames , мы добавили вспомогательный метод для извлечения имен методов из массива объектов Method :

private static List<String> getMethodNames(Method[] methods) {
List<String> methodNames = new ArrayList<>();
for (Method method : methods)
methodNames.add(method.getName());
return methodNames;
}

6. Проверка конструкторов

С помощью отражения Java мы можем проверять конструкторы любого класса и даже создавать объекты класса во время выполнения. Это стало возможным благодаря классу java.lang.reflect.Constructor .

Ранее мы только рассмотрели, как получить массив объектов Constructor , из которого мы смогли получить имена конструкторов.

В этом разделе мы сосредоточимся на том, как получить определенные конструкторы.

В Java, как мы знаем, нет двух конструкторов класса с одинаковой сигнатурой метода. Итак, мы будем использовать эту уникальность, чтобы получить один конструктор из многих.

Чтобы оценить особенности этого класса, мы создадим подкласс Bird класса Animal с тремя конструкторами.

Мы не будем реализовывать Locomotion , чтобы можно было указать это поведение с помощью аргумента конструктора, чтобы добавить еще больше разнообразия:

public class Bird extends Animal {
private boolean walks;

public Bird() {
super("bird");
}

public Bird(String name, boolean walks) {
super(name);
setWalks(walks);
}

public Bird(String name) {
super(name);
}

public boolean walks() {
return walks;
}

// standard setters and overridden methods
}

Давайте подтвердим с помощью отражения, что этот класс имеет три конструктора:

@Test
public void givenClass_whenGetsAllConstructors_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Constructor<?>[] constructors = birdClass.getConstructors();

assertEquals(3, constructors.length);
}

Далее мы получим каждый конструктор для класса Bird , передав типы классов параметров конструктора в объявленном порядке:

@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");

Constructor<?> cons1 = birdClass.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
Constructor<?> cons3 = birdClass.getConstructor(String.class, boolean.class);
}

Нет необходимости в утверждении, так как мы получим исключение NoSuchMethodException , и тест автоматически завершится ошибкой, если конструктор с заданными типами параметров в заданном порядке не существует.

В последнем тесте мы увидим, как создавать экземпляры объектов во время выполнения, передавая их параметры:

@Test
public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Constructor<?> cons1 = birdClass.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
Constructor<?> cons3 = birdClass.getConstructor(String.class,
boolean.class);

Bird bird1 = (Bird) cons1.newInstance();
Bird bird2 = (Bird) cons2.newInstance("Weaver bird");
Bird bird3 = (Bird) cons3.newInstance("dove", true);

assertEquals("bird", bird1.getName());
assertEquals("Weaver bird", bird2.getName());
assertEquals("dove", bird3.getName());

assertFalse(bird1.walks());
assertTrue(bird3.walks());
}

Мы создаем экземпляры объектов класса, вызывая метод newInstance класса Constructor и передавая необходимые параметры в объявленном порядке. Затем мы приводим результат к требуемому типу.

Также можно вызвать конструктор по умолчанию с помощью метода Class.newInstance() . Однако этот метод устарел, начиная с Java 9, и мы не должны использовать его в современных проектах Java.

Для bird1 мы используем конструктор по умолчанию, который автоматически устанавливает имя bird из нашего кода Bird , и мы подтверждаем это тестом.

Затем мы создаем экземпляр bird2 только с именем и тестом. Помните, что когда мы не устанавливаем поведение передвижения, оно по умолчанию принимает значение false, как видно из двух последних утверждений.

7. Проверка полей

Раньше мы проверяли только имена полей. В этом разделе мы покажем, как получить и установить их значения во время выполнения.

Есть два основных метода, используемых для проверки полей класса во время выполнения: getFields() и getField(fieldName) .

Метод getFields() возвращает все доступные общедоступные поля рассматриваемого класса. Он вернет все общедоступные поля как в классе, так и во всех суперклассах.

Например, когда мы вызываем этот метод для класса Bird , мы получаем только поле CATEGORY его суперкласса Animal , поскольку сам Bird не объявляет никаких общедоступных полей:

@Test
public void givenClass_whenGetsPublicFields_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Field[] fields = birdClass.getFields();

assertEquals(1, fields.length);
assertEquals("CATEGORY", fields[0].getName());
}

У этого метода также есть вариант под названием getField , который возвращает только один объект Field , беря имя поля:

@Test
public void givenClass_whenGetsPublicFieldByName_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Field field = birdClass.getField("CATEGORY");

assertEquals("CATEGORY", field.getName());
}

Мы не можем получить доступ к закрытым полям, объявленным в суперклассах и не объявленным в дочернем классе. Вот почему мы не можем получить доступ к полю имени .

Однако мы можем проверить закрытые поля, объявленные в классе, с которым мы имеем дело, вызвав метод getDeclaredFields :

@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect(){
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Field[] fields = birdClass.getDeclaredFields();

assertEquals(1, fields.length);
assertEquals("walks", fields[0].getName());
}

Мы также можем использовать другой его вариант, если мы знаем имя поля:

@Test
public void givenClass_whenGetsFieldsByName_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Field field = birdClass.getDeclaredField("walks");

assertEquals("walks", field.getName());
}

Если мы получим неправильное имя поля или введем несуществующее поле, мы получим NoSuchFieldException .

Теперь получим тип поля:

@Test
public void givenClassField_whenGetsType_thenCorrect() {
Field field = Class.forName("com.foreach.reflection.Bird")
.getDeclaredField("walks");
Class<?> fieldClass = field.getType();

assertEquals("boolean", fieldClass.getSimpleName());
}

Далее давайте посмотрим, как получить доступ к значениям полей и изменить их.

Чтобы получить значение поля, не говоря уже о его установке, мы должны сначала установить его доступность, вызвав метод setAccessible для объекта Field и передав ему логическое значение true :

@Test
public void givenClassField_whenSetsAndGetsValue_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Bird bird = (Bird) birdClass.getConstructor().newInstance();
Field field = birdClass.getDeclaredField("walks");
field.setAccessible(true);

assertFalse(field.getBoolean(bird));
assertFalse(bird.walks());

field.set(bird, true);

assertTrue(field.getBoolean(bird));
assertTrue(bird.walks());
}

В приведенном выше тесте мы убеждаемся, что значение поля walks действительно ложно, прежде чем установить для него значение true.

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

Важно отметить одну важную вещь об объектах Field : когда они объявлены как public static , нам не нужен экземпляр класса, содержащего их.

Мы можем просто передать null вместо него и получить значение поля по умолчанию:

@Test
public void givenClassField_whenGetsAndSetsWithNull_thenCorrect(){
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Field field = birdClass.getField("CATEGORY");
field.setAccessible(true);

assertEquals("domestic", field.get(null));
}

8. Методы проверки

В предыдущем примере мы использовали отражение только для проверки имен методов. Однако отражение Java более мощное, чем это.

С помощью отражения Java мы можем вызывать методы во время выполнения и передавать им необходимые параметры, как мы это делали для конструкторов. Точно так же мы также можем вызывать перегруженные методы, указав типы параметров каждого из них.

Как и в случае с полями, есть два основных метода, которые мы используем для получения методов класса. Метод getMethods возвращает массив всех общедоступных методов класса и суперклассов.

Это означает, что с помощью этого метода мы можем получить общедоступные методы класса java.lang.Object , такие как toString , hashCode и notifyAll :

@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Method[] methods = birdClass.getMethods();
List<String> methodNames = getMethodNames(methods);

assertTrue(methodNames.containsAll(Arrays
.asList("equals", "notifyAll", "hashCode",
"walks", "eats", "toString")));
}

Чтобы получить только публичные методы интересующего нас класса, мы должны использовать метод getDeclaredMethods :

@Test
public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect(){
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
List<String> actualMethodNames
= getMethodNames(birdClass.getDeclaredMethods());

List<String> expectedMethodNames = Arrays
.asList("setWalks", "walks", "getSound", "eats");

assertEquals(expectedMethodNames.size(), actualMethodNames.size());
assertTrue(expectedMethodNames.containsAll(actualMethodNames));
assertTrue(actualMethodNames.containsAll(expectedMethodNames));
}

Каждый из этих методов имеет единственную вариацию, которая возвращает один объект Method , имя которого нам известно:

@Test
public void givenMethodName_whenGetsMethod_thenCorrect() throws Exception {
Bird bird = new Bird();
Method walksMethod = bird.getClass().getDeclaredMethod("walks");
Method setWalksMethod = bird.getClass().getDeclaredMethod("setWalks", boolean.class);

assertTrue(walksMethod.canAccess(bird));
assertTrue(setWalksMethod.canAccess(bird));
}

Обратите внимание, как мы извлекаем отдельные методы и указываем, какие типы параметров они принимают. Те, которые не принимают типы параметров, извлекаются с пустым переменным аргументом, оставляя нам только один аргумент — имя метода.

Далее мы покажем, как вызывать метод во время выполнения.

Мы знаем, что по умолчанию атрибут walks класса Bird имеет значение false .

Мы хотим вызвать его метод setWalks и установить для него значение true :

@Test
public void givenMethod_whenInvokes_thenCorrect() {
Class<?> birdClass = Class.forName("com.foreach.reflection.Bird");
Bird bird = (Bird) birdClass.getConstructor().newInstance();
Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
Method walksMethod = birdClass.getDeclaredMethod("walks");
boolean walks = (boolean) walksMethod.invoke(bird);

assertFalse(walks);
assertFalse(bird.walks());

setWalksMethod.invoke(bird, true);

boolean walks2 = (boolean) walksMethod.invoke(bird);
assertTrue(walks2);
assertTrue(bird.walks());
}

Обратите внимание, как мы сначала вызываем метод walks и приводим возвращаемый тип к соответствующему типу данных, а затем проверяем его значение. Позже мы вызываем метод setWalks , чтобы изменить это значение и повторить проверку.

9. Заключение

В этой статье мы рассмотрели Java Reflection API и рассмотрели, как использовать его для проверки классов, интерфейсов, полей и методов во время выполнения без предварительного знания их внутреннего устройства во время компиляции.

Полный исходный код и примеры для этой статьи можно найти на GitHub .