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