Java Reflection API - examples

Полное руководство по Java Reflection API. Рефлексия на примерах

В этой статье мы познакомимся со всеми элементами и функциональными возможностями Java Reflection API.

Рефлексия в Java — это механизм, с помощью которого можно вносить изменения и получать информацию о классах, интерфейсах, полях и методах во время выполнения, при этом не зная имен этих классов, методов и полей. Кроме того, Reflection API дает возможность создавать новые экземпляры классов, вызывать методы, а также получать или устанавливать значения полей.

Начинающие Java-программисты часто путают рефлексию с интроспекцией. Интроспекция — проверка кода и возможность видеть типы объектов во время выполнения. Рефлексия дает возможность вносить изменения во время выполнения программы путем использования интроспекции. Здесь важно понимать различие, поскольку некоторые языки поддерживают интроспекцию, но не поддерживают рефлексию, например, C++.

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

Java Reflection API

Рефлексия — мощная концепция, которая лежит в основе большинства современных Java/Java EE фреймворков и библиотек. Например, для Java классическими примерами являются:

  • JUnit – фреймворк для модульного тестирования. Он использует рефлексию для парсинга аннотаций (например, @Test) для получения описанных программистом тестовых методов и дальнейшего их выполнения.
  • Spring – фреймворк для разработки приложений на Java платформе, в основе которого лежит внедрение зависимостей (инверсия управления).

Список можно продолжать бесконечно: от веб-контейнеров до решения задач объектно-реляционного отображения (ORM). Их всех объединяет одно: они используют Java рефлексию, потому что не имеют доступа и представления к определенных пользователем классах, методах, интерфейсах и т.д.

Ограничения при работе с рефлексией в Java

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

  1. Низкая производительность — поскольку рефлексия в Java определяет типы динамически, то она сканирует classpath, чтобы найти класс для загрузки, в результате чего снижается производительность программы.
  2. Ограничения системы безопасности — рефлексия требует разрешения времени выполнения, которые не могут быть доступны для систем, работающих под управлением менеджера безопасности (Java Security Manager).
  3. Нарушения безопасности приложения — с помощью рефлексии мы можем получить доступ к части кода, к которой мы не должны получать доступ. Например, мы можем получить доступ к закрытым полям класса и менять их значения. Это может быть серьезной угрозой безопасности.
  4. Сложность в поддержке — код, написанный с помощью рефлексии трудно читать и отлаживать, что делает его менее гибким и трудно поддерживаемым.

Java Reflection: Работа с классами

Объект java.lang.Class является точкой входа для всех операций рефлексии. Для каждого типа объекта, JVM создает неизменяемый экземпляр java.lang.Class который предоставляет методы для получения свойств объекта, создания новых объектов, вызова методов.

В этом разделе мы рассмотрим важные методы при работе с java.lang.Class:

  • Все типы в Java, включая примитивные типы и массивы имеют связанный с ними java.lang.Class объект. Если мы знаем название класса во время компиляции, то сможем получить объект следующим образом:

  • Если мы не знаем имя во время компиляции, но знаем имя класса во время выполнения, то можно сделать так:

При использовании метода Class.forName() мы должны указать полное имя класса. То есть имя класса, включая все имена пакетов. Например, если SomeObject находится в пакете com.javadevblog.app, то полным именем вашего класса является строка: com.javadevblog.app.SomeObject.

Метод Class.forName() может бросить исключение ClassNotFoundException если класс не будет найден в classpath во время выполнения.

Получаем название класса

С помощью объекта Class мы можем можете получить имя класса двумя способами:

Методом getName() , который вернет полное имя класса с пакетом:

И с помощью метода getSimpleName(), который вернет только название класса без имени пакета:

Работа с модификаторами доступа

Мы можем получить доступ к модификаторам доступа с помощью Class объекта. Модификаторы представляют собой ключевые слова public, static, private и т.д. Мы можем получить модификаторы с помощью метода getModifiers():

Результат выполнения находится в переменной int, где каждый модификатор — это битовый флаг, который может быть установлен или сброшен. Мы можем проверить модификаторы, используя следующие методы в классе java.lang.reflect.Modifier:

В параметры метода просто передадим modifiers и каждый из этих методов вернет true или false.

Получение информации о пакете

Зная только класс мы можем получить информацию о пакете:

Мы также можем получить доступ к информации, указанной для данного пакета в Manifest файле внутри JAR файла. этот пакет находится в пути к классам. Подробнее о Package читайте в оф. документации: java.lang.Package.

Получаем Superclass

Зная объект Class, мы можем получить доступ к его суперклассу:

Получим с помощью рефлексии суперкласс, мы можем работать с ним, как и с любым другим объектом.

Реализованные интерфейсы

Список интерфейсов, реализуемых данным классом можно получить так:

Класс может реализовать много интерфейсов. Поэтому возвращается массив объектов Class. В Java Reflection API интерфейсы также представлены объектами Class.

Обратите внимание: Метод вернул только интерфейсы, которые реализует указанный класс, а не его суперкласс. Для того, чтобы получить полный список интерфейсов, реализованных в этом классе, вы должны обратиться как к текущему классу, так и к его суперклассу.

Рефлексия в Java: Конструкторы

С помощью Java Reflection API можно получать информацию про конструкторы классов и создавать объекты во время выполнения. Это делается с помощью класса Java java.lang.reflect.Constructor.

Получаем конструкторы класса

В коде выше мы получили массив конструкторов класса и теперь можем работать с ними. Если известны параметры конкретного конструктора, то массив можно не получать, а работать уже с известным. Например, следующий конструктор класса принимает String качестве параметра:

Если мы ошиблись с аргументом конструктора, то будет выброшено исключение NoSuchMethodException.

Получаем параметры конструктора

На примерах выше мы получили массив конструкторов. Теперь мы можем узнать параметры для каждого из них. Например, получить типы параметров какого-то конструктора можно так:

Создание объектов с помощью конструкторов

Теперь получив конструкторы класса и зная их переметры, мы можем создать новый экземпляр указанного класса:

Создание нового экземпляра класса произошло после вызова метода newInstance() на конструкторе класса.

Рефлексия в Java: Поля

Используя рефлексию мы можем работать с полями — переменными-членами класса. В этом нам помогает Java класс java.lang.reflect.Field: с помощью него в рантайме мы можем устанавливать значения и получать данные с полей.

Получаем поля класса

Получить поля класса с помощью рефлексии можно с помощью следующей строчки кода:

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

Если вы знаете имя поля, к которому вы хотите получить доступ, достаточно вызвать следующую строчку:

Строчка выше вернет значение поля под именем fieldName с типом Field нашего класса SomeObject:

Если мы укажем неверное имя поля, то получим NoSuchFieldException.

Получаем название поля

Получаем тип поля

Получаем и устанавливаем значения полей

Параметр instance, который передается в методы для получения и установки значения поля, должен быть экземпляром класса, которому принадлежит само поле. В приведенном выше примере используется экземпляр класса SomeObject, потому что поле fieldName является членом экземпляра этого класса.

Java Reflection API: Методы

Используя ревфлексию в Java мы можем получать информацию о методах и вызывать их в рантайме. Для этого используется класс java.lang.reflect.Method:

В коде выше мы получили все методы класса.

Нам не нужно получать массив со всеми методами, если нам известны точные типы параметров метода, который мы хотим использовать. Например, у нас есть метод под названием «sayHello«, который принимает String в качестве параметра. Получить объект Method для него можно так:

Если такого метода нет, то будет выброшен NoSuchMethodException.

Если метод sayHello() без параметров, то нужно передать null в методе getMethod():

Параметры метода и типы возвращаемых значений

Получить параметры метода можно так:

В строчке ниже мы можем получить тип возвращаемого значения:

Вызов метода с помощью Java рефлексии

В коде выше мы получили метод указанного класса и вызвали для него метод invoke().

  • objectToInvokeOn имеет тип Object и является объектом, для которого мы хотим вызвать метод sayHello().
  • parameterTypes — имеет тип Class[] и представляет собой параметры, которые метод принимает на вход.
  • params имеет тип Object[] и представляет собой пераметры, которые переданы методы.

Следует отметить: если мы знаем, что метод sayHello — статический, то вызов метода с помощью рефлексии можно переписать так:

То есть вместо объекта вызова objectToInvokeOn указать null.

Java Reflection API: Методы установки и получения значения

Кроме обычных методов класса мы также можем работать с методам get и set с помощью рефлексии. Получить эти методы можно несколькими способами: явно указать геттер или сеттер при работе или обойти все доступные в классе методы и проверить их на то являются ли они методами get или set:

Чтобы определить геттер или сеттер нам нужно определить некоторые правила:

  • Геттер — метод, имя которого начинается на ‘get’. Он не принимает аргументы и обязательно возвращает какое-то значение.
  • Сеттер — метод, имя которого начинается на ‘set’. Он принимает 1 параметр.

Обычно сеттеры не возвращают значение, однако при опеределении метода к сеттеру возвращаемый параметр не учитывается.

Смотрим на примере определения метода как сеттера или геттера с помощью рефлексии в Java:

Java Reflection API: Приватные поля и методы

С помощью рефлексии в Java можно получить доступ к private полям других классов. Это очень удобно, например, для модульного тестирования.

Обратите внимание, что получение приватных полей корректно работает только в standalone Java приложениях. Если вы попытаетесь обратиться к приватным полям внутри Java-апплета, то нужно будет возиться с Java SecurityManager.

Доступ к private полям с помощью рефлексии

Чтобы получить доступ к закрытому полю нужно будет использовать метод Class.getDeclaredField(String name) или Class.getDeclaredFields() метод. Методы Class.getField(String name) и Class.getFields() возвращают только public поля, поэтому в нашем случае они работать не будут.

Давайте на примере посмотрим работу с private полям с помощью Java Reflection:

У нас есть класс PrivateClass с закрытым полем mPrivateString. Теперь напишем код, с помощью которого мы будем получить к нему доступ:

В коде выше мы использовали метод getDeclaredField, который имеет доступ лишь к полям, объявленным в конкретном классе и не имеет доступ к полям суперкласса.

Обратите внимание, что в коде выше мы использовали Field.setAcessible(true), который отключает проверку доступа для указанного поля. Теперь мы можем работать с ним с помощью рефлексии, даже если у него был private, protected или default доступ. Без использования рефлексии этот метод все также приватный и компилятор не позволит нам обратиться к нему.

Доступ к приватным методам с помощью рефлексии

Уже известные нам методы работы с методами Class.getMethod(String name, Class[] parameterTypes) и Class.getMethods() здесь не помогут, так как они имеют доступ лишь к public методам.

Доступ к закрытым методам осуществляется с помощью методов Class.getDeclaredMethod(String name, Class[] parameterTypes) или Class.getDeclaredMethods().

Теперь давайте рассмотрим способ получения private метода в Java с помощью рефлексии. Давайте добавим закрытый метод getPrivateString() в созданный выше класс PrivateClass:

Теперь напишем код, который используя рефлексию получит доступ к этому private методу:

Также как и приватными полями, мы можем получить доступ к приватным методам только текущего класса (без доступа к private методам суперкласса).

С помощью Method.setAcessible(true) снимаем проверку доступа только для рефлексии.

Java Reflection: Аннотации

С помощью Java рефлексии мы можем обрабатывать аннотации в рантайме.

Что такое аннотации в Java. Краткое описание и пример работы.

Аннотации также могут быть обработаны с помощью рефлексии. Для полноты руководства по Java Reflection давайте создадим простую аннотацию и затем научимся обрабатывать ее с помощью рефлексии:

С помощью @ мы указываем, что описываем аннотацию.

Также мы использовали две аннотации, которые применяются только к аннотациям при создании:

  • @Retention(RetentionPolicy.RUNTIME) и @Target(ElementType.TYPE)указывают как будет использоваться пользовательская аннотация.
  • @Retention(RetentionPolicy.RUNTIME) указала на то, что аннотация будет использована в рантайме, следовательно мы сможем получить к ней доступ с помощью рефлексии.
  • @Target(ElementType.TYPE) указывает на то, что мы можем использовать аннотацию на интерфейсах и классах.

Теперь давайте «повесим» нашу аннотацию на какой-то класс:

С помощью рефлексии мы можем работать с аннотациями класса, метода или поля в рантайме. Давайте теперь получим все аннотации класса SimpleClass (в нашем случае у нас только 1 наша аннотация):

В коде выше мы использовали метод getAnnotations() для получения всех аннотаций класса в виде массива объектов Annotation. В цикле по всем аннотациям мы ищем пользовательскую аннотацию — Reflectable и получаем информацию о ней.

Если же нам интересна конкретная аннотация, то получить к ней доступ можно следующим образом:

Работа с аннотациями на методах

Аннотации на методах работают точно также, как и на примере выше, единственная разница в получении списка аннотаций. Если выше мы использовали метод getAnnotations(), то для аннотаций на методах можно вызывать метод getDeclaredAnnotations() — получение всех аннотаций с указанного метода.

Наш класс с аннотацией на методе:

Используем рефлексию:

Или же явно получаем с указанного метода (если знаем наверняка, что она есть на этом методе):

Аннотации на параметрах метода

Давайте добавим в наш класс еще один метод sayBye() с одним параметром, помеченным аннотацией:

Теперь напишем код для получения аннотаций на параметрах метода:

В коде выше мы получили двумерный массив аннотаций на методе: внутренний массив каждого элемента является набором аннотаций для каждого параметра (аргумента метода).

Аннотации на полях

В наш класс добавим поле: пусть это будет строка со значением null:

Теперь используя рефлексию получим аннотации к полю:

Java Reflection API: Дженерики (Generics — родовые типы)

В различных статьях и на форумах часто пишут, что вся информация про дженерики в Java стирается во время компиляции и мы не можем получить ее во время выполнения (так называемое стирание типов). Это утверждение не совсем верно. Возможность получить информацию о дженериках во время выполнения есть в нескольких случаях. Давайте ниже рассмотрим их.

Как правило, дженерики в Java используются в следующих ситуациях:

  • При объявлении параметризованного класса или интерфейса.
  • Использование параметризованного класса.

Примером может послужить java.util.List интерфейс. Вместо того, чтобы создать список объектов, можно параметризовать его, скажем с помощью String.

Во время выполнения программы посмотреть тип параметризированного java.util.List возможности нет, но мы можем найти его в полях и методах, где он используется и параметризируется с помощью Java рефлексии. Смотрим на примере:

Информация о дженериках в рантайме

Информацию о типе мы можем получить с помощью класса java.lang.reflect.Method.

Ниже представлен класс, который мы будем обрабатывать:

Получить информацию о дженериках параметризированного списка можно, если работать не с самим списком, а с геттером -методом getList():

Будет напечатано в консоль: тип: java.lang.String.

В коде выше мы определили, что это не просто List, а именно List<String>.

Если же у нас есть поле, то получить информацию о типе в рантайме можно следующим образом:

Будет напечатано в консоль: тип: java.lang.String. В коде выше мы определили, что тип поля — параметризированный (с помощью instanceof) и по нему уже получили массив типов Type[], который содержит 1 элемент типа Class (он реализует интерфейс Type). Поэтому мы просто кастовали его к Class и распечатали в консоль.

Java Reflection API: Массивы

Работать с массивами сложнее всего, особенно если мы хотим получить объект Class для массива каких-то int[], например. Получать информацию о массивах можно с помощью класса java.lang.reflect.Array (не путать с java.util.Arrays в пакете Java коллекций)

Создание массива int с 2 элементами выглядит так:

В первом параметре метода newInstance() указываем тип элементов массива, во втором — количество элементов этого массива.

Теперь рассмотрим работу геттеров и сеттеров при работе с массивами с помощью Java рефлексии:

Получаем объект типа Class для массива

С использованием рефлексии это выглядит довольно трудно:

Обратите внимание на параметр: "[I":

  • JVM представляет тип int с помощью символа "I".
  • Символ «[» слева представляет собой класс массива int.

Это работает для всех примитивов. А вот для объектов дело обстоит иначе. Смотрим на примере String:

Комбинация "[L" вначале и ";" представляет собой массив объектов определенного типа (тип описан между ними).

Такие сложности порождают другие сложности — уже с удобством работы. Для обхода этого обычно пишут такие хелпер-методы:

А теперь создаем экземпляр Class:

Получаем тип массива

Получить тип массива так же просто:

И получим в консоли java.lang.String.

На этом руководство по рефлексии в Java подошло к концу. Подписывайтесь и следите за обновлениями: вас ждет множество новых материалов по современной разработке на Java и под Android.

Подробнее о рефлексии:

Рефлексия на примерах

Динамические прокси и класс лоадеры

2 Комментарии “Полное руководство по Java Reflection API. Рефлексия на примерах

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *