Вітаю вас, юний падаване. У цій статті я розповім про Силу, могутність якої java-програмісти використовують тільки у, здавалося б, безвихідній ситуації. Отже, темна сторона Java — Reflection API Рефлексія в Java здійснюється за допомогою Java Reflection API. Що таке ця рефлексія? Існує коротке та точне, а також популярне в інтернеті визначення. Рефлексія (від пізньолат. reflexio — звернення назад) — це механізм дослідження даних про програму під час її виконання. Рефлексія дозволяє досліджувати інформацію про поля, методи та конструктори класів. Сам механізм рефлексії дозволяє обробляти типи, відсутні при компіляції, але які з'явилися під час виконання програми. Рефлексія та наявність логічно цілісної моделі видачі інформації про помилки дають можливість створювати коректний динамічний код. Інакше кажучи, розуміння принципів роботи рефлексії в java відкриває перед вами ряд дивовижних можливостей. Ви буквально можете жонглювати класами та їх складовими.
Reflection API. Рефлексія. Темна сторона Java - 2
Ось основний список того, що дозволяє рефлексія:
  • Дізнатися/визначити клас об'єкта;
  • Отримати інформацію про модифікатори класу, поля, методи, константи, конструктори та суперкласи;
  • Дізнатися, які методи належать реалізованому інтерфейсу/інтерфейсам;
  • Створити екземпляр класу, причому ім'я класу невідоме до моменту виконання програми;
  • Отримати та встановити значення поля об'єкта за ім'ям;
  • Викликати метод об'єкта за ім'ям.
Рефлексія використовується практично у всіх сучасних технологіях Java. Важко уявити, чи могла б 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’а, для встановлення нового значення полю 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) "нове значення");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0нове значення
}
Ура, тепер у нас є доступ до приватного методу класу. Але що робити якщо у методу все ж таки будуть аргументи, і навіщо той закоментований конструктор? Усьому свій час. З визначення на початку зрозуміло, що рефлексія дозволяє створювати екземпляри класу в режимі runtime (під час виконання програми)! Ми можемо створити об'єкт класу за повним ім'ям цього класу. Повне ім'я класу — це ім'я класу, враховуючи шлях до нього в package.
Reflection API. Рефлексія. Темна сторона Java - 3
У моїй ієрархії 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. Дякую за увагу!