Cat
клас:
package learn.codegym;
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
(екземпляр на родителски клас) и програмата третира животното по подходящ начин въз основа на това дали е куче or котка. Въпреки че това не са най-простите задачи, програмата е в състояние да научи цялата необходима информация за класовете по време на компorране. Съответно, когато преминете Cat
обект към методите на класа на ветеринарната клиника вmain()
метод, програмата вече знае, че това е котка, а не куче. Сега нека си представим, че сме изпequalsи пред друга задача. Нашата цел е да напишем codeов анализатор. Трябва да създадем CodeAnalyzer
клас с един метод: void analyzeObject(Object o)
. Този метод трябва:
- определя класа на обекта, който му се предава и показва името на класа на конзолата;
- определя имената на всички полета на преминалия клас, включително частните, и ги показва на конзолата;
- определя имената на всички методи на предавания клас, включително частните, и ги показва на конзолата.
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
// Print the name of the class of object o
// Print the names of all variables of this class
// Print the names of all methods of this class
}
}
Сега можем ясно да видим How тази задача се различава от другите задачи, които сте решавали преди. С настоящата ни цел трудността се състои във факта, че нито ние, нито програмата знаем Howво точно ще бъде предадено наanalyzeClass()
метод. Ако напишете такава програма, други програмисти ще започнат да я използват и те могат да предадат всичко на този метод - всеки standardн Java клас or всеки друг клас, който пишат. Предаденият клас може да има произволен брой променливи и методи. С други думи, ние (и нашата програма) нямаме представа с Howви класове ще работим. Но все пак трябва да изпълним тази задача. И тук на помощ ни идва стандартният Java Reflection API. Reflection API е мощен инструмент на езика. Официалната documentация на Oracle препоръчва този механизъм да се използва само от опитни програмисти, които знаят Howво правят. Скоро ще разберете защо даваме този вид предупреждение предварително :) Ето списък с неща, които можете да правите с Reflection API:
- Идентифицирайте/определете класа на обект.
- Получете информация за модификатори на класове, полета, методи, константи, конструктори и суперкласове.
- Разберете кои методи принадлежат към внедрен интерфейс(и).
- Създайте екземпляр на клас, чието име на клас не е известно, докато програмата не бъде изпълнена.
- Вземете и задайте стойността на поле на екземпляр по име.
- Извикайте метод на екземпляр по име.
Как да идентифицираме/определим класа на обект
Да започнем с основите. Входната точка към машината за отразяване на Java е класътClass
. Да, изглежда наистина смешно, но това е отражението :) Използвайки класа Class
, първо определяме класа на всеки обект, предаден на нашия метод. Нека се опитаме да направим това:
import learn.codegym.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("Fluffy", 6));
}
}
Конзолен изход:
class learn.codegym.Cat
Обърнете внимание на две неща. Първо, съзнателно поставихме Cat
класа в отделен learn.codegym
пакет. Сега можете да видите, че getClass()
методът връща пълното име на класа. Второ, наименувахме нашата променлива clazz
. Това изглежда малко странно. Би имало смисъл да го наречем "клас", но "клас" е запазена дума в Java. Компилаторът няма да позволи променливите да се наричат така. Трябваше да го заобиколим няHow :) Не е зле като за начало! Какво друго имахме в този списък с възможности?
Как да получите информация за модификатори на класове, полета, методи, константи, конструктори и суперкласове.
Сега нещата стават по-интересни! В текущия клас нямаме константи or родителски клас. Нека ги добавим, за да създадем пълна картина. Създайте най-простияAnimal
родителски клас:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
И ще направим нашия Cat
клас наследник Animal
и ще добавим една константа:
package learn.codegym;
public class Cat extends Animal {
private static final String ANIMAL_FAMILY = "Feline family";
private String name;
private int age;
// ...the rest of the class
}
Сега имаме пълната картина! Да видим на Howво е способно отражението :)
import learn.codegym.Cat;
import java.util.Arrays;
public class CodeAnalyzer {
public static void analyzeClass(Object o) {
Class clazz = o.getClass();
System.out.println("Class name: " + clazz);
System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
System.out.println("Parent class: " + clazz.getSuperclass());
System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
}
public static void main(String[] args) {
analyzeClass(new Cat("Fluffy", 6));
}
}
Ето Howво виждаме на конзолата:
Class name: class learn.codegym.Cat
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age]
Parent class: class learn.codegym.Animal
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()]
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
Вижте цялата тази подробна информация за класа, която успяхме да получим! И не само публична информация, но и частна информация! Забележка: private
променливите също се показват в списъка. Нашият "анализ" на класа може да се счита за завършен по същество: ние използваме метода, analyzeObject()
за да научим всичко, което можем. Но това не е всичко, което можем да направим с размисъл. Не се ограничаваме до просто наблюдение — ще преминем към предприемане на действия! :)
Как да създадете екземпляр на клас, чието име на клас не е известно, докато програмата не бъде изпълнена.
Нека започнем с конструктора по подразбиране. НашиятCat
клас все още няма такъв, така че нека го добавим:
public Cat() {
}
Ето codeа за създаване на Cat
обект с помощта на отражение ( createCat()
метод):
import learn.codegym.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.codegym.Cat
Конзолен изход:
Cat{name='null', age=0}
Това не е грешка: стойностите на name
и age
се показват на конзолата, защото ние написахме code за извеждането им в toString()
метода на Cat
класа. Тук четем името на клас, чийто обект ще създадем от конзолата. Програмата разпознава името на класа, чийто обект ще бъде създаден. За краткост пропуснахме правилния code за обработка на изключения, който би заел повече място от самия пример. В реална програма, разбира се, трябва да се справите със ситуации, включващи неправилно въведени имена и т.н. Конструкторът по подразбиране е доста прост, така че, Howто можете да видите, лесно е да се използва за създаване на екземпляр на класа :) Използване на newInstance()
метода , създаваме нов обект от този клас. Друг е въпросът далиCat
конструкторът приема аргументи като вход. Нека премахнем конструктора по подразбиране на класа и опитаме да изпълним нашия code отново.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
Нещо се обърка! Получихме грешка, защото извикахме метод за създаване на обект, използвайки конструктора по подразбиране. Но сега нямаме такъв конструктор. Така че, когато newInstance()
методът се изпълнява, механизмът за отразяване използва стария ни конструктор с два параметъра:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
Но ние не направихме нищо с параметрите, сякаш съвсем ги бяхме забравor! Използването на отражение за предаване на аргументи към конструктора изисква малко „креативност“:
import learn.codegym.Cat;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static Cat createCat() {
Class clazz = null;
Cat cat = null;
try {
clazz = Class.forName("learn.codegym.Cat");
Class[] catClassParams = {String.class, int.class};
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 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='Fluffy', age=6}
Нека да разгледаме по-отблизо Howво се случва в нашата програма. Създадохме масив от Class
обекти.
Class[] catClassParams = {String.class, int.class};
Те съответстват на параметрите на нашия конструктор (който има само параметри String
и int
). Предаваме ги на clazz.getConstructor()
метода и получаваме достъп до желания конструктор. След това всичко, което трябва да направим, е да извикаме newInstance()
метода с необходимите аргументи и не забравяйте изрично да прехвърлите обекта към желания тип: Cat
.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Сега нашият обект е успешно създаден! Конзолен изход:
Cat{name='Fluffy', age=6}
Продължавам напред :)
Как да получите и зададете стойността на поле на екземпляр по име.
Представете си, че използвате клас, написан от друг програмист. Освен това нямате възможност да го редактирате. Например готова библиотека от класове, опакована в JAR. Можете да прочетете codeа на класовете, но не можете да го промените. Да предположим, че програмистът, който е създал един от класовете в тази библиотека (нека това да е нашият старCat
клас), не успявайки да спи достатъчно в нощта преди финализирането на дизайна, е премахнал гетъра и сетера за полето age
. Сега този клас дойде при вас. Той отговаря на всичките ви нужди, тъй като просто имате нужда от Cat
обекти във вашата програма. Но ти трябват, за да имаш age
поле! Това е проблем: не можем да стигнем до полето, защото имаprivate
модификатор, а гетърът и сетерът бяха изтрити от лишения от сън разработчик, който създаде класа :/ Е, отражението може да ни помогне в тази ситуация! Имаме достъп до codeа на Cat
класа, така че можем поне да разберем Howви полета има и How се наричат. Въоръжени с тази информация, можем да разрешим нашия проблем:
import learn.codegym.Cat;
import java.lang.reflect.Field;
public class Main {
public static Cat createCat() {
Class clazz = null;
Cat cat = null;
try {
clazz = Class.forName("learn.codegym.Cat");
cat = (Cat) clazz.newInstance();
// We got lucky with the name field, since it has a setter
cat.setName("Fluffy");
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
полето е лесно, тъй като разработчиците на класа предоставиха настройка. Вече знаете How да създавате обекти от конструктори по подразбиране: имаме newInstance()
за това. Но ще трябва да поработим малко с второто поле. Нека да разберем Howво става тук :)
Field age = clazz.getDeclaredField("age");
Тук, използвайки нашия Class clazz
обект, ние осъществяваме достъп до age
полето чрез getDeclaredField()
метода. Това ни позволява да получим възрастовото поле като Field age
обект. Но това не е достатъчно, защото не можем просто да присвоим стойности на private
полета. За да направим това, трябва да направим полето достъпно чрез метода setAccessible()
:
age.setAccessible(true);
След като направим това на поле, можем да присвоим стойност:
age.set(cat, 6);
Както можете да видите, нашият Field age
обект има нещо като сетер отвътре навън, към който предаваме int стойност и обекта, чието поле трябва да бъде присвоено. Изпълняваме нашия main()
метод и виждаме:
Cat{name='Fluffy', age=6}
Отлично! Успяхме! :) Да видим Howво още можем да направим...
Как да извикате метод на екземпляр по име.
Нека леко променим ситуацията в предишния пример. Да кажем, чеCat
разработчикът на класа не е направил грешка с гетерите и сетерите. Всичко е наред в това отношение. Сега проблемът е различен: има метод, от който определено се нуждаем, но разработчикът го направи частен:
private void sayMeow() {
System.out.println("Meow!");
}
Това означава, че ако създадем Cat
обекти в нашата програма, тогава няма да можем да извикаме sayMeow()
метода върху тях. Ще имаме котки, които не мяукат? Това е странно :/ Как ще поправим това? Още веднъж Reflection API ни помага! Знаем името на метода, от който се нуждаем. Всичко останало е чисто технически:
import learn.codegym.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("Fluffy", 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 ни дава. В трудни и неочаквани ситуации (като нашите примери с клас от затворена библиотека) наистина може да ни помогне много. Но, Howто при всяка велика сила, това носи голяма отговорност. Недостатъците на отражението са описани в специален раздел на уебсайта на Oracle. Има три основни недостатъка:
-
Изпълнението е по-лошо. Методите, наречени използване на отражение, имат по-лоша производителност от методите, наречени по нормален начин.
-
Има ограничения за сигурност. Механизмът за отразяване ни позволява да променим поведението на програмата по време на изпълнение. Но на работното си място, когато работите върху реален проект, може да се сблъскате с ограничения, които не позволяват това.
-
Риск от разкриване на вътрешна информация. Важно е да се разбере, че отражението е пряко нарушение на принципа на капсулиране: то ни позволява достъп до частни полета, методи и т.н. Не мисля, че трябва да споменавам, че трябва да се прибегне до пряко и грубо нарушение на принципите на ООП само в най-крайните случаи, когато няма други начини за разрешаване на проблем по причини извън вашия контрол.
GO TO FULL VERSION