Чому я повинен використовувати рефлексію?


29

Я новачок у Java; під час своїх досліджень я читав, що рефлексія використовується для виклику класів і методів, а також для того, щоб знати, які методи реалізовані чи ні.

Коли я повинен використовувати рефлексію, і яка різниця між використанням відображення та інстанцією об'єктів та методами називання традиційним способом?



10
Будь ласка, зробіть свою частку досліджень перед публікацією. Про StackExchange (як зазначав @Jalayn) і в Інтернеті багато інформації про роздуми. Я пропоную вам прочитати, наприклад, навчальний посібник Java з роздумів і повернутися після цього, якщо у вас є якісь конкретні питання.
Péter Török

1
Має бути мільйон дуп.
DeadMG

3
Більше кількох професійних програмістів відповіли б "як можна рідше, можливо, навіть ніколи".
Росс Паттерсон

Відповіді:


38
  • Відображення набагато повільніше, ніж просто виклик методів за їх іменем, оскільки він повинен перевіряти метадані в байт-коді, а не просто використовувати попередньо складені адреси та константи.

  • Відображення також є більш потужним: ви можете отримати визначення a protectedабо finalчлена, зняти захист і маніпулювати ним так, ніби воно було оголошено змінним! Очевидно, це підриває багато гарантій, які мова зазвичай надає для ваших програм і може бути дуже, дуже небезпечною.

І це в значній мірі пояснює, коли його використовувати. Зазвичай, не варто. Якщо ви хочете викликати метод, просто зателефонуйте йому. Якщо ви бажаєте вимкнути член, просто оголосьте його мутаційним, а не за спиною компіляції.

Одне корисне використання рефлексії в реальному світі - це при написанні рамки, яка повинна взаємодіяти з визначеними користувачем класами, де автор рамки не знає, якими будуть члени (або навіть класи). Рефлексія дозволяє їм мати справу з будь-яким класом, не знаючи цього заздалегідь. Наприклад, я не думаю, що неможливо було б скласти складну бібліотеку, орієнтовану на аспекти, без роздумів.

Як інший приклад, JUnit використовував тривіальний біт відображення: він перераховує всі методи у вашому класі, припускає, що всі викликані testXXXє методами тестування, і виконує лише ті. Але це тепер можна зробити краще, якщо замість цього є анотації, і насправді JUnit 4 значною мірою перейшов до анотацій.


6
"Більш потужним" потрібен догляд. Для отримання повноти Тьюрінга вам не потрібні роздуми, тому жодне обчислення не потребує роздумів. Звичайно, Тьюрінг завершений нічого не говорить про інші види енергії, як, наприклад, можливості вводу / виводу і, звичайно, роздуми.
Steve314

1
Рефлексія не обов'язково "набагато повільніше". Ви можете використовувати відображення один раз, щоб створити байт-код обертання прямого виклику.
SK-логіка

2
Ви знатимете це, коли вам це буде потрібно. Я часто замислювався, навіщо (поза мовою покоління) комусь це потрібно. Потім, раптом, я зробив ... Довелося ходити вгору і вниз ланцюжками батьків / дітей, щоб зазирнути / ткнути дані, коли я отримав панелі від інших розробників, щоб потрапити в одну з систем, які я підтримую.
Брайан Кноблауш

@ SK-логіка: насправді для генерації байт-коду вам взагалі не потрібна рефлексія (насправді відображення зовсім не містить API для маніпулювання байт-кодом!).
Йоахім Зауер

1
@JoachimSauer, звичайно, але вам знадобиться API відображення для завантаження цього генерованого байтового коду.
SK-логіка

15

Я був такий, як ти колись, я мало що знав про роздуми - все ще не знаю, - але я користувався ним колись.

У мене був клас з двома внутрішніми класами, і кожен клас мав безліч методів.

Мені потрібно було викликати всі методи у внутрішньому класі, і виклик їх вручну було б занадто багато роботи.

Використовуючи рефлексію, я міг би викликати всі ці методи всього за 2-3 рядки коду, а не кількість самих методів.


4
Чому потік?
Махмуд Хоссам

1
Оголошення преамбули
Дмитро Мінковський

1
@MahmoudHossam Можливо, це не найкраща практика, але ваша відповідь ілюструє важливу тактику, яку можна застосувати.
ankush981

13

Я б згрупував використання рефлексії на три групи:

  1. Миттєві довільні класи. Наприклад, у структурі введення залежностей ви, ймовірно, заявляєте, що інтерфейс ThingDoer реалізований класом NetworkThingDoer. Потім рамка знайде конструктор NetworkThingDoer і інстанціює його.
  2. Маршоподібний і непридатний до іншого формату. Наприклад, відображення об’єкта за допомогою геттерів та налаштувань, які відповідають конвенції про боби в JSON і назад. Код насправді не знає назв полів або методів, він просто вивчає клас.
  3. Загортання класу в шар переадресації (можливо, цей список насправді не завантажений, а лише вказівник на те, що знає, як отримати його з бази даних) або повністю підроблений клас (jMock створить синтетичний клас, який реалізує інтерфейс для тестування).

Це найкраще пояснення рефлексії, яку я знайшов на StackExchange. Більшість відповідей повторюють те, що говорить Java Trail (це "ви можете отримати доступ до всіх цих властивостей", але не чому б це зробити), наводять приклади того, як робити речі з відображенням, без яких набагато простіше, або дають певну неясну відповідь про те, як весна використовує його. Ця відповідь фактично дає три вагомі приклади, які не можуть легко обминути JVM без роздумів щодо CANNOT. Дякую!
ndm13

3

Рефлексія дозволяє програмі працювати з кодом, який може бути відсутнім, і робити це надійно.

"Звичайний код" має фрагменти, такі, URLConnection c = nullякі своєю чистою присутністю змушують завантажувач класів завантажувати клас URLConnection як частину завантаження цього класу, викидаючи виняток ClassNotFound та виходячи з нього.

Роздуми дозволяють завантажувати класи на основі їх імен у рядковій формі та перевіряти їх на різні властивості (корисні для декількох версій поза вашим контролем) перед запуском фактичних класів, які залежать від них. Типовим прикладом є специфічний код OS X, який використовується для того, щоб програми Java виглядали як рідні в ОС X, яких немає на інших платформах.


2

По суті, рефлексія означає використання коду вашої програми як даних.

Тому використання рефлексії може бути хорошою ідеєю, коли код вашої програми є корисним джерелом даних. (Але є компроміси, тому це може бути не завжди гарною ідеєю.)

Наприклад, розглянемо простий клас:

public class Foo {
  public int value;
  public string anotherValue;
}

і ви хочете генерувати з нього XML Ви можете написати код для створення XML:

public XmlNode generateXml(Foo foo) {
  XmlElement root = new XmlElement("Foo");
  XmlElement valueElement = new XmlElement("value");
  valueElement.add(new XmlText(Integer.toString(foo.value)));
  root.add(valueElement);
  XmlElement anotherValueElement = new XmlElement("anotherValue");
  anotherValueElement.add(new XmlText(foo.anotherValue));
  root.add(anotherValueElement);
  return root;
}

Але це дуже багато кодового коду, і кожного разу, коли ви змінюєте клас, вам доведеться оновити код. Дійсно, ви могли б описати, як працює цей код

  • створити XML-елемент із назвою класу
  • для кожної властивості класу
    • створити XML-елемент із назвою властивості
    • помістити значення властивості в елемент XML
    • додати елемент XML до кореня

Це алгоритм, а вхід алгоритму - клас: нам потрібна його назва, імена, типи та значення його властивостей. Ось тут і з’являється рефлексія: вона дає вам доступ до цієї інформації. Java дозволяє перевіряти типи, використовуючи методи Classкласу.

Ще кілька випадків використання:

  • визначити URL-адреси веб-сервера на основі імен методів класу та параметрів URL-адрес на основі аргументів методу
  • перетворити структуру класу у визначення типу GraphQL
  • викликати кожен метод класу, ім'я якого починається з "test" як одиничний тестовий випадок

Однак повне відображення означає не тільки перегляд існуючого коду (який сам по собі відомий як «самоаналіз»), але і зміну або генерування коду. Для цього у Java два видатні випадки використання: проксі-сервери та макети.

Скажімо, у вас є інтерфейс:

public interface Froobnicator {
  void froobnicateFruits(List<Fruit> fruits);
  void froobnicateFuel(Fuel fuel);
  // lots of other things to froobnicate
}

і у вас є реалізація, яка робить щось цікаве:

public class PowerFroobnicator implements Froobnicator {
  // awesome implementations
}

І насправді у вас є друга реалізація:

public class EnergySaverFroobnicator implements Froobnicator {
  // efficient implementations
}

Тепер ви також хочете деякий вихід журналу; ви просто хочете повідомлення журналу щоразу, коли викликається метод. Ви можете додати журнальний вихід до кожного методу явно, але це буде дратувати, і вам доведеться це зробити двічі; один раз на кожну реалізацію. (Так ще більше, коли ви додаєте більше реалізацій.)

Натомість ви можете написати проксі:

public class LoggingFroobnicator implements Froobnicator {
  private Logger logger;
  private Froobnicator inner;

  // constructor that sets those two

  public void froobnicateFruits(List<Fruit> fruits) {
    logger.logDebug("froobnicateFruits called");
    inner.froobnicateFruits(fruits);
  }

  public void froobnicateFuel(Fuel fuel) {
    logger.logDebug("froobnicateFuel( called");
    inner.froobnicateFuel(fuel);
  }
  // lots of other things to froobnicate
}

Знову ж таки, існує повторювана закономірність, яку можна описати алгоритмом:

  • проксі-реєстратор - клас, який реалізує інтерфейс
  • він має конструктор, який займає іншу реалізацію інтерфейсу та реєстратор
  • для кожного методу в інтерфейсі
    • реалізація записує повідомлення "$ namename name"
    • а потім викликає той же метод у внутрішньому інтерфейсі, передаючи всі аргументи

а вхід цього алгоритму - визначення інтерфейсу.

Рефлексія дозволяє визначити новий клас за допомогою цього алгоритму. Java дозволяє зробити це за допомогою методів java.lang.reflect.Proxyкласу, і є бібліотеки, які дають вам ще більше сил.

То які недоліки відображення?

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

1

Рефлексія може автоматично зберігати частини вашої програми в синхронізації, де раніше вам довелося б оновити програму вручну, щоб використовувати нові інтерфейси.


5
Ціна, яку ви платите в цьому випадку, полягає в тому, що ви втрачаєте перевірку типу компілятором і безпекою рефактора в IDE. Це я не бажаю робити компроміс.
Баренд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.