Понятие «рефлексия» ты мог встречать в обычной жизни. Обычно этим словом называют процесс изучения себя. В программировании оно имеет похожий смысл — это механизм исследования данных о программе, а также изменения структуры и поведения программы во время ее выполнения. Примеры использования Reflection - 1Здесь важно: именно во время выполнения, а не во время компиляции. Но зачем исследовать код во время выполнения? Ты же и так видишь его :/ Идея рефлексии может быть не сразу понятна по одной причине: до этого момента ты всегда знал классы, с которыми работаешь. Ну, например, ты мог написать класс Cat:
package learn.javarush;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Ты все знаешь о нем, видишь, какие у него есть поля и методы. Наверняка сможешь создать для удобства систему наследования с общим классом Animal, если вдруг в программе нужны будут другие классы животных. Ранее мы даже создавали класс ветеринарной клиники, в который можно было передать объект-родитель Animal, а программа лечила животное в зависимости от того, собака это или кошка. Несмотря на то, что эти задачи не очень-то простые, программа узнает всю необходимую ей информацию о классах во время компиляции. Поэтому когда ты в методе main() передаешь объект Cat в методы класса ветеринарной клиники, программа уже знает, что это кошка, а не собака. А теперь давай представим, что перед нами стоит другая задача. Наша цель — написать анализатор кода. Нам нужно создать класс CodeAnalyzer с единственным методом — void analyzeObject(Object o). Этот метод должен:
  • определить, какого класса объект ему передали и вывести название класса в консоль;
  • определить названия всех полей этого класса, включая приватные, и вывести их в консоль;
  • определить названия всех методов этого класса, включая приватные, и вывести их в консоль.
Выглядеть это будет примерно так:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит объект o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
Теперь разница между этой задачей и остальными задачами, которые ты решал до этого, видна. В данном случае сложность заключается в том, что ни ты, ни программа не знаете, что именно передастся в метод analyzeClass(). Ты напишешь программу, ею начнут пользоваться другие программисты, которые могут передать в этот метод вообще что угодно — любой стандартный Java-класс или любой написанный ими класс. У этого класса может быть сколько угодно переменных и методов. Иными словами, в данном случае мы (и наша программа) не имеем ни малейшего представления о том, с какими классами мы будем работать. И тем не менее, мы должны решить эту задачу. И здесь нам на помощь приходит стандартная библиотека Java — Java Reflection API. Reflection API — мощное средство языка. В официальной документации Oracle написано, что этот механизм рекомендуют использовать только опытным программистам, которые очень хорошо понимают, что делают. Скоро ты поймешь, с чего вдруг нам заранее дают такие предостережения :) Вот список того, что можно сделать при помощи Reflection API:
  1. Узнать / определить класс объекта.
  2. Получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах.
  3. Выяснить, какие методы принадлежат реализуемому интерфейсу / интерфейсам.
  4. Создать экземпляр класса, когда имя класса неизвестно до момента выполнения программы.
  5. Получить и установить значение поля объекта по имени.
  6. Вызвать метод объекта по имени.
Впечатляющий список, да? :) Обрати внимание: все это механизм рефлексии способен делать «на ходу» независимо от того, объект какого класса мы передадим в наш анализатор кода! Давай рассмотрим возможности Reflection API на примерах.

Как узнать / определить класс объекта

Начнем с основ. Входная точка в механизм рефлексии Java — класс Class. Да, выглядит действительно забавно, но на то она и рефлексия :) С помощью класса Class мы, прежде всего, определяем класс любого объекта, переданного в наш метод. Давай попробуем это сделать:
import learn.javarush.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Вывод в консоль:
class learn.javarush.Cat
Обрати внимание на две вещи. Во-первых, мы специально положили класс Cat в отдельный пакет learn.javarush; теперь ты видишь, что getClass() возвращает полное имя класса. Во-вторых, мы назвали нашу переменную clazz. Выглядит немного странно. Ее, конечно, стоило бы назвать «class», но «class» — зарезервированное слово в языке Java, и компилятор не позволит так называть переменные. Пришлось выкручиваться :) Что ж, для начала неплохо! Что у нас там было еще в списке возможностей? Примеры использования Reflection - 2

Как получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах

Это уже поинтереснее! В текущем классе у нас нет констант и родительского класса. Давай добавим их для полноты картины. Создадим самый простой родительский класс Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
И добавим в наш класс Cat наследование от Animal и одну константу:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
Теперь у нас полный набор! Давай испытаем возможности рефлексии :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Имя класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Вот что мы получим в консоли:
Имя класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
Как много подробной информации о классе мы получили! Причем не только о публичных, но и о приватных частях. Обрати внимание: private-переменные тоже отображены в списке. Собственно, «анализ» класса можно на этом считать завершенным: теперь с помощью метода analyzeObject() мы узнаем все, что только можно. Но это не все возможности, которые у нас есть при работе с рефлексией. Не будем ограничиваться простым наблюдением и перейдем к активным действиям! :)

Как создать экземпляр класса, если имя класса неизвестно до выполнения программы

Начнем с конструктора по умолчанию. Его пока нет в нашем классе Cat, поэтому давай добавим его:
public Cat() {

}
Вот как будет выглядеть код для создания объекта Cat с помощью рефлексии (метод createCat()):
import learn.javarush.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Вводим в консоль:
learn.javarush.Cat
Вывод в консоль:
Cat{name='null', age=0}
Это не ошибка: значения name и age отображаются в консоли из-за того, что мы запрограммировали их вывод в методе toString() класса Cat. Здесь мы считываем имя класса, объект которого будем создавать, с консоли. Запущенная программа узнает имя класса, объект которого ей предстоит создать. Примеры использования Reflection - 3Для краткости мы опустили код правильной обработки исключений, чтобы он не занял больше места, чем сам пример. В реальной программе, конечно, обязательно стоит обработать ситуации ввода некорректных имен и т.д. Конструктор по умолчанию — штука довольно простая, поэтому и создать экземпляр класса с его помощью, как видишь, несложно :) А с помощью метода newInstance() мы создаем новый объект этого класса. Другое дело, если конструктор класса Cat будет принимать на вход параметры. Удалим дефолтный конструктор из класса и попробуем запустить наш код снова.
null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Что-то пошло не так! Мы получили ошибку, потому что вызвали метод для создания объекта через конструктор по умолчанию. А ведь такого конструктора у нас теперь нет. Значит при работе метода newInstance() механизм рефлексии будет использовать наш старый конструктор с двумя параметрами:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
А с параметрами-то мы ничего не сделали, как будто вообще забыли о них! Чтобы передать их в конструктор с помощью рефлексии, придется немного «похимичить»:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Вывод в консоль:
Cat{name='Barsik', age=6}
Давай рассмотрим подробнее, что в нашей программе происходит. Мы создали массив объектов Class.
Class[] catClassParams = {String.class, int.class};
Они соответствуют параметрам нашего конструктора (у нас там как раз параметры String и int). Мы передаем их в метод clazz.getConstructor() и получаем доступ к нужному конструктору. После этого остается только вызвать метод newInstance() с нужными параметрами и не забыть явно привести объект к нужному нам классу — Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
В результате наш объект успешно создастся! Вывод в консоль:
Cat{name='Barsik', age=6}
Едем дальше :)

Как получить и установить значение поля объекта по имени

Представь, что ты используешь класс, написанный другим программистом. При этом у тебя нет возможности его редактировать. Например, готовую библиотеку классов, упакованную в JAR. Прочитать код классов ты можешь, а вот поменять — нет. Программист, создавший класс в этой библиотеке (пусть это будет наш старый класс Cat) не выспался перед финальным проектированием и удалил геттеры и сеттеры для поля age. Теперь этот класс попал к тебе. Он полностью соответствует твоим запросам, ведь тебе как раз нужны в программе объекты Cat. Но они нужны тебе с тем самым полем age! Это проблема: достучаться до поля мы не можем, ведь оно имеет модификатор private, а геттеры и сеттеры удалил горе-разработчик этого класса :/ Что ж, рефлексия способна помочь нам и в этой ситуации! Доступ к коду класса Cat у нас есть: мы можем хотя бы узнать, какие у него есть поля и как они называются. Вооружившись этой информацией, решаем нашу проблему:
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           Field age = clazz.getDeclaredField("age");

           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Как и сказано в комментарии, с полем name все просто: для него разработчики класса предоставили сеттер. Создавать объекты из конструкторов по умолчанию ты тоже уже умеешь: для этого есть метод newInstance(). А вот со вторым полем придется повозиться. Давай разбираться, что у нас тут происходит :)
Field name = clazz.getDeclaredField("age");
Здесь мы, используя наш объект Class clazz, получаем доступ к полю age с помощью метода getDeclaredField(). Он дает нам возможность получить поле age в виде объекта Field age. Но этого пока недостаточно, ведь private-полям нельзя просто так присваивать значения. Для этого нужно сделать поле «доступным» с помощью метода setAccessible():
name.setAccessible(true);
Тем полям, для которых это сделано, можно присваивать значения:
age.set(cat, 6);
Как видишь, у нас получился эдакий перевернутый с ног на голову сеттер: мы присваиваем полю Field age его значение, а также передаем ему объект, в который это поле должно быть присвоено. Запустим наш метод main() и увидим:
Cat{name='Barsik', age=6}
Отлично, у нас все получилось! :) Посмотрим, какие еще возможности у нас есть…

Как вызвать метод объекта по имени

Немного изменим ситуацию из предыдущего примера. Допустим, разработчик класса Cat не ошибся с полями — оба доступны, для них есть геттеры и сеттеры, все ок. Проблема в другом: он сделал приватным метод, который нам обязательно нужен:
private void sayMeow() {

   System.out.println("Meow!");
}
В результате мы будем создавать объекты Cat в нашей программе, но не сможем вызвать у них метод sayMeow(). У нас будут кошки, которые не мяукают? Довольно странно :/ Как бы это исправить? И снова Reflection API выручает нас! Название нужного метода мы знаем. Остальное — дело техники:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Barsik", 6);

           clazz = Class.forName(Cat.class.getName());

           Method sayMeow = clazz.getDeclaredMethod("sayMeow");

           sayMeow.setAccessible(true);

           sayMeow.invoke(cat);

       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Здесь мы действуем примерно так же, как в ситуации с доступом к приватному полю. Сначала мы получаем нужный нам метод, который инкапсулирован в объекте класса Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
При помощи getDeclaredMethod() можно «достучаться» в том числе и до приватных методов. Далее мы делаем метод доступным для вызова:
sayMeow.setAccessible(true);
И, наконец, вызываем метод у нужного объекта:
sayMeow.invoke(cat);
Вызов метода у нас тоже выглядит «вызовом наоборот»: мы привыкли указывать объекту на нужным метод с помощью точки (cat.sayMeow()), а при работе с рефлексией передаем методу тот объект, у которого его нужно вызвать. Что же у нас в консоли?
Meow!
Все получилось! :) Теперь ты видишь, какие обширные возможности нам дает механизм рефлексии в Java. В сложных и неожиданных ситуациях (как в примерах с классом из закрытой библиотеки) она действительно может нас сильно выручить. Однако, как и всякая большая сила, она подразумевает и большую ответственность. О недостатках рефлексии написано в специальном разделе на сайте Oracle. Можно выделить три главных минуса:
  1. Производительность снижается. У методов, которые вызываются с помощью рефлексии, меньшая производительность по сравнению с методами, которые вызываются обычным способом.

  2. Есть ограничения по безопасности. Механизм рефлексии позволяет менять поведение программы во время выполнения (runtime). Но в твоем рабочем окружении на реальном проекте могут быть ограничения, не позволяющие этого делать.

  3. Риск раскрытия внутренней информации. Важно понимать, что использование рефлексии напрямую нарушает принцип инкапсуляции: позволяет нам получить доступ к приватным полям, методам и т.д. Думаю, не стоит объяснять, что к прямому и грубому нарушению принципов ООП стоит прибегать только в самых крайних случаях, когда иных способов решить задачу не существует по не зависящим от тебя причинам.

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