Для чего нужен Reflection API?

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

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

Давай соберем все возможности использования рефлексии в один список:

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

Рефлексия используется практически во всех современных технологиях Java и лежит в основе большинства современных Java/Java EE фреймворков и библиотек, например, в:

  • Spring — фреймворков для создания веб-приложений
  • JUnit — фреймворк для тестирования

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

Но всегда есть свои плюсы и минусы. Поэтому немного о минусах:

  • Нарушения безопасности приложения. С помощью рефлексии мы можем получить доступ к части кода, к которой не должны были (нарушение инкапсуляции).
  • Ограничения системы безопасности. Рефлексия требует разрешения времени выполнения, недоступные для систем под управлением менеджера безопасности.
  • Низкая производительность. Рефлексия в Java определяет типы динамически, сканируя classpath, чтобы найти класс для загрузки. Это снижает производительность программы.
  • Сложность в поддержке. Код, написанный с помощью рефлексии, трудно читать и отлаживать. Он становится менее гибким и его сложнее поддерживать.

Работа с классами с помощью Reflection API

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

Давай посмотрим на список основных методов для работы с java.lang.Class:

Метод Дейстиве
String getName(); Возвращает название класса
int getModifiers(); Возвращает модификаторы доступа
Package getPackage(); Возвращает информацию о пакете
Class getSuperclass(); Возвращает информацию о классе-родителе
Class[] getInterfaces(); Возвращает массив интерфейсов
Constructor[] getConstructors(); Возвращает информацию о конструкторах класса
Fields[] getFields(); Возвращает поля класса
Files getFiled(String fieldName); Возвращает определенное поле класса по имени
Method[] getMethods(); Возвращает массив методов

Это основные методы для получения данных о классе и интерфейсах, полях и методах. Также есть методы, которые позволяют получать или устанавливать значения полей, дают доступ к private полям класса. Рассмотрим их чуть позже.

Сейчас мы поговорим о получении самого java.lang.Class. Для это у нас есть три способа.

1. С помощью Class.forName

В работающем приложении для получения класса необходимо использовать метод forName (String className).

Этот код демонстрирует возможность создавать классы с использованием Reflection. Создадим класс Person, с которым будем работать:


package com.company;

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

И вторая часть нашего примера — это код с рефлексией:


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Такой подход возможен в случае, если известно полное имя класса. Тогда можно получить соответствующий класс с помощью статического метода Class.forName(). Этот способ нельзя использовать для примитивных типов.

2. С помощью .class

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


Class aClass = Person.class;

3. C помощью .getClass()

Если экземпляр объекта доступен, то самый простой способ получить его класс — вызвать object.getClass().


Person person = new Person();
Class aClass = person.getClass();

В чем же отличие между двумя последними подходами?

Используй A.class, если заранее, при написании кода знаешь, какой объект класса тебя интересует. Если экземпляра нет, тогда нужно использовать .class.

Получение методов класса

Рассмотрим методы, которые возвращают методы нашего класса: getDeclaredMethods() и getMethods().

getDeclaredMethods() возвращает массив, который содержит объекты типа Method, отражающие все объявленные методы класса или интерфейса, представленного этим объектом класса, включая публичные, приватные, по умолчанию и protected методы, но исключая унаследованные методы.

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

Давай рассмотрим работу каждого из них.

Начнем с getDeclaredMethods(). Ниже мы будем работать с абстрактным классом Numbers, который поможет нам еще раз понять разницу между двумя методами. Напишем статический метод, который будет преобразовывать наш массив Method в List<String>:


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

Результат работы кода выглядит вот так:

byteValue
shortValue
intValue
longValue
floatValue
doubleValue

Это и есть методы, которые объявлены внутри класса Number. А что вернет нам getMethods()? Изменим две строки в примере:


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

И в результате мы увидим вот такой набор методов:

byteValue
shortValue
intValue
longValue
floatValue
doubleValue
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll

Так как все классы наследуются от Object, наш метод вернул нам еще и публичные методы класса Object.

Получение полей класса

Методы getFields и getDeclaredFields используются для получения полей класса. Посмотрим на примере класса LocalDateTime. Перепишем наш код:


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

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

MIN
MAX
serialVersionUID
date
time

По аналогии с методами посмотрим, что будет, если немного изменить код:


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

Вывод программы:

MIN
MAX

Теперь давай разберем, в чем разница между нашими методами.

Метод getDeclaredFields возвращает массив объектов Field, отражающих все поля, объявленные классом или интерфейсом, представленным этим объектом Class.

Метод getFields возвращает массив объектов Field отражающие все общедоступные (public) поля класса или интерфейса, представленного этим объектом Class.

А теперь заглянем внутрь нашего LocalDateTime.

Поля класса MIN и MAX являются общедоступными, значит, они будут видны через метод getFields. В свою очередь поля date, time, serialVersionUID имеют модификатор private, а значит не будут видны через метод getFields, и получить их мы можем с помощью getDeclaredFields. Таким образом мы можем получить доступ к полям (Field) для private полей.

Другие методы и их описание

Настало время поговорить о некоторых методы нашего класса Class, а именно:

Метод Действие
getModifiers Получение модификаторов нашего класса
getPackage Получение пакета, в котором лежит наш класс
getSuperclass Получение суперкласса
getInterfaces Получение массива интерфейсов, которые имплементируют класс
getName Получение полного имени класса
getSimpleName Получение названия класса

getModifiers()

Доступ к модификаторам можно получить с помощью Class объекта.

Модификаторы представляют собой ключевые слова public, static, interface и т. д. Получаем модификаторы с помощью метода getModifiers():


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

Результат выполнения находится в переменной int, где каждый модификатор — это битовый флаг, который может быть установлен или сброшен. Мы можем проверить модификаторы, используя методы в классе java.lang.reflect.Modifier:


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Вспомни, как выглядит объявление нашего класса Person:


public class Person {
   …
}

В результате получаем вот такой вывод на экран:

Class modifiers: 1
Is public: true
Is static: false
Is final: false
Is abstract: false
Is interface: false

Если мы сделаем наш класс абстрактным, то получим такой результат:


public abstract class Person { … }

и такой вывод:

Class modifiers: 1025
Is public: true
Is static: false
Is final: false
Is abstract: true
Is interface: false

У нас изменился модификатор доступа, а значит и данные, которые будут возвращаться через наши статические методы класса Modifier.

getPackage()

Зная только класс, мы можем получить информацию о пакете:


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

Имея доступ к объекту класса, можем получить доступ к его суперклассу:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

В результате получим всем известный класс Object:


class java.lang.Object

А вот если у нашего класса будет класс-родитель, то мы увидим именно его:


package com.company;

class Human {
    // some info
}

public class Person extends Human {
    private int age;
    private String name;

    // some info
}

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


class com.company.Human

getInterfaces()

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


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

И не забудем модифицировать наш класс Person:


public class Person implements Serializable { … }

Вывод:

[interface java.io.Serializable]

Класс может реализовать много интерфейсов. Поэтому возвращается массив объектов Class. В Java Reflection API интерфейсы также представлены объектами типа Class.

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

getName() & getSimpleName() & getCanonicalName()

Напишем пример для примитива, вложенного класса, аннонимного класса и класса String:


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

Результат нашей программы:

int class (primitive):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (ordinary class):
getName()): java.lang.String
getCanonicalName()): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (nested class):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName()): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (anonymous inner class):
getName()): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Теперь давай разберем результаты нашей программы:

  • getName() возвращает имя сущности.

  • getCanonicalName() возвращает каноническое имя базового класса, как определено спецификацией языка Java. Возвращает null, если базовый класс не имеет канонического имени (то есть если это локальный или анонимный класс или массив, тип компонента которого не имеет канонического имени).

  • getSimpleName() возвращает простое имя базового класса, как указано в исходном коде. Возвращает пустую строку, если базовый класс является анонимным.

  • getTypeName() возвращает информативную строку для имени этого типа.