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. На твои собственные классы, как в примерах выше, это ограничение не распространяется.
Используй механизм рефлексии с умом и только в тех ситуациях, когда этого не избежать, и не забывай о его недостатках.
На этом наша лекция подошла к концу! Она получилась довольно большой, но сегодня ты узнал много нового :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
подковыметоды).Field name = clazz.getDeclaredField("age");Очепятка, поправьте))