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 +
'}';
}
}
Ты все знаешь о нем, видишь, Howие у него есть поля и методы. Наверняка сможешь создать для удобства систему наследования с общим классом Animal
, если вдруг в программе нужны будут другие классы животных. Ранее мы даже создавали класс ветеринарной клиники, в который можно было передать an object-родитель Animal
, а программа лечила животное в зависимости от того, собака это or кошка. Несмотря на то, что эти задачи не очень-то простые, программа узнает всю необходимую ей информацию о классах во время компиляции. Поэтому когда ты в методе main()
передаешь an object Cat
в методы класса ветеринарной клиники, программа уже знает, что это кошка, а не собака. А теперь давай представим, что перед нами стоит другая задача. Наша цель — написать анализатор codeа. Нам нужно создать класс CodeAnalyzer
с единственным методом — void analyzeClass(Object o)
. Этот метод должен:
- определить, Howого класса an object ему передали и вывести название класса в консоль;
- определить названия всех полей этого класса, включая приватные, и вывести их в консоль;
- определить названия всех методов этого класса, включая приватные, и вывести их в консоль.
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
//Вывести название класса, к которому принадлежит an object o
//Вывести названия всех переменных этого класса
//Вывести названия всех методов этого класса
}
}
Теперь разница между этой задачей и остальными задачами, которые ты решал до этого, видна. В данном случае сложность заключается в том, что ни ты, ни программа не знаете, что именно передастся в метод analyzeClass()
. Ты напишешь программу, ею начнут пользоваться другие программисты, которые могут передать в этот метод вообще что угодно — любой стандартный Java-класс or любой написанный ими класс. У этого класса может быть сколько угодно переменных и методов. Иными словами, в данном случае мы (и наша программа) не имеем ни малейшего представления о том, с Howими классами мы будем работать. И тем не менее, мы должны решить эту задачу. И здесь нам на помощь приходит стандартная библиотека Java — Java Reflection API. Reflection API — мощное средство языка. В официальной documentации Oracle написано, что этот механизм рекомендуют использовать только опытным программистам, которые очень хорошо понимают, что делают. Скоро ты поймешь, с чего вдруг нам заранее дают такие предостережения :) Вот список того, что можно сделать при помощи Reflection API:
- Узнать / определить класс an object.
- Получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах.
- Выяснить, Howие методы принадлежат реализуемому интерфейсу / интерфейсам.
- Создать экземпляр класса, когда Name класса неизвестно до момента выполнения программы.
- Получить и установить meaning поля an object по имени.
- Вызвать метод an object по имени.
Как узнать / определить класс an object
Начнем с основ. Входная точка в механизм рефлексии Java — классClass
. Да, выглядит действительно забавно, но на то она и рефлексия :) С помощью класса Class
мы, прежде всего, определяем класс любого an object, переданного в наш метод. Давай попробуем это сделать:
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
Обрати внимание на две вещи. Во-первых, мы специально положor класс Cat
в отдельный пакет learn.javarush;
теперь ты видишь, что getClass()
возвращает полное Name класса. Во-вторых, мы назвали нашу переменную clazz
. Выглядит немного странно. Ее, конечно, стоило бы назвать «class», но «class» — зарезервированное слово в языке Java, и компилятор не позволит так называть переменные. Пришлось выкручиваться :) What ж, для начала неплохо! What у нас там было еще в списке возможностей?
Как получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах
Это уже поинтереснее! В текущем классе у нас нет констант и родительского класса. Давай добавим их для полноты картины. Создадим самый простой родительский класс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("Name класса: " + 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));
}
}
Вот что мы получим в консоли:
Name класса: 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)]
Как много подробной информации о классе мы получor! Причем не только о публичных, но и о приватных частях. Обрати внимание: private
-переменные тоже отображены в списке. Собственно, «анализ» класса можно на этом считать завершенным: теперь с помощью метода analyzeClass()
мы узнаем все, что только можно. Но это не все возможности, которые у нас есть при работе с рефлексией. Не будем ограничиваться простым наблюдением и перейдем к активным действиям! :)
Как создать экземпляр класса, если Name класса неизвестно до выполнения программы
Начнем с конструктора по умолчанию. Его пока нет в нашем классеCat
, поэтому давай добавим его:
public Cat() {
}
Вот How будет выглядеть code для создания an object 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
. Здесь мы считываем Name класса, an object которого будем создавать, с консоли. Запущенная программа узнает Name класса, an object которого ей предстоит создать. Для краткости мы опустor code правильной обработки исключений, чтобы он не занял больше места, чем сам пример. В реальной программе, конечно, обязательно стоит обработать ситуации ввода некорректных имен и т.д. Конструктор по умолчанию — штука довольно простая, поэтому и создать экземпляр класса с его помощью, How видишь, несложно :) А с помощью метода newInstance()
мы создаем новый an object этого класса. Другое дело, если конструктор класса Cat
будет принимать на вход параметры. Удалим дефолтный конструктор из класса и попробуем запустить наш code снова.
null
java.lang.InstantiationException: learn.javarush.Cat
at java.lang.Class.newInstance(Class.java:427)
What-то пошло не так! Мы получor ошибку, потому что вызвали метод для создания an object через конструктор по умолчанию. А ведь такого конструктора у нас теперь нет. Значит при работе метода newInstance()
механизм рефлексии будет использовать наш старый конструктор с двумя параметрами:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
А с параметрами-то мы ничего не сделали, How будто вообще забыли о них! Whatбы передать их в конструктор с помощью рефлексии, придется немного «похимичить»:
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}
Давай рассмотрим подробнее, что в нашей программе происходит. Мы создали массив an objectов Class
.
Class[] catClassParams = {String.class, int.class};
Они соответствуют параметрам нашего конструктора (у нас там How раз параметры String
и int
). Мы передаем их в метод clazz.getConstructor()
и получаем доступ к нужному конструктору. После этого остается только вызвать метод newInstance()
с нужными параметрами и не забыть явно привести an object к нужному нам классу — Cat
.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
В результате наш an object успешно создастся! Вывод в консоль:
Cat{name='Barsik', age=6}
Едем дальше :)
Как получить и установить meaning поля an object по имени
Представь, что ты используешь класс, написанный другим программистом. При этом у тебя нет возможности его редактировать. Например, готовую библиотеку классов, упакованную в JAR. Прочитать code классов ты можешь, а вот поменять — нет. Программист, создавший класс в этой библиотеке (пусть это будет наш старый классCat
) не выспался перед финальным проектированием и удалил геттеры и сеттеры для поля age
. Теперь этот класс попал к тебе. Он fully соответствует твоим requestам, ведь тебе How раз нужны в программе an objectы Cat
. Но они нужны тебе с тем самым полем age
! Это проблема: достучаться до поля мы не можем, ведь оно имеет модификатор private
, а геттеры и сеттеры удалил горе-разработчик этого класса :/ What ж, рефлексия способна помочь нам и в этой ситуации! Доступ к codeу класса Cat
у нас есть: мы можем хотя бы узнать, Howие у него есть поля и How они называются. Вооружившись этой информацией, решаем нашу проблему:
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
все просто: для него разработчики класса предоставor сеттер. Создавать an objectы из конструкторов по умолчанию ты тоже уже умеешь: для этого есть метод newInstance()
. А вот со вторым полем придется повозиться. Давай разбираться, что у нас тут происходит :)
Field age = clazz.getDeclaredField("age");
Здесь мы, используя наш an object Class clazz
, получаем доступ к полю age
с помощью метода getDeclaredField()
. Он дает нам возможность получить поле age в виде an object Field age
. Но этого пока недостаточно, ведь private
-полям нельзя просто так присваивать значения. Для этого нужно сделать поле «доступным» с помощью метода setAccessible()
:
age.setAccessible(true);
Тем полям, для которых это сделано, можно присваивать значения:
age.set(cat, 6);
Как видишь, у нас получился эдакий перевернутый с ног на голову сеттер: мы присваиваем полю Field age
его meaning, а также передаем ему an object, в который это поле должно быть присвоено. Запустим наш метод main()
и увидим:
Cat{name='Barsik', age=6}
Отлично, у нас все получилось! :) Посмотрим, Howие еще возможности у нас есть…
Как вызвать метод an object по имени
Немного изменим ситуацию из предыдущего примера. Допустим, разработчик классаCat
не ошибся с полями — оба доступны, для них есть геттеры и сеттеры, все ок. Проблема в другом: он сделал приватным метод, который нам обязательно нужен:
private void sayMeow() {
System.out.println("Meow!");
}
В результате мы будем создавать an objectы 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();
}
}
Здесь мы действуем примерно так же, How в ситуации с доступом к приватному полю. Сначала мы получаем нужный нам метод, который инкапсулирован в an objectе класса Method
:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
При помощи getDeclaredMethod()
можно «достучаться» в том числе и до приватных методов. Далее мы делаем метод доступным для вызова:
sayMeow.setAccessible(true);
И, наконец, вызываем метод у нужного an object:
sayMeow.invoke(cat);
Вызов метода у нас тоже выглядит «вызовом наоборот»: мы привыкли указывать an objectу на нужным метод с помощью точки (cat.sayMeow()
), а при работе с рефлексией передаем методу тот an object, у которого его нужно вызвать. What же у нас в консоли?
Meow!
Все получилось! :) Теперь ты видишь, Howие обширные возможности нам дает механизм рефлексии в Java. В сложных и неожиданных ситуациях (How в примерах с классом из закрытой библиотеки) она действительно может нас сильно выручить. Однако, How и всякая большая сила, она подразумевает и большую ответственность. О недостатках рефлексии написано в специальном разделе на сайте Oracle. Можно выделить три главных минуса:
-
Производительность снижается. У методов, которые вызываются с помощью рефлексии, меньшая производительность по сравнению с методами, которые вызываются обычным способом.
-
Есть ограничения по безопасности. Механизм рефлексии позволяет менять поведение программы во время выполнения (runtime). Но в твоем рабочем окружении на реальном проекте могут быть ограничения, не позволяющие этого делать.
-
Риск раскрытия внутренней информации. Важно понимать, что использование рефлексии напрямую нарушает принцип инкапсуляции: позволяет нам получить доступ к приватным полям, методам и т.д. Думаю, не стоит объяснять, что к прямому и грубому нарушению принципов ООП стоит прибегать только в самых крайних случаях, когда иных способов решить задачу не существует по не зависящим от тебя причинам.
GO TO FULL VERSION