Поняття «рефлексія» ти міг зустрічати у звичайному житті. Зазвичай цим словом називають процес дослідження себе. У програмуванні воно має схожий сенс — це механізм дослідження даних про програму, а також зміни структури та поведінки програми під час її виконання. Тут важливо: саме під час виконання, а не під час компіляції. А навіщо досліджувати код під час виконання? Ти ж і так його бачиш :/Приклади використання Reflection - 1Ідея рефлексії може бути не одразу зрозумілою з однієї причини: до цього моменту ти завжди знав класи, з якими працюєш. Ну, наприклад, ти міг написати клас Cat:

package learn.javarush;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

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

   public int getAge() {
       return age;
   }

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

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Ти все знаєш про нього, бачиш, які у нього є поля і методи. Напевно, зможеш створити для зручності систему наслідування із загальним класом Animal, якщо раптом у програмі потрібні будуть інші класи тварин. Раніше ми навіть створювали клас ветеринарної клініки, у який можна було передати об'єкт-батько Animal, а програма лікувала тварину залежно від того, собака це чи кішка. Попри те, що ці завдання не дуже-то прості, програма дізнається всю необхідну їй інформацію про класи під час компіляції. Тому коли ти в методі main() передаєш об'єкт Cat у методи класу ветеринарної клініки, програма вже знає, що це кішка, а не собака. А тепер давай уявимо, що перед нами стоїть інше завдання. Наша мета — написати аналізатор коду. Потрібно створити клас CodeAnalyzer з єдиним методом — void analyzeClass(Object o). Цей метод має:
  • визначити, якого класу об'єкт йому передали і вивести назву класу в консоль;
  • визначити назви всіх полів цього класу, включаючи приватні, і вивести їх у консоль;
  • визначити назви всіх методів цього класу, включаючи приватні, і вивести їх у консоль.
Виглядати це буде приблизно так:

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
      
       //Вивести назву класу, до якого належить об'єкт o
       //Вивести назви всіх змінних цього класу
       //Вивести назви всіх методів цього класу
   }
  
}
Тепер різниця між цим завданням та іншими завданнями, які ти вирішував до цього, помітна. У цьому випадку складність полягає в тому, що ні ти, ні програма не знаєте, що саме передасться у метод analyzeClass(). Ти напишеш програму, нею почнуть користуватися інші програмісти, які можуть передати у цей метод взагалі що завгодно — будь-який стандартний Java-клас чи будь-який написаний ними клас. У цього класу може бути скільки завгодно змінних і методів. Іншими словами, у цьому разі ми (і наша програма) не маємо найменшого уявлення про те, з якими класами ми будемо працювати. І тим не менш, ми маємо вирішити це завдання. І тут нам на допомогу приходить стандартна бібліотека Java — Java Reflection API. Reflection API — потужний інструмент мови. В офіційній документації Oracle написано, що цей механізм рекомендують використовувати лише досвідченим програмістам, які дуже добре розуміють, що роблять. Скоро ти зрозумієш, чому нам заздалегідь дають такі застереження :) Ось список того, що можна зробити за допомогою Reflection API:
  1. Дізнатися / визначити клас об'єкта.
  2. Отримати інформацію про модифікатори класу, поля, методи, константи, конструктори і суперкласи.
  3. Дізнатися, які методи належать реалізованому інтерфейсу / інтерфейсам.
  4. Створити екземпляр класу, коли ім'я класу невідоме до моменту виконання програми.
  5. Отримати і встановити значення поля об'єкта за назвою.
  6. Викликати метод об'єкта за назвою.
Вражаючий список, правда? :) Зверни увагу: все це механізм рефлексії здатний робити «на ходу» незалежно від того, об'єкт якого класу ми передамо в наш аналізатор коду! Давай розглянемо можливості Reflection API на прикладах.

Як дізнатися / визначити клас об'єкта

Почнемо з основ. Вхідна точка у механізм рефлексії Java — клас Class. Так, виглядає справді смішно, але на те вона й рефлексія :) За допомогою класу Class ми, насамперед, визначаємо клас будь-якого об'єкта, переданого у наш метод. Давай спробуємо це зробити:

import learn.javarush.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Вивід у консоль:

class learn.javarush.Cat
Зверни увагу на дві речі. По-перше, ми спеціально поклали клас Cat в окремий пакет learn.javarush; тепер ти бачиш, що getClass() повертає повне ім'я класу. По-друге, ми назвали нашу змінну clazz. Виглядає трохи дивно. Її, звісно, слід було б назвати «class», але «class» — зарезервоване слово у мові Java, і компілятор не дозволить так називати змінні. Довелося викручуватись :) Що ж, для початку непогано! Що у нас там було ще у списку можливостей?

Як отримати інформацію про модифікатори класу, поля, методи, константи, конструктори і суперкласи

Це вже цікавіше! У поточному класі у нас немає констант і батьківського класу. Давай додамо їх для повноти картини. Створимо найпростіший батьківський клас Animal:

package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
І додамо у наш клас Cat наслідування від Animal та одну константу:

package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Сімейство котячих";

   private String name;
   private int age;

   //...інша частина класу
}
Тепер у нас повний набір! Давай випробуємо можливості рефлексії :)

import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Назва класу: " + clazz);
       System.out.println("Поля класу: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Батьківський клас: " + clazz.getSuperclass());
       System.out.println("Методи класу: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструктори класу: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Ось що ми отримаємо у консолі:

Назва класу: class learn.javarush.Cat
Поля класу: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Батьківський клас: class learn.javarush.Animal
Методи класу: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструктори класу: [public learn.javarush.Cat(java.lang.String,int)]
Як багато детальної інформації про клас ми отримали! Причому не лише про публічні, але й про приватні частини. Зверни увагу: private-змінні теж відображені у списку. Власне, «аналіз» класу можна на цьому вважати завершеним: тепер за допомогою методу analyzeClass() ми дізнаємося все, що тільки можливо. Але це не всі можливості, які у нас є при роботі з рефлексією. Не будемо обмежуватися простим спостереженням і перейдемо до активних дій! :)

Як створити екземпляр класу, якщо ім'я класу невідоме до виконання програми

Почнемо з конструктора за замовчуванням. Його поки що немає в нашому класі Cat, тому давай додамо його:

public Cat() {
  
}
Ось як виглядатиме код для створення об'єкта Cat за допомогою рефлексії (метод createCat()):

import learn.javarush.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Вводимо в консоль:

learn.javarush.Cat
Вивід у консоль:

Cat{name='null', age=0}
Це не помилка: значення name і age відображаються в консолі через те, що ми запрограмували їх вивід у методі toString() класу Cat. Тут ми зчитуємо ім'я класу, об'єкт якого будемо створювати, з консолі. Запущена програма дізнається ім'я класу, об'єкт якого їй належить створити. Приклади використання Reflection - 3Для стислості ми опустили код правильної обробки винятків, щоб він не займав більше місця, ніж сам приклад. У реальній програмі, звичайно, обов'язково варто обробити ситуації введення некоректних імен тощо. Конструктор за замовчуванням — річ досить проста, тому створити екземпляр класу за його допомогою, як бачиш, нескладно :) А за допомогою методу newInstance() ми створюємо новий об'єкт цього класу. Інша справа, якщо конструктор класу Cat прийматиме на вхід параметри. Видалимо дефолтний конструктор із класу і спробуємо запустити наш код знову.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Щось пішло не так! Ми отримали помилку, тому що викликали метод для створення об'єкта через конструктор за замовчуванням. А такого конструктора у нас тепер немає. Значить, при роботі методу newInstance() механізм рефлексії використовуватиме наш старий конструктор з двома параметрами:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
А з параметрами ми нічого не зробили, наче взагалі забули про них! Щоб передати їх у конструктор за допомогою рефлексії, доведеться трохи «похімічити»:

import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Вивід у консоль:

Cat{name='Barsik', age=6}
Давай розглянемо детальніше, що у нашій програмі відбувається. Ми створили масив об'єктів Class.

Class[] catClassParams = {String.class, int.class};
Вони відповідають параметрам нашого конструктора (у нас там якраз параметри String та int). Ми передаємо їх у метод clazz.getConstructor() і отримуємо доступ до потрібного конструктора. Після цього залишається лише викликати метод newInstance() з потрібними параметрами і не забути явно привести об'єкт до потрібного нам класу — Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
В результаті наш об'єкт успішно створиться! Вивід у консоль:

Cat{name='Barsik', age=6}
Їдемо далі :)

Як отримати і встановити значення поля об'єкта за ім'ям

Уяви, що ти використовуєш клас, написаний іншим програмістом. При цьому ти не маєш можливості його редагувати. Наприклад, готову бібліотеку класів, упаковану в JAR. Прочитати код класів ти можеш, а от змінити — ні. Програміст, який створив клас у цій бібліотеці (нехай це буде наш старий клас Cat) не виспався перед фінальним проектуванням і видалив геттери і сеттери для поля age. Тепер цей клас потрапив до тебе. Він повністю відповідає твоїм запитам, адже тобі якраз потрібні в програмі об'єкти Cat. Але вони потрібні тобі з тим самим полем age! Це проблема: дістатися до поля ми не можемо, адже воно має модифікатор private, а геттери і сеттери видалив горе-розробник цього класу :/ Що ж, рефлексія здатна допомогти нам і в цій ситуації! Доступ до коду класу Cat у нас є: ми можемо хоча б дізнатися, які у нього є поля і як вони називаються. Озброївшись цією інформацією, вирішуємо нашу проблему:

import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //з полем name нам пощастило - для нього в класі є setter
           cat.setName("Barsik");

           Field age = clazz.getDeclaredField("age");
          
           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Як і сказано в коментарі, з полем name все просто: для нього розробники класу надали сеттер. Створювати об'єкти з конструкторів за замовчуванням ти теж вже вмієш: для цього є метод newInstance(). А от з другим полем доведеться повозитися. Давай розбиратися, що у нас тут відбувається :)

Field age = clazz.getDeclaredField("age");
Тут ми, використовуючи наш об'єкт Class clazz, отримуємо доступ до поля age за допомогою методу getDeclaredField(). Він дає нам можливість отримати поле age у вигляді об'єкта Field age. Але цього поки що недостатньо, адже private-полям не можна просто так присвоювати значення. Для цього потрібно зробити поле «доступним» за допомогою методу setAccessible():

age.setAccessible(true);
Тим полям, для яких це зроблено, можна присвоювати значення:

age.set(cat, 6);
Як бачиш, у нас вийшов такий собі перевернутий з ніг на голову сеттер: ми присвоюємо полю Field age його значення, а також передаємо йому об'єкт, в який це поле має бути присвоєно. Запустимо наш метод main() і побачимо:

Cat{name='Barsik', age=6}
Чудово, у нас все вийшло! :) Подивимося, які ще можливості у нас є…

Як викликати метод об'єкта за іменем

Трохи змінимо ситуацію з попереднього прикладу. Припустимо, розробник класу Cat не помилився з полями — обидва доступні, для них є геттери і сеттери, все ок. Проблема в іншому: він зробив приватним метод, який нам обов'язково потрібен:

private void sayMeow() {

   System.out.println("Мяу!");
}
У результаті ми будемо створювати об'єкти Cat у нашій програмі, але не зможемо викликати у них метод sayMeow(). У нас будуть коти, які не мяукають? Досить дивно :/ Як би це виправити? І знову Reflection API нас виручає! Назву потрібного методу ми знаємо. Решта — справа техніки:

import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Барсик", 6);
          
           clazz = Class.forName(Cat.class.getName());
          
           Method sayMeow = clazz.getDeclaredMethod("sayMeow");
          
           sayMeow.setAccessible(true);
          
           sayMeow.invoke(cat);
          
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Тут ми діємо приблизно так само, як у ситуації з доступом до приватного поля. Спочатку ми отримуємо потрібний нам метод, який інкапсульований в об'єкті класу Method:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
За допомогою getDeclaredMethod() можна «достукатись» у тому числі й до приватних методів. Далі ми робимо метод доступним для виклику:

sayMeow.setAccessible(true);
І, нарешті, викликаємо метод у потрібного об'єкта:

sayMeow.invoke(cat);
Виклик методу у нас також виглядає «викликом навпаки»: ми звикли вказувати об'єкту на потрібний метод за допомогою точки (cat.sayMeow()), а при роботі з рефлексією передаємо методу той об'єкт, у якого його треба викликати. Що ж у нас у консолі?

Мяу!
Усе вийшло! :) Тепер ти бачиш, які широкі можливості нам дає механізм рефлексії в Java. У складних і несподіваних ситуаціях (як у прикладах із класом із закритої бібліотеки) вона дійсно може нас сильно виручити. Однак, як і будь-яка велика сила, вона передбачає і велику відповідальність. Про недоліки рефлексії написано в спеціальному розділі на сайті Oracle. Можна виділити три головних мінуси:
  1. Знижується продуктивність. У методів, які викликаються за допомогою рефлексії, менша продуктивність порівняно з методами, які викликаються звичайним способом.

  2. Є обмеження щодо безпеки. Механізм рефлексії дозволяє змінювати поведінку програми під час виконання (runtime). Але у твоєму робочому середовищі на реальному проєкті можуть бути обмеження, що не дозволяють цього робити.

  3. Ризик розкриття внутрішньої інформації. Важливо розуміти, що використання рефлексії напряму порушує принцип інкапсуляції: дозволяє нам отримати доступ до приватних полів, методів тощо. Думаю, не варто пояснювати, що до прямого та грубого порушення принципів ООП варто вдаватися тільки в найкритичніших випадках, коли інших способів вирішити задачу не існує з причин, які від тебе не залежать.

Використовуй механізм рефлексії з розумом і тільки в тих ситуаціях, коли цього не уникнути, і не забувай про його недоліки. На цьому наша лекція підійшла до кінця! Вона вийшла досить великою, але сьогодні ти дізнався багато нового :)