JavaRush /Java блог /Random UA /Reflection в Java - Приклади використання

Reflection в Java - Приклади використання

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

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

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

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

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

public class Cat extends Animal {

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

   private String name;
   private int age;

   //...остальная часть класса
}
Тепер маємо повний набір! Давай відчуємо можливості рефлексії :)
import learn.codegym.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.codegym.Cat
Поля класса: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age]
Родительский класс: class learn.codegym.Animal
Методы класса: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()]
Конструкторы класса: [public learn.codegym.Cat(java.lang.String,int)]
Як багато інформації про клас ми отримали! Причому не лише про публічні, а й про приватні частини. Зверни увагу: private-Змінні теж відображені у списку. Власне, аналіз класу можна на цьому вважати завершеним: тепер за допомогою методу analyzeClass()ми дізнаємося все, що тільки можна. Але це не всі можливості, які ми маємо при роботі з рефлексією. Не обмежуватимемося простим спостереженням і перейдемо до активних дій! :)

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

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

}
Ось як виглядатиме код для створення об'єкта Catза допомогою рефлексії (метод createCat()):
import learn.codegym.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.codegym.Cat
Виведення в консоль:

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

null
java.lang.InstantiationException: learn.codegym.Cat
  at java.lang.Class.newInstance(Class.java:427)
Щось пішло не так! Ми отримали помилку, тому що викликали метод створення об'єкта через конструктор за замовчуванням. Адже такого конструктора в нас тепер немає. Значить при роботі методу newInstance()механізм рефлексії використовуватиме наш старий конструктор із двома параметрами:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
А з параметрами ми нічого не зробабо, ніби взагалі забули про них! Щоб передати їх до конструктора за допомогою рефлексії, доведеться трохи «похимічити»:
import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.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.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.codegym.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("Meow!");
}
В результаті ми створюватимемо об'єкти Catв нашій програмі, але не зможемо викликати у них метод sayMeow(). У нас будуть кішки, які не нявкають? Досить дивно :/ Як це виправити? І знову Reflection API рятує нас! Назву потрібного методу ми знаємо. Решта – справа техніки:
import learn.codegym.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("Barsik", 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()), а під час роботи з рефлексією передаємо методу той об'єкт, що його потрібно викликати. Що ж у нас у консолі?

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

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

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

Використовуй механізм рефлексії з розумом і лише в тих ситуаціях, коли цього не уникнути, і не забувай про його недоліки. На цьому наша лекція добігла кінця! Вона вийшла досить великою, але сьогодні ти дізнався багато нового :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ