JavaRush /Java блог /Random /Reflection API. Рефлексия. Темная сторона Java
Oleksandr Klymenko
13 уровень
Харків

Reflection API. Рефлексия. Темная сторона Java

Статья из группы Random
Приветствую вас, юный падаван. В этой статье я поведаю о Силе, мощь которой java-программисты используют только в, казалось бы, безвыходной ситуации. Итак, темная сторона Java — Reflection API
Reflection API. Рефлексия. Темная сторона Java - 1
Рефлексия в 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’a, для установки нового значения полю 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) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
Ура, теперь у нас есть доступ к приватному методу класса. Но что делать если у метода все таки будут аргументы, и зачем тот закомментированный конструктор? Всему свое время. Из определения в начале ясно, что рефлексия позволяет создавать экземпляры класса в режиме 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 раскрывается в комбинации c Runtime Annotations, о чем мы, возможно, поговорим в следующей статье про темную сторону Java. Спасибо за внимание!
Комментарии (118)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Сергей Сак Уровень 2
28 марта 2024
Цитата: "Примечание: getFields() и getDeclaredFields() не возвращают поля класса-родителя!" Код:

class Test{
    private int a = 5;
    public int b = 1;
}
class A extends Test {
    private int c = 7;
    public int d = 0;
}
public class Main {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Test t = new A();
        Field[] field = t.getClass().getDeclaredFields();
        for (Field f: field) System.out.println(f);
    }
}
И вот, как не странно, результат вызова:

private int A.c
public int A.d
То есть, всё-таки возвращают поля класса-родителя?
Lo Уровень 35 Expert
7 ноября 2023
Небольшая каша в голове, но в целом все понятно, хорошо статья написана, мне понравился принцип это то-то - делает то-то.
partiec Уровень 33
4 ноября 2023
годицца +
Denis Gritsay Уровень 35
10 сентября 2023
так и не понял, зачем ломать инкапсуляцию, какая практическая цель этого в написании программ ???
Anonymous #2502407 Уровень 2
29 июля 2023
MyClass.class.getName() Это забавно, для того чтобы узнать имя класса, надо знать имя класса 😂
Oleksandr Klymenko Уровень 13
1 мая 2023
Thanks all for your responses! My LinkedIn profile: https://www.linkedin.com/in/zephyr-ventum
Владимир Уровень 108
23 декабря 2022
А если в приватном конструкторе по умолчанию нет синглтона? На стэк оверфлоу я прочитал, что через Class.forName() все-равно создается экземпляр, не зависимо от модификатора
13 декабря 2022
Спасибо! До прочтения этой статьи я уж подумывал завязывать с Java
Kurama Уровень 50
26 октября 2022
Я часто встречаю подобное: "Самое время раскомментировать наш конструктор. Как и ожидалось, newInstance() не находит конструктор по умолчанию и больше не работает." Например, интерфейс Externalizable (сериализация) работает, только если есть конструктор по умолчанию, потому что не использует рефлексию в отличие от Serialilzable (хотя, казалось бы, используешь Externalizable и можешь даже не знать о рефлексии, а тут какое-то новое обязательное условие). Но мой препод на курсах говорил всегда создавать доп конструктор без параметров (потому что, когда ты создаешь свой конструктор, скрытый конструктор по умолчанию удаляется), это нужно для фреймворков или чего-то подобного (это требование было на том же уровне, что и переопределение toString() и hashcode()...)
Igor Petrashevsky Уровень 47
31 августа 2022
Годный материал. У автора хорошее умение объяснять и раскладывать по полочкам.