Почему я должен использовать отражение?


29

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

Когда я должен использовать отражение, и в чем разница между использованием отражения и созданием экземпляров объектов и вызовом методов традиционным способом?



10
Пожалуйста, сделайте свою долю исследований перед публикацией. На StackExchange (как отметил @Jalayn) и в Интернете есть много материалов о рефлексии. Я предлагаю вам прочитать, например, Учебник Java по Reflection и вернуться после этого, если у вас есть более конкретные вопросы.
Петер Тёрёк

1
Там должно быть миллион обманщиков.
DeadMG

3
Более чем несколько профессиональных программистов ответили бы «настолько редко, насколько это возможно, возможно, даже никогда».
Росс Паттерсон

Ответы:


38
  • Отражение намного медленнее, чем просто вызов метода по имени, потому что он должен проверять метаданные в байт-коде, а не просто использовать предварительно скомпилированные адреса и константы.

  • Отражение также более мощное: вы можете получить определение члена protectedили final, снять защиту и манипулировать им, как если бы оно было объявлено изменяемым! Очевидно, что это подрывает многие гарантии, которые язык обычно дает вашим программам, и может быть очень и очень опасным.

И это в значительной степени объясняет, когда его использовать. Обычно нет. Если вы хотите вызвать метод, просто вызовите его. Если вы хотите изменить член, просто объявите его изменяемым вместо того, чтобы идти за спиной компиляции.

Одним из полезных применений отражения в реальных условиях является написание фреймворка, который должен взаимодействовать с пользовательскими классами, когда автор фреймворка не знает, какими будут члены (или даже классы). Отражение позволяет им иметь дело с любым классом, не зная об этом заранее. Например, я не думаю, что можно было бы написать сложную аспектно-ориентированную библиотеку без размышлений.

В качестве другого примера, JUnit использовал тривиальный бит отражения: он перечисляет все методы в вашем классе, предполагает, что все вызываемые testXXXявляются тестовыми методами, и выполняет только те. Но теперь это можно сделать лучше с помощью аннотаций, и на самом деле JUnit 4 вместо этого в основном перешел на аннотации.


6
«Более сильный» нуждается в заботе. Вам не нужно рефлексия, чтобы получить полноту Тьюринга, поэтому никакие вычисления никогда не нуждаются в рефлексии. Конечно, Turing complete ничего не говорит о других видах мощности, таких как возможности ввода-вывода и, конечно же, рефлексия.
Steve314

1
Отражение не обязательно «намного медленнее». Вы можете использовать отражение один раз, чтобы сгенерировать байт-код прямого вызова оболочки.
SK-logic

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

@ SK-logic: на самом деле для генерации байт-кода вам вообще не нужно рефлексия (действительно, рефлексия вообще не содержит API для манипулирования байт-кодом!).
Иоахим Зауэр

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

15

Когда-то я был таким же, как ты, я мало что знал об отражении - до сих пор не знаю - но я использовал его один раз

У меня был класс с двумя внутренними классами, и у каждого класса было много методов.

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

Используя рефлексию, я мог бы вызвать все эти методы всего за 2-3 строки кода вместо числа самих методов.


4
Почему отрицательный голос?
Махмуд Хоссам

1
Голосование за преамбулу
Дмитрий Минковский

1
@MahmoudHossam Возможно, это не лучшая практика, но ваш ответ иллюстрирует важную тактику, которую можно использовать.
ankush981

13

Я бы сгруппировал использование отражения в три группы:

  1. Создание произвольных классов. Например, в среде внедрения зависимостей вы, вероятно, объявляете, что интерфейс ThingDoer реализуется классом NetworkThingDoer. Затем фреймворк найдет конструктор NetworkThingDoer и создаст его экземпляр.
  2. Маршаллинг и демаршаллинг в каком-то другом формате. Например, сопоставление объекта с получателями и настройками, которые следуют соглашению bean-компонентов, с JSON и обратно. Код фактически не знает имен полей или методов, он просто проверяет класс.
  3. Обертывание класса в слое перенаправления (возможно, что List на самом деле не загружен, а просто указатель на что-то, что знает, как извлечь его из базы данных) или подделка класса целиком (jMock создаст синтетический класс, который реализует интерфейс в целях тестирования).

Это лучшее объяснение отражения, которое я нашел на StackExchange. Большинство ответов повторяют то, что говорит Java Trail (то есть «вы можете получить доступ ко всем этим свойствам», но не то, почему вы это делаете), дают примеры выполнения действий с отражением, без которых гораздо легче обойтись, или дают какой-то расплывчатый ответ о том, как Spring использует это. Этот ответ на самом деле дает три достоверных примера, которые не могут быть легко обработаны JVM без размышлений. Спасибо!
ndm13

3

Reflection позволяет программе работать с кодом, который может отсутствовать, и делать это надежным способом.

«Нормальный код» имеет такие фрагменты, URLConnection c = nullкоторые, благодаря своему явному присутствию, заставляют загрузчик классов загрузить класс URLConnection как часть загрузки этого класса, вызывая исключение ClassNotFound и выход.

Отражение позволяет вам загружать классы на основе их имен в строковой форме и проверять их на различные свойства (полезно для нескольких версий вне вашего контроля) перед запуском реальных классов, которые зависят от них. Типичным примером является специальный код OS X, используемый для того, чтобы Java-программы выглядели как родные в OS 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
}

Опять же, однако, есть повторяющийся шаблон, который может быть описан алгоритмом:

  • прокси-сервер журнала является классом, который реализует интерфейс
  • у него есть конструктор, который принимает другую реализацию интерфейса и регистратор
  • для каждого метода в интерфейсе
    • реализация регистрирует сообщение «$ methodname named»
    • а затем вызывает тот же метод на внутреннем интерфейсе, передавая все аргументы

и ввод этого алгоритма - определение интерфейса.

Отражение позволяет вам определить новый класс, используя этот алгоритм. Java позволяет вам делать это, используя методы java.lang.reflect.Proxyкласса, и есть библиотеки, которые дают вам еще больше возможностей.

Так каковы недостатки отражения?

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

1

Reflection может автоматически синхронизировать части вашей программы, где раньше вам приходилось вручную обновлять программу, чтобы использовать новые интерфейсы.


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