Може би сте срещали понятието "отражение" в обикновения живот. Тази дума обикновено се отнася до процеса на изучаване на себе си. В програмирането има подобно meaning - това е механизъм за анализиране на данни за програма и дори за промяна на структурата и поведението на програмата, докато програмата работи. Примери за размисъл - 1 Важното тук е, че правим това по време на изпълнение, а не по време на компorране. Но защо да изследвате codeа по време на изпълнение? В края на краищата вече можете да прочетете codeа :/ Има причина идеята за размисъл да не е ясна: до този момент винаги сте знаели с кои класове работите. Например, можете да напишете 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:
  1. Идентифицирайте/определете класа на обект.
  2. Получете информация за модификатори на класове, полета, методи, константи, конструктори и суперкласове.
  3. Разберете кои методи принадлежат към внедрен интерфейс(и).
  4. Създайте екземпляр на клас, чието име на клас не е известно, докато програмата не бъде изпълнена.
  5. Вземете и задайте стойността на поле на екземпляр по име.
  6. Извикайте метод на екземпляр по име.
Впечатляващ списък, а? :) Забележка:механизмът за отразяване може да направи всички тези неща "в движение", независимо от типа обект, който предаваме на нашия codeов анализатор! Нека проучим възможностите на 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класа. Тук четем името на клас, чийто обект ще създадем от конзолата. Програмата разпознава името на класа, чийто обект ще бъде създаден. Примери за размисъл - 3За краткост пропуснахме правилния 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. Има три основни недостатъка:
  1. Изпълнението е по-лошо. Методите, наречени използване на отражение, имат по-лоша производителност от методите, наречени по нормален начин.

  2. Има ограничения за сигурност. Механизмът за отразяване ни позволява да променим поведението на програмата по време на изпълнение. Но на работното си място, когато работите върху реален проект, може да се сблъскате с ограничения, които не позволяват това.

  3. Риск от разкриване на вътрешна информация. Важно е да се разбере, че отражението е пряко нарушение на принципа на капсулиране: то ни позволява достъп до частни полета, методи и т.н. Не мисля, че трябва да споменавам, че трябва да се прибегне до пряко и грубо нарушение на принципите на ООП само в най-крайните случаи, когато няма други начини за разрешаване на проблем по причини извън вашия контрол.

Използвайте отражението разумно и само в ситуации, в които не може да бъде избегнато, и не забравяйте за неговите недостатъци. С това нашият урок приключи. Оказа се доста дълго, но днес научихте много :)