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:
- Узнать / определить класс объекта.
- Получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах.
- Выяснить, какие методы принадлежат реализуемому интерфейсу / интерфейсам.
- Создать экземпляр класса, когда имя класса неизвестно до момента выполнения программы.
- Получить и установить значение поля объекта по имени.
- Вызвать метод объекта по имени.
Как узнать / определить класс объекта
Начнем с основ. Входная точка в механизм рефлексии 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()
мы узнаем все, что только можно. Но это не все возможности, которые у нас есть при работе с рефлексией.
Не будем ограничиваться простым наблюдением и перейдем к активным действиям! :)
Как создать экземпляр класса, если имя класса неизвестно до выполнения программы
Начнем с конструктора по умолчанию. Его пока нет в нашем классе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
.
Здесь мы считываем имя класса, объект которого будем создавать, с консоли. Запущенная программа узнает имя класса, объект которого ей предстоит создать.
Для краткости мы опустили код правильной обработки исключений, чтобы он не занял больше места, чем сам пример. В реальной программе, конечно, обязательно стоит обработать ситуации ввода некорректных имен и т.д.
Конструктор по умолчанию — штука довольно простая, поэтому и создать экземпляр класса с его помощью, как видишь, несложно :) А с помощью метода 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 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. В сложных и неожиданных ситуациях (как в примерах с классом из закрытой библиотеки) она действительно может нас сильно выручить.
Однако, как и всякая большая сила, она подразумевает и большую ответственность.
О недостатках рефлексии написано в специальном разделе на сайте Oracle.
Можно выделить три главных минуса:
Производительность снижается. У методов, которые вызываются с помощью рефлексии, меньшая производительность по сравнению с методами, которые вызываются обычным способом.
Есть ограничения по безопасности. Механизм рефлексии позволяет менять поведение программы во время выполнения (runtime). Но в твоем рабочем окружении на реальном проекте могут быть ограничения, не позволяющие этого делать.
Риск раскрытия внутренней информации. Важно понимать, что использование рефлексии напрямую нарушает принцип инкапсуляции: позволяет нам получить доступ к приватным полям, методам и т.д. Думаю, не стоит объяснять, что к прямому и грубому нарушению принципов ООП стоит прибегать только в самых крайних случаях, когда иных способов решить задачу не существует по не зависящим от тебя причинам.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
подковыметоды).