Class, а остальные инструменты лежат в пакете java.lang.reflect.Кратко
- Рефлексия работает во время выполнения программы, а не во время компиляции. Она нужна там, где класс заранее неизвестен.
- Точка входа это объект
Class. Его берут черезo.getClass()у готового объекта или черезClass.forName("имя")по строке. getDeclaredFields()иgetDeclaredMethods()возвращают все поля и методы класса, включая приватные.- Объект можно создать по имени класса. Если у конструктора есть параметры, сначала получают нужный конструктор методом
getConstructor(). setAccessible(true)снимает защитуprivate: после этого поле пишут черезset(), а метод вызывают черезinvoke().- Плата за это: падение производительности, ограничения безопасности и прямое нарушение инкапсуляции. Рефлексию берут только тогда, когда иначе задачу не решить.
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 analyzeClass(Object o).
Этот метод должен:
- определить, какого класса объект ему передали и вывести название класса в консоль;
- определить названия всех полей этого класса, включая приватные, и вывести их в консоль;
- определить названия всех методов этого класса, включая приватные, и вывести их в консоль.
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
//Вывести название класса, к которому принадлежит объект o
//Вывести названия всех переменных этого класса
//Вывести названия всех методов этого класса
}
}
Теперь разница между этой задачей и остальными задачами, которые ты решал до этого, видна.
В данном случае сложность заключается в том, что ни ты, ни программа не знаете, что именно передастся в метод analyzeClass().
Ты напишешь программу, ею начнут пользоваться другие программисты, которые могут передать в этот метод вообще что угодно: любой стандартный Java-класс или любой написанный ими класс. У этого класса может быть сколько угодно переменных и методов.
Иными словами, в данном случае мы (и наша программа) не имеем ни малейшего представления о том, с какими классами мы будем работать.
И тем не менее, мы должны решить эту задачу.
И здесь нам на помощь приходит стандартная библиотека Java, Java Reflection API.
Reflection API это мощное средство языка. В официальной документации Oracle написано, что этот механизм рекомендуют использовать только опытным программистам, которые очень хорошо понимают, что делают.
Скоро ты поймешь, с чего вдруг нам заранее дают такие предостережения :)Что умеет Reflection API
Вот список того, что можно сделать при помощи 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, и компилятор не позволит так называть переменные. Пришлось выкручиваться :)
Что ж, для начала неплохо! Что у нас там было еще в списке возможностей?Как получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах
Это уже поинтереснее! В текущем классе у нас нет констант и родительского класса. Давай добавим их для полноты картины. Создадим самый простой родительский класс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-переменные тоже отображены в списке.
Собственно, «анализ» класса можно на этом считать завершенным: теперь с помощью метода analyzeClass() мы узнаем все, что только можно. Но это не все возможности, которые у нас есть при работе с рефлексией.Как узнать, какие методы принадлежат реализуемому интерфейсу
Третий пункт нашего списка мы пока пропустили, исправимся. Рефлексия умеет показывать и то, какие интерфейсы реализует класс, и какие методы в них объявлены. Добавим нашему коту простой интерфейс:package learn.javarush;
public interface Pet {
void play();
}
И укажем его в объявлении класса Cat:
package learn.javarush;
public class Cat extends Animal implements Pet {
//...остальная часть класса
@Override
public void play() {
System.out.println("Кот играет с бантиком!");
}
}
Теперь спросим у класса, что он реализует:
import learn.javarush.Cat;
import java.util.Arrays;
public class CodeAnalyzer {
public static void analyzeInterfaces(Object o) {
Class clazz = o.getClass();
for (Class anInterface : clazz.getInterfaces()) {
System.out.println("Интерфейс: " + anInterface.getName());
System.out.println("Его методы: " + Arrays.toString(anInterface.getDeclaredMethods()));
}
}
public static void main(String[] args) {
analyzeInterfaces(new Cat("Barsik", 6));
}
}
Вывод в консоль:
Интерфейс: learn.javarush.Pet
Его методы: [public abstract void learn.javarush.Pet.play()]
Метод getInterfaces() возвращает массив объектов Class, по одному на каждый интерфейс. Дальше с каждым из них работаем как с обычным объектом Class: getDeclaredMethods() покажет объявленные в интерфейсе методы.
Обрати внимание: методы интерфейса помечены как abstract, ведь тела у них нет. И еще один нюанс: getInterfaces() показывает только те интерфейсы, которые указаны в объявлении самого класса. Те, что достались от родителя, в этот список не попадут.
Не будем ограничиваться простым наблюдением и перейдем к активным действиям! :)Как создать экземпляр класса, если имя класса неизвестно до выполнения программы
Начнем с конструктора по умолчанию. Его пока нет в нашем классеCat, поэтому давай добавим его:
public Cat() {
}
Вот как будет выглядеть код для создания объекта Cat с помощью рефлексии (метод createCat()):
import learn.javarush.Cat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static Cat createCat() throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String className = reader.readLine();
Class clazz = Class.forName(className);
Cat cat = (Cat) clazz.getDeclaredConstructor().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.getDeclaredConstructor().newInstance();
return result;
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
System.out.println(createCat());
}
}
Вводим в консоль:
learn.javarush.Cat
Вывод в консоль:
Cat{name='null', age=0}
Это не ошибка: значения name и age отображаются в консоли из-за того, что мы запрограммировали их вывод в методе toString() класса Cat.
Здесь мы считываем имя класса, объект которого будем создавать, с консоли. Запущенная программа узнает имя класса, объект которого ей предстоит создать.
getDeclaredConstructor().newInstance() мы создаем новый объект этого класса.
Небольшое пояснение, почему конструкция такая длинная. Раньше объект создавали короче, через clazz.newInstance(), но этот метод помечен устаревшим начиная с Java 9: он пробрасывает наружу любые исключения конструктора, включая проверяемые, в обход проверок компилятора. Современная замена это clazz.getDeclaredConstructor().newInstance(), ее мы и используем. А вот newInstance() у объекта Constructor, который встретится ниже, устаревшим не считается.Конструктор с параметрами
Другое дело, если конструктор классаCat будет принимать на вход параметры.
Удалим дефолтный конструктор из класса и попробуем запустить наш код снова.
java.lang.NoSuchMethodException: learn.javarush.Cat.<init>()
Что-то пошло не так!
Мы получили ошибку, потому что вызвали метод для создания объекта через конструктор по умолчанию. А ведь такого конструктора у нас теперь нет.
Значит, придется заставить механизм рефлексии использовать наш старый конструктор с двумя параметрами:
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;
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");
cat = (Cat) clazz.getDeclaredConstructor().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();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return cat;
}
public static void main(String[] args) {
System.out.println(createCat());
}
}
Как и сказано в комментарии, с полем name все просто: для него разработчики класса предоставили сеттер. Создавать объекты из конструкторов по умолчанию ты тоже уже умеешь: для этого есть getDeclaredConstructor().newInstance().
А вот со вторым полем придется повозиться. Давай разбираться, что у нас тут происходит :)
Field age = clazz.getDeclaredField("age");
Здесь мы, используя наш объект Class clazz, получаем доступ к полю age с помощью метода getDeclaredField(). Он дает нам возможность получить поле age в виде объекта Field age.
Но этого пока недостаточно, ведь private-полям нельзя просто так присваивать значения. Для этого нужно сделать поле «доступным» с помощью метода setAccessible():
age.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. В сложных и неожиданных ситуациях (как в примерах с классом из закрытой библиотеки) она действительно может нас сильно выручить.Основные методы Reflection API: шпаргалка
Соберем все методы, которые встретились в примерах, в одну таблицу:| Метод | Что делает |
|---|---|
o.getClass() | возвращает объект Class для уже существующего объекта |
Class.forName("пакет.Класс") | получает объект Class по строке с полным именем класса |
getDeclaredFields() | все поля класса, включая приватные |
getDeclaredMethods() | все методы класса, включая приватные |
getSuperclass() | родительский класс |
getInterfaces() | интерфейсы, указанные в объявлении самого класса |
getConstructors() | публичные конструкторы класса |
getConstructor(типы) | конкретный конструктор по типам его параметров |
getDeclaredConstructor() | конструктор без параметров, современная замена устаревшему Class.newInstance() |
getDeclaredField("имя") | одно поле по имени, в виде объекта Field |
getDeclaredMethod("имя") | один метод по имени, в виде объекта Method |
setAccessible(true) | снимает защиту private у поля или метода |
field.set(объект, значение) | записывает значение в поле конкретного объекта |
method.invoke(объект) | вызывает метод у конкретного объекта |
constructor.newInstance(аргументы) | создает объект через полученный конструктор |
Недостатки рефлексии
Однако, как и всякая большая сила, она подразумевает и большую ответственность. О недостатках рефлексии написано в специальном разделе на сайте Oracle. Можно выделить три главных минуса:Производительность снижается. У методов, которые вызываются с помощью рефлексии, меньшая производительность по сравнению с методами, которые вызываются обычным способом.
Есть ограничения по безопасности. Механизм рефлексии позволяет менять поведение программы во время выполнения (runtime). Но в твоем рабочем окружении на реальном проекте могут быть ограничения, не позволяющие этого делать.
Риск раскрытия внутренней информации. Важно понимать, что использование рефлексии напрямую нарушает принцип инкапсуляции: позволяет нам получить доступ к приватным полям, методам и т.д. Думаю, не стоит объяснять, что к прямому и грубому нарушению принципов ООП стоит прибегать только в самых крайних случаях, когда иных способов решить задачу не существует по не зависящим от тебя причинам.
setAccessible(true) для чужого класса из стандартной библиотеки выбросит InaccessibleObjectException: такой пакет придется явно открыть параметром --add-opens. На твои собственные классы, как в примерах выше, это ограничение не распространяется.
Используй механизм рефлексии с умом и только в тех ситуациях, когда этого не избежать, и не забывай о его недостатках.
На этом наша лекция подошла к концу! Она получилась довольно большой, но сегодня ты узнал много нового :)
подковыметоды).