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). Але у твоєму робочому середовищі на реальному проєкті можуть бути обмеження, що не дозволяють цього робити.
Ризик розкриття внутрішньої інформації. Важливо розуміти, що використання рефлексії напряму порушує принцип інкапсуляції: дозволяє нам отримати доступ до приватних полів, методів тощо. Думаю, не варто пояснювати, що до прямого та грубого порушення принципів ООП варто вдаватися тільки в найкритичніших випадках, коли інших способів вирішити задачу не існує з причин, які від тебе не залежать.