Рубрики
Без рубрики

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

Простое и практическое руководство по Java Reflection API

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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

import java.lang.reflect.*;

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

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

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

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

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

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

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

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

    List actualFieldNames = getFieldNames(fields);

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

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

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

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

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

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

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

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

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

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

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

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

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

5.1. Подготовка

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

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

public interface Eating {
    String eats();
}

а потом бетон Животное реализация интерфейса Еда :

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.baeldung.reflection.Goat", clazz.getName());
    assertEquals("com.baeldung.reflection.Goat", clazz.getCanonicalName());
}

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

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

@Test
public void givenClassName_whenCreatesObject_thenCorrect(){
    Class clazz = Class.forName("com.baeldung.reflection.Goat");

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

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

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

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

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

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

@Test
public void givenClass_whenRecognisesModifiers_thenCorrect() {
    Class goatClass = Class.forName("com.baeldung.reflection.Goat");
    Class animalClass = Class.forName("com.baeldung.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 , который возвращается вызовом метода get Package для объекта класса.

Давайте запустим тест для получения имени пакета:

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

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

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

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

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

Итак, давайте продолжим и определим суперкласс Goat . Кроме того, мы также покажем, что java.lang.String class-это подкласс java.lang.Объект класс:

@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.baeldung.reflection.Goat");
    Class animalClass = Class.forName("com.baeldung.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 является подклассом абстрактного класса Animal и реализует метод интерфейса eats () , затем Goat также реализует интерфейс Eating .

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

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

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

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

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

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

@Test
public void givenClass_whenGetsConstructor_thenCorrect(){
    Class goatClass = Class.forName("com.baeldung.reflection.Goat");

    Constructor[] constructors = goatClass.getConstructors();

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

Мы также можем проверить поля класса Animal следующим образом:

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

    List 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.baeldung.reflection.Animal");
    Method[] methods = animalClass.getDeclaredMethods();
    List actualMethods = getMethodNames(methods);

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

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

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

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

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

Ранее мы только рассмотрели, как получить массив объектов 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.baeldung.reflection.Bird");
    Constructor[] constructors = birdClass.getConstructors();

    assertEquals(3, constructors.length);
}

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

@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
    Class birdClass = Class.forName("com.baeldung.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.baeldung.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.

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

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

7. Осмотр Полей

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

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

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

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

@Test
public void givenClass_whenGetsPublicFields_thenCorrect() {
    Class birdClass = Class.forName("com.baeldung.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.baeldung.reflection.Bird");
    Field field = birdClass.getField("CATEGORY");

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

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

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

@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect(){
    Class birdClass = Class.forName("com.baeldung.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.baeldung.reflection.Bird");
    Field field = birdClass.getDeclaredField("walks");

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

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

Мы получаем тип поля следующим образом:

@Test
public void givenClassField_whenGetsType_thenCorrect() {
    Field field = Class.forName("com.baeldung.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.baeldung.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 objects, заключается в том, что когда он объявлен как public static , нам не нужен экземпляр класса, содержащего их, мы можем просто передать null на его место и все равно получить значение поля по умолчанию, например:

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

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

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

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

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

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

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

@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
    Class birdClass = Class.forName("com.baeldung.reflection.Bird");
    Method[] methods = birdClass.getMethods();
    List 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.baeldung.reflection.Bird");
    List actualMethodNames
      = getMethodNames(birdClass.getDeclaredMethods());

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

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

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

@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 , мы хотим вызвать его метод set Walks и установить для него значение true :

@Test
public void givenMethod_whenInvokes_thenCorrect() {
    Class birdClass = Class.forName("com.baeldung.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 и приводим возвращаемый тип к соответствующему типу данных, а затем проверяем его значение. Затем мы вызываем метод set/|, чтобы изменить это значение и снова протестировать.

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

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

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