Вітаю вас, молодий падан. У цій статті я розповім про Сил, міць якої java-програмісти використовують тільки в, здавалося б, безвихідній ситуації. Отже, темна сторона Java -
Рефлексія Java здійснюється за допомогою Java Reflection API. Що таке рефлексія? Існує коротке та точне, а також популярне на просторах інтернету визначення. Рефлексія (від пізньолат. reflexio - звернення назад)- Це механізм дослідження даних про програму під час її виконання. Рефлексія дозволяє досліджувати інформацію про поля, методи та конструктори класів. Сам механізм рефлексії дозволяє обробляти типи, відсутні при компіляції, але які з'явабося під час виконання програми. Рефлексія та наявність логічно цілісної моделі видачі інформації про помилки дає можливість створювати коректний динамічний код. Інакше висловлюючись, розуміння принципів роботи рефлексії в java відкриває перед вами низку дивовижних можливостей. Ви буквально можете жонглювати класами та їх складовими.
Ось основний список того, що дозволяє рефлексія:
У моїй ієрархії
Reflection API
- Дізнатися/визначити клас об'єкта;
- Отримати інформацію про модифікаторів класу, поля, методи, константи, конструктори та суперкласи;
- З'ясувати, які методи належать реалізованому інтерфейсу/інтерфейсу;
- Створити екземпляр класу, причому ім'я класу невідоме до виконання програми;
- Отримати та встановити значення поля об'єкта на ім'я;
- Викликати метод об'єкта на ім'я.
MyClass
.
public class MyClass {
private int number;
private String name = "default";
// public MyClass(int number, String name) {
// this.number = number;
// this.name = name;
// }
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public void setName(String name) {
this.name = name;
}
private void printData(){
System.out.println(number + name);
}
}
Як бачимо, це звичайнісінький клас. Конструктор із параметрами закоментований не просто так, ми до цього ще повернемося. Якщо ви уважно переглянули вміст класу, то, напевно, побачабо відсутність getter
'a для поля name
. Саме поле name
позначене модифікатором доступу private
, звернутися до нього поза самим класом у нас не вийде =>
ми не можемо отримати його значення. "Так в чому проблема? - Скажете ви. — Допиши getter
або зміни модифікатор доступу”. І ви будете праві, але, що якщо MyClass
знаходиться в скомпільованій aar бібліотеці або в іншому закритому модулі без доступу до редагування, а на практиці таке трапляється вкрай часто. І якийсь неуважний програміст просто забув написати getter
. Саме час згадати рефлексії! Спробуємо дістатися доprivate
поля name
класу MyClass
:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //no getter =(
System.out.println(number + name);//output 0null
try {
Field field = myClass.getClass().getDeclaredField("name");
field.setAccessible(true);
name = (String) field.get(myClass);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(number + name);//output 0default
}
Розберемо, що тут зараз сталося. У java є чудовий клас Class
. Він представляє класи та інтерфейси у додатку Java, що виконується. Зв'язок між Class
і ClassLoader
ми не зачіпатимемо, т.к. це не є темою статті. Далі, щоб отримати поля цього класу потрібно викликати метод getFields()
, цей метод поверне нам усі доступні поля класу. Нам це не підходить, так як наше поле private
, тому використовуємо метод getDeclaredFields()
, цей метод також повертає масив полів класу, але тепер і private
і protected
. У нашій ситуації ми знаємо ім'я поля, яке нас цікавить, і можемо використовувати метод getDeclaredField(String)
, де String
ім'я потрібного поля. Примітка: getFields()
і getDeclaredFields()
не повертають поля класу-батька! Відмінно ми отримали об'єкт Field
з посиланням на наш name
. Т.к. поле не було публичным
(public) слід дати доступ до роботи з ним. Метод setAccessible(true)
дозволяє нам подальшу роботу. Тепер поле name
є повністю під нашим контролем! Отримати його значення можна викликом get(Object)
у об'єкта Field
, де Object
екземпляр нашого класу MyClass
. Приводимо до типу String
і присвоюємо нашій змінній name
. На той випадок, якщо у нас раптом не виявилося setter
'a, для встановлення нового значення полю name можна використовувати метод set
:
field.set(myClass, (String) "new value");
Вітаю! Ви тільки що опанували базовий механізм рефлексії і змогли отримати доступ до private
поля! Зверніть увагу на блок try/catch
і типи винятків, що обробляються. IDE сама вкаже на їхню обов'язкову присутність, але за їхньою назвою отже ясно навіщо вони тут. Йдемо далі! Як ви могли помітити, наш MyClass
уже має метод для виведення інформації про дані класу:
private void printData(){
System.out.println(number + name);
}
Але цей програміст і тут успадкував. Метод знаходиться під модифікатором доступу private
, і нам довелося самим щоразу писати код виводу. Не порядок, де там наша рефлексія? Напишемо таку функцію:
public static void printData(Object myClass){
try {
Method method = myClass.getClass().getDeclaredMethod("printData");
method.setAccessible(true);
method.invoke(myClass);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
Тут приблизно така сама процедура як і з отриманням поля - отримуємо потрібний метод на ім'я і даємо доступ до нього. І для виклику об'єкта Method
використовуємо invoke(Оbject, Args)
, де Оbject
- все також екземпляр класу MyClass
. Args
- аргументи методу - наших не має. Тепер для виведення інформації ми використовуємо функцію printData
:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //?
printData(myClass); // outout 0default
try {
Field field = myClass.getClass().getDeclaredField("name");
field.setAccessible(true);
field.set(myClass, (String) "new value");
name = (String) field.get(myClass);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
printData(myClass);// output 0new value
}
Ура, тепер ми маємо доступ до приватного методу класу. Але що робити якщо у методу все ж таки будуть аргументи, і навіщо той закоментований конструктор? Всьому свій час. З визначення спочатку ясно, що рефлексія дозволяє створювати екземпляри класу в режимі runtime
(під час виконання програми)! Ми можемо створити об'єкт класу на повне ім'я цього класу. Повне ім'я класу - це ім'я класу, враховуючи шлях до нього package
.
package
повним ім'ям MyClass
буде “ reflection.MyClass
”. Також дізнатися ім'я класу можна простим способом (поверне ім'я класу у вигляді рядка):
MyClass.class.getName()
Створимо екземпляр класу за допомогою рефлексії:
public static void main(String[] args) {
MyClass myClass = null;
try {
Class clazz = Class.forName(MyClass.class.getName());
myClass = (MyClass) clazz.newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
На момент старту програми Java далеко не всі класи виявляються завантаженими в JVM. Якщо у вашому коді немає звернення до класу MyClass
, то той, хто відповідає за завантаження класів у JVM, а ним є ClassLoader
, ніколи його туди і не завантажить. Тому потрібно змусити ClassLoader
завантажити його та отримати опис нашого класу у вигляді змінної типу Class
. Для цього завдання існує метод forName(String)
, де String
ім'я класу, опис якого нам потрібно. Отримавши Сlass
, виклик методу newInstance()
поверне Object
, який буде створений за тим самим описом. Залишається привести цей об'єкт до нашого класуMyClass
. Круто! Було складно, але сподіваюся, зрозуміло. Тепер ми вміємо створювати екземпляр класу буквально з одного рядка! На жаль, описаний спосіб буде працювати тільки з конструктором за умовчанням (без параметрів). Як викликати методи з аргументами і конструктори з параметрами? Саме час розкоментувати наш конструктор. Як і очікувалося, newInstance()
не знаходить за замовчуванням конструктор і більше не працює. Перепишемо створення екземпляра класу:
public static void main(String[] args) {
MyClass myClass = null;
try {
Class clazz = Class.forName(MyClass.class.getName());
Class[] params = {int.class, String.class};
myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Для отримання конструкторів класу слід в описі класу викликати метод getConstructors()
, а для отримання параметрів конструктора - getParameterTypes()
:
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
Class[] paramTypes = constructor.getParameterTypes();
for (Class paramType : paramTypes) {
System.out.print(paramType.getName() + " ");
}
System.out.println();
}
Таким чином отримуємо всі конструктори та всі параметри до них. У моєму прикладі йде звернення до конкретного конструктора з вже конкретними відомими параметрами. І виклику цього конструктора використовуємо метод newInstance
, у якому вказуємо значення цим параметрам. Так само буде і зinvoke
для виклику методів. Виникає питання: де може стати в нагоді рефлексивний виклик конструкторів? Сучасні технології java, як говорилося на початку, не обходяться без Reflection API. Наприклад, DI (Dependency Injection), де анотації у поєднанні з рефлексією методів та конструкторів утворюють популярну в Android розробці бібліотеку Dagger. Після прочитання цієї статті ви з упевненістю можете вважати себе освіченим механізмами Reflection API. Темною стороною java рефлексія називається не дарма. Вона геть-чисто ламає парадигму ОВП. У Java інкапсуляція служить для приховування та обмеження доступу одних компонентів програми до інших. Використовуючи модифікатор private ми маємо на увазі, що доступ до цього поля буде тільки в межах класу, де це поле існує, ґрунтуючись на цьому, ми будуємо подальшу архітектуру програми. У цій статті ми побачабо, як за допомогою рефлексії можна пробиратися будь-куди. Хорошим прикладом у вигляді архітектурного рішення є шаблон проектування, що породжує.Singleton
. Основна його ідея в тому, щоб протягом всієї роботи програми клас, що реалізує цей шаблон, був тільки в одному примірнику. Здійснюється це за допомогою установки конструктору за замовчуванням модифікатор private доступу. І буде дуже погано, якщо якийсь програміст зі своєю рефлексією плодитиме такі класи. До речі, є дуже цікаве питання, яке я нещодавно почув від свого співробітника: чи може бути у класу Singleton
спадкоємці, що реалізує шаблон? Невже в цьому випадку неспроможна навіть рефлексія? Пишіть ваші feedback'и за статтею та відповідь у коментарі, а також ставте свої запитання! Справжня Сила Reflection API розкривається в комбінації з Runtime Annotations, про що ми, можливо, поговоримо в наступній статті про темну сторону Java. Дякую за увагу!