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. Спасибо за внимание!
Комментарии (117)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Lo
Уровень 35
Expert
7 ноября 2023, 13:37
Небольшая каша в голове, но в целом все понятно, хорошо статья написана, мне понравился принцип это то-то - делает то-то.
partiec
Уровень 33
4 ноября 2023, 17:36
годицца +
Denis Gritsay
Уровень 35
10 сентября 2023, 21:47
так и не понял, зачем ломать инкапсуляцию, какая практическая цель этого в написании программ ???
Рин Java Developer
11 декабря 2023, 04:42
Например, для сериализации Я как раз пришла по ссылке из статьи про сериализацию Насколько я вижу, во-первых, метод readResolve как раз используется при сериализации рефлексией. Это раз. Во-вторых, иначе никак не достать и не записать приватные данные, к которым нет доступа вне класса. Это вещи, которые прояснились в моей голове при чтении этой статьи Если я ошибаюсь, может, кто-нибудь меня поправит
Anonymous #2502407
Уровень 2
29 июля 2023, 08:58
MyClass.class.getName() Это забавно, для того чтобы узнать имя класса, надо знать имя класса 😂
Anonymous #3193220
Уровень 1
30 августа 2023, 14:29
Не узнать, а получить для дальнейшего использования программой. Две большие разницы.
Oleksandr Klymenko Lead Software Engineer
1 мая 2023, 15:55
Thanks all for your responses! My LinkedIn profile: https://www.linkedin.com/in/zephyr-ventum
Владимир
Уровень 108
23 декабря 2022, 12:20
А если в приватном конструкторе по умолчанию нет синглтона? На стэк оверфлоу я прочитал, что через Class.forName() все-равно создается экземпляр, не зависимо от модификатора
13 декабря 2022, 15:09
Спасибо! До прочтения этой статьи я уж подумывал завязывать с Java
Kurama
Уровень 50
26 октября 2022, 19:18
Я часто встречаю подобное: "Самое время раскомментировать наш конструктор. Как и ожидалось, newInstance() не находит конструктор по умолчанию и больше не работает." Например, интерфейс Externalizable (сериализация) работает, только если есть конструктор по умолчанию, потому что не использует рефлексию в отличие от Serialilzable (хотя, казалось бы, используешь Externalizable и можешь даже не знать о рефлексии, а тут какое-то новое обязательное условие). Но мой препод на курсах говорил всегда создавать доп конструктор без параметров (потому что, когда ты создаешь свой конструктор, скрытый конструктор по умолчанию удаляется), это нужно для фреймворков или чего-то подобного (это требование было на том же уровне, что и переопределение toString() и hashcode()...)
Dmitry Student в Home
18 января 2023, 14:48
наоборот же Externalizable не использует рефлексию и поэтому нужен конструктор по умолчанию.
Kurama
Уровень 50
18 января 2023, 15:56
Да, всё так, я проходил это ещё на джава кор, детали работы помню, а в теории напутал... Ща подправлю
Igor Petrashevsky
Уровень 47
31 августа 2022, 23:33
Годный материал. У автора хорошее умение объяснять и раскладывать по полочкам.
Игорь
Уровень 29
10 сентября 2022, 11:26
дело за малым, запомнить и научиться пользоваться))
Дмитрий Java Developer в Есть. Expert
6 июля 2022, 20:01
Примечание: getFields() и getDeclaredFields() не возвращают поля класса-родителя! Это утверждение не верно. Верно: 1. getFields() - вернёт все публичные поля класса и всех его родителей по цепочке. 2. getDeclaredFields() - вернёт все поля класса, не зависимо от модификатора доступа, но не вернёт поля классов-родителей.
Марат Гарипов
Уровень 108
Expert
15 августа 2022, 12:48
Решил убедиться самостоятельно, при помощи тестового кода, добавив при этом getMethods и getDeclaredMethods. Надеюсь будет полезным еще кому-то! public class Solution { void print(Object[] objects) { for (Object object : objects) { System.out.println("-" + object.toString()); } } public static void main(String[] args) { Solution solution = new Solution(); Class<Child> clazz = Child.class; System.out.println("\n***********************\ngetFields Method:\n"); solution.print(clazz.getFields()); System.out.println("\n***********************\ngetDeclaredFields Method:\n"); solution.print(clazz.getDeclaredFields()); System.out.println("\n***********************\ngetMethods Method:\n"); solution.print(clazz.getMethods()); System.out.println("\n***********************\ngetDeclaredMethods Method:\n"); solution.print(clazz.getDeclaredMethods()); } } class Child extends Parent { public int publicChildInt = 7; private boolean privateChildBoolean = true; static String staticChildString = "строка"; final double FINAL_CHILD_DOUBLE_NUMBER = 1.5d; short defaultChildShort = 15; public int publicChildMethod() { return 0; } private void privateChildMethod() { } } class Parent { public long publicParentLong = 1L; private boolean privateParentBoolean = false; static String staticParentString = "другая строка"; final float FINAL_PARENT_FLOAT = 5.9f; byte defaultParentByte = 3; public String publicParentMethod() { return ""; } private void privateParentMethod() { } }
Марат Гарипов
Уровень 108
Expert
15 августа 2022, 12:49
Вывод: *********************** getFields Method: -public int Test.Child.publicChildInt -public long Test.Parent.publicParentLong *********************** getDeclaredFields Method: -public int Test.Child.publicChildInt -private boolean Test.Child.privateChildBoolean -static java.lang.String Test.Child.staticChildString -final double Test.Child.FINAL_CHILD_DOUBLE_NUMBER -short Test.Child.defaultChildShort *********************** getMethods Method: -public int Test.Child.publicChildMethod() -public java.lang.String Test.Parent.publicParentMethod() -public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException -public final void java.lang.Object.wait() throws java.lang.InterruptedException -public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException -public boolean java.lang.Object.equals(java.lang.Object) -public java.lang.String java.lang.Object.toString() -public native int java.lang.Object.hashCode() -public final native java.lang.Class java.lang.Object.getClass() -public final native void java.lang.Object.notify() -public final native void java.lang.Object.notifyAll() *********************** getDeclaredMethods Method: -public int Test.Child.publicChildMethod() -private void Test.Child.privateChildMethod()