Для чого потрібен 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. За допомогою .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() повертає інформативний рядок для імені цього типу.