Руководство по Java Reflection

Руководство по Java Reflection

1. обзор

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

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

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

To use java reflection, we do not need to include any special jars, любая особая конфигурация или зависимости Maven. JDK поставляется с группой классов, которые включены в пакетjava.lang.reflect специально для этой цели.

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

import java.lang.reflect.*;

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

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

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

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

Давайте создадим простой классPerson только с полямиname иage и без каких-либо методов. Вот класс 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 actualFieldNames = getFieldNames(fields);

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

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

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

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

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

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

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

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

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

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

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

В этом разделе мы рассмотрим наиболее фундаментальный компонент API отражения java. Объекты класса 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. Имена классов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В большинстве случаев нам может потребоваться использовать подход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.example.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, мы также можемget the list of interfaces implemented by a given class.

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

@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect(){
    Class goatClass = Class.forName("com.example.reflection.Goat");
    Class animalClass = Class.forName("com.example.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.example.reflection.Goat");

    Constructor[] constructors = goatClass.getConstructors();

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

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

@Test
public void givenClass_whenGetsFields_thenCorrect(){
    Class animalClass = Class.forName("com.example.java.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.example.java.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 мы можемinspect constructors любого класса и дажеcreate class objects at runtime. Это стало возможным благодаря классу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.example.reflection.Bird");
    Constructor[] constructors = birdClass.getConstructors();

    assertEquals(3, constructors.length);
}

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

@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect(){
    Class birdClass = Class.forName("com.example.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.example.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 и передавая необходимые параметры в объявленном порядке. Затем мы приводим результат к требуемому типу.

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

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

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

Раньше мы проверяли только имена полей, в этом разделеwe will show how toget and set their values at runtime.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect(){
    Class birdClass = Class.forName("com.example.java.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.example.java.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));
}

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

@Test
public void givenMethodName_whenGetsMethod_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Method walksMethod = birdClass.getDeclaredMethod("walks");
    Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);

    assertFalse(walksMethod.isAccessible());
    assertFalse(setWalksMethod.isAccessible());

    walksMethod.setAccessible(true);
    setWalksMethod.setAccessible(true);

    assertTrue(walksMethod.isAccessible());
    assertTrue(setWalksMethod.isAccessible());
}

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

Далее мы покажем, как вызывать метод во время выполнения. По умолчанию мы знаем, что атрибутwalks классаBird равенfalse, мы хотим вызвать его методsetWalks и установить для него значениеtrue:

@Test
public void givenMethod_whenInvokes_thenCorrect() {
    Class birdClass = Class.forName("com.example.reflection.Bird");
    Bird bird = (Bird) birdClass.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. Заключение

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

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