Ідея рефлексії може бути не одразу зрозумілою з однієї причини: до цього моменту ти завжди знав класи, з якими працюєш.
Ну, наприклад, ти міг написати клас 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:
- Дізнатися / визначити клас об'єкта.
- Отримати інформацію про модифікатори класу, поля, методи, константи, конструктори і суперкласи.
- Дізнатися, які методи належать реалізованому інтерфейсу / інтерфейсам.
- Створити екземпляр класу, коли ім'я класу невідоме до моменту виконання програми.
- Отримати і встановити значення поля об'єкта за назвою.
- Викликати метод об'єкта за назвою.
Як дізнатися / визначити клас об'єкта
Почнемо з основ. Вхідна точка у механізм рефлексії 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.
Тут ми зчитуємо ім'я класу, об'єкт якого будемо створювати, з консолі. Запущена програма дізнається ім'я класу, об'єкт якого їй належить створити.
Для стислості ми опустили код правильної обробки винятків, щоб він не займав більше місця, ніж сам приклад. У реальній програмі, звичайно, обов'язково варто обробити ситуації введення некоректних імен тощо.
Конструктор за замовчуванням — річ досить проста, тому створити екземпляр класу за його допомогою, як бачиш, нескладно :) А за допомогою методу 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.
Можна виділити три головних мінуси:
Знижується продуктивність. У методів, які викликаються за допомогою рефлексії, менша продуктивність порівняно з методами, які викликаються звичайним способом.
Є обмеження щодо безпеки. Механізм рефлексії дозволяє змінювати поведінку програми під час виконання (runtime). Але у твоєму робочому середовищі на реальному проєкті можуть бути обмеження, що не дозволяють цього робити.
Ризик розкриття внутрішньої інформації. Важливо розуміти, що використання рефлексії напряму порушує принцип інкапсуляції: дозволяє нам отримати доступ до приватних полів, методів тощо. Думаю, не варто пояснювати, що до прямого та грубого порушення принципів ООП варто вдаватися тільки в найкритичніших випадках, коли інших способів вирішити задачу не існує з причин, які від тебе не залежать.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ