CodeGym /Blog Java /Random-PL /Przykłady refleksji
Autor
Alex Vypirailenko
Java Developer at Toshiba Global Commerce Solutions

Przykłady refleksji

Opublikowano w grupie Random-PL
Być może spotkałeś się z pojęciem „refleksji” w codziennym życiu. Słowo to zwykle odnosi się do procesu studiowania samego siebie. W programowaniu ma to podobne znaczenie — jest to mechanizm analizujący dane o programie, a nawet zmieniający strukturę i zachowanie programu w trakcie jego działania. Przykłady refleksji - 1 Ważne jest to, że robimy to w czasie wykonywania, a nie w czasie kompilacji. Ale po co sprawdzać kod w czasie wykonywania? W końcu możesz już przeczytać kod :/ Jest powód, dla którego idea refleksji może nie być od razu jasna: do tego momentu zawsze wiedziałeś, z którymi klasami pracujesz. Na przykład możesz napisać Catklasę:

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 +
           '}';
}

}
Wiesz o nim wszystko i widzisz, jakie ma pola i metody. Załóżmy, że nagle musisz wprowadzić do programu inne klasy zwierząt. AnimalPrawdopodobnie dla wygody możesz utworzyć strukturę dziedziczenia klas z klasą nadrzędną. Wcześniej stworzyliśmy nawet klasę reprezentującą klinikę weterynaryjną, do której mogliśmy przekazać obiekt Animal(instancję klasy nadrzędnej), a program odpowiednio potraktował zwierzę w zależności od tego, czy był to pies, czy kot. Mimo że nie są to najprostsze zadania, program jest w stanie nauczyć się wszystkich niezbędnych informacji o klasach w czasie kompilacji. W związku z tym, przekazując Catobiekt metodom klasy kliniki weterynaryjnej wmain()metody program już wie, że to kot, a nie pies. Teraz wyobraźmy sobie, że mamy do czynienia z innym zadaniem. Naszym celem jest napisanie analizatora kodu. Musimy stworzyć CodeAnalyzerklasę z jedną metodą: void analyzeObject(Object o). Ta metoda powinna:
  • określić klasę przekazanego mu obiektu i wyświetlić nazwę klasy na konsoli;
  • określić nazwy wszystkich pól przekazanej klasy, w tym prywatnych, i wyświetlić je na konsoli;
  • określić nazwy wszystkich metod przekazanej klasy, w tym prywatnych, i wyświetlić je na konsoli.
Będzie wyglądać mniej więcej tak:

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
   }
  
}
Teraz wyraźnie widać, jak to zadanie różni się od innych zadań, które rozwiązałeś wcześniej. Przy naszym obecnym celu trudność polega na tym, że ani my, ani program nie wiemy, co dokładnie zostanie przekazaneanalyzeClass()metoda. Jeśli napiszesz taki program, inni programiści zaczną go używać i mogą przekazać tej metodzie wszystko — dowolną standardową klasę Java lub jakąkolwiek inną napisaną przez siebie klasę. Przekazana klasa może mieć dowolną liczbę zmiennych i metod. Innymi słowy, my (i nasz program) nie mamy pojęcia, z jakimi klasami będziemy pracować. Ale nadal musimy wykonać to zadanie. I tu z pomocą przychodzi standardowy Java Reflection API. Reflection API to potężne narzędzie języka. Oficjalna dokumentacja Oracle zaleca, aby ten mechanizm był używany tylko przez doświadczonych programistów, którzy wiedzą, co robią. Wkrótce zrozumiesz, dlaczego dajemy tego rodzaju ostrzeżenia z wyprzedzeniem :) Oto lista rzeczy, które możesz zrobić z Reflection API:
  1. Zidentyfikuj/określ klasę obiektu.
  2. Uzyskaj informacje o modyfikatorach klas, polach, metodach, stałych, konstruktorach i klasach nadrzędnych.
  3. Dowiedz się, które metody należą do zaimplementowanych interfejsów.
  4. Utwórz instancję klasy, której nazwa klasy nie jest znana, dopóki program nie zostanie wykonany.
  5. Pobierz i ustaw wartość pola instancji według nazwy.
  6. Wywołaj metodę instancji według nazwy.
Imponująca lista, co? :) Notatka:mechanizm refleksji może robić to wszystko „w locie”, niezależnie od typu obiektu, który przekazujemy do naszego analizatora kodu! Przyjrzyjmy się możliwościom Reflection API, patrząc na kilka przykładów.

Jak zidentyfikować/określić klasę obiektu

Zacznijmy od podstaw. Punktem wejścia do silnika refleksji Java jest Classklasa. Tak, wygląda to naprawdę śmiesznie, ale na tym polega refleksja :) Korzystając z Classklasy, najpierw określamy klasę dowolnego obiektu przekazanego naszej metodzie. Spróbujmy to zrobić:

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));
   }
}
Wyjście konsoli:

class learn.codegym.Cat
Zwróć uwagę na dwie rzeczy. Po pierwsze, celowo umieściliśmy tę Catklasę w osobnym learn.codegympakiecie. Teraz widać, że getClass()metoda zwraca pełną nazwę klasy. Po drugie, nazwaliśmy naszą zmienną clazz. To wygląda trochę dziwnie. Sensowne byłoby nazywanie tego „klasą”, ale „klasa” jest słowem zastrzeżonym w Javie. Kompilator nie pozwoli na takie nazywanie zmiennych. Musieliśmy to jakoś obejść :) Nieźle jak na początek! Co jeszcze mieliśmy na tej liście możliwości?

Jak uzyskać informacje o modyfikatorach klas, polach, metodach, stałych, konstruktorach i klasach nadrzędnych.

Teraz robi się coraz ciekawiej! W bieżącej klasie nie mamy żadnych stałych ani klasy nadrzędnej. Dodajmy je, aby stworzyć pełny obraz. Utwórz najprostszą Animalklasę nadrzędną:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Sprawimy, że nasza Catklasa będzie dziedziczyć Animali dodamy jedną stałą:

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
}
Teraz mamy pełny obraz! Zobaczmy do czego zdolna jest refleksja :)

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));
   }
}
Oto, co widzimy na konsoli:

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)]
Spójrz na te wszystkie szczegółowe informacje o klasie, które udało nam się uzyskać! I to nie tylko informacja publiczna, ale także prywatna! Notatka: privatezmienne są również wyświetlane na liście. Naszą „analizę” klasy można uznać za zasadniczo kompletną: używamy tej analyzeObject()metody, aby nauczyć się wszystkiego, co możliwe. Ale to nie wszystko, co możemy zrobić dzięki refleksji. Nie ograniczamy się do prostej obserwacji — przejdziemy do działania! :)

Jak utworzyć instancję klasy, której nazwa klasy nie jest znana, dopóki program nie zostanie wykonany.

Zacznijmy od domyślnego konstruktora. Nasza Catklasa jeszcze go nie ma, więc dodajmy:

public Cat() {
  
}
Oto kod do tworzenia Catobiektu przy użyciu metody odbicia ( createCat()metoda):

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());
   }
}
Wejście konsoli:

learn.codegym.Cat
Wyjście konsoli:

Cat{name='null', age=0}
To nie jest błąd: wartości namei agesą wyświetlane na konsoli, ponieważ napisaliśmy kod, który wyświetla je w toString()metodzie klasy Cat. Tutaj odczytujemy nazwę klasy, której obiekt stworzymy z konsoli. Program rozpoznaje nazwę klasy, której obiekt ma zostać utworzony. Przykłady refleksji - 3Dla zwięzłości pominęliśmy właściwy kod obsługi wyjątków, który zająłby więcej miejsca niż sam przykład. W prawdziwym programie oczywiście powinieneś poradzić sobie z sytuacjami związanymi z błędnie wprowadzonymi nazwami itp. Konstruktor domyślny jest dość prosty, więc jak widać łatwo jest go użyć do stworzenia instancji klasy :) Za pomocą newInstance()metody , tworzymy nowy obiekt tej klasy. Inna sprawa, czy tzwCatkonstruktor przyjmuje argumenty jako dane wejściowe. Usuńmy domyślny konstruktor klasy i spróbujmy ponownie uruchomić nasz kod.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Coś poszło nie tak! Wystąpił błąd, ponieważ wywołaliśmy metodę tworzenia obiektu przy użyciu konstruktora domyślnego. Ale teraz nie mamy takiego konstruktora. Więc kiedy newInstance()metoda działa, mechanizm odbicia używa naszego starego konstruktora z dwoma parametrami:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Ale nie robiliśmy nic z parametrami, jakbyśmy o nich zupełnie zapomnieli! Używanie refleksji do przekazywania argumentów konstruktorowi wymaga odrobiny „kreatywności”:

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());
   }
}
Wyjście konsoli:

Cat{name='Fluffy', age=6}
Przyjrzyjmy się bliżej temu, co dzieje się w naszym programie. Stworzyliśmy tablicę Classobiektów.

Class[] catClassParams = {String.class, int.class};
Odpowiadają one parametrom naszego konstruktora (który ma właśnie Stringi intparametry). Przekazujemy je do clazz.getConstructor()metody i uzyskujemy dostęp do żądanego konstruktora. Potem wystarczy tylko wywołać newInstance()metodę z niezbędnymi argumentami i nie zapomnieć o jawnym rzutowaniu obiektu na żądany typ: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Teraz nasz obiekt został pomyślnie utworzony! Wyjście konsoli:

Cat{name='Fluffy', age=6}
Idąc dalej :)

Jak uzyskać i ustawić wartość pola instancji według nazwy.

Wyobraź sobie, że używasz klasy napisanej przez innego programistę. Ponadto nie masz możliwości jego edycji. Na przykład gotowa biblioteka klas spakowana w JAR. Możesz odczytać kod klas, ale nie możesz go zmienić. Załóżmy, że programista, który stworzył jedną z klas w tej bibliotece (niech to będzie nasza stara Catklasa), nie mogąc się wyspać w noc poprzedzającą sfinalizowanie projektu, usunął moduł pobierający i ustawiający dla pola age. Teraz ta klasa przyszła do ciebie. Spełnia wszystkie Twoje potrzeby, ponieważ potrzebujesz tylko Catobiektów w swoim programie. Ale potrzebujesz ich, aby mieć agepole! To jest problem: nie możemy dotrzeć na pole, bo maprivatemodyfikator oraz getter i setter zostały usunięte przez niewyspanego programistę, który stworzył klasę :/ Cóż, refleksja może nam pomóc w tej sytuacji! Mamy dostęp do kodu klasy Cat, więc możemy przynajmniej dowiedzieć się, jakie ona ma pola i jak się nazywają. Uzbrojeni w te informacje możemy rozwiązać nasz problem:

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());
   }
}
Jak stwierdzono w komentarzach, wszystko z namepolem jest proste, ponieważ twórcy klas dostarczyli setera. Wiesz już, jak tworzyć obiekty z domyślnych konstruktorów: mamy newInstance()do tego odpowiednie narzędzia. Ale będziemy musieli trochę majstrować przy drugim polu. Zastanówmy się, o co tutaj chodzi :)

Field age = clazz.getDeclaredField("age");
Tutaj za pomocą naszego Class clazzobiektu uzyskujemy dostęp do agepola za pomocą getDeclaredField()metody. Pozwala nam uzyskać pole wieku jako Field ageobiekt. Ale to nie wystarczy, ponieważ nie możemy po prostu przypisać wartości do privatepól. W tym celu musimy udostępnić pole za pomocą setAccessible()metody:

age.setAccessible(true);
Gdy zrobimy to dla pola, możemy przypisać wartość:

age.set(cat, 6);
Jak widać nasz Field ageobiekt posiada coś w rodzaju ustawiacza wywróconego na zewnątrz, do którego przekazujemy wartość int oraz obiekt, którego pole ma zostać przypisane. Uruchamiamy naszą main()metodę i widzimy:

Cat{name='Fluffy', age=6}
Doskonały! Zrobiliśmy to! :) Zobaczmy, co jeszcze możemy zrobić...

Jak wywołać metodę instancji według nazwy.

Zmieńmy nieco sytuację z poprzedniego przykładu. Załóżmy, że Cattwórca klasy nie pomylił się z metodami pobierającymi i ustawiającymi. Pod tym względem wszystko jest w porządku. Teraz problem jest inny: istnieje metoda, której zdecydowanie potrzebujemy, ale programista ustawił ją jako prywatną:

private void sayMeow() {

   System.out.println("Meow!");
}
Oznacza to, że jeśli stworzymy Catobiekty w naszym programie, to nie będziemy mogli wywołać sayMeow()na nich metody. Będziemy mieć koty, które nie miauczą? To dziwne :/ Jak byśmy to naprawili? Po raz kolejny Reflection API nam pomaga! Znamy nazwę metody, której potrzebujemy. Wszystko inne to kwestia techniczna:

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();
   }
}
Tutaj robimy wiele z tych samych rzeczy, które robiliśmy podczas uzyskiwania dostępu do pola prywatnego. Najpierw otrzymujemy potrzebną nam metodę. Jest zamknięty w Methodobiekcie:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Metoda getDeclaredMethod()pozwala nam przejść do metod prywatnych. Następnie sprawiamy, że metoda jest wywoływalna:

sayMeow.setAccessible(true);
I na koniec wywołujemy metodę na żądanym obiekcie:

sayMeow.invoke(cat);
Tutaj nasze wywołanie metody wygląda jak „callback”: jesteśmy przyzwyczajeni do używania kropki do wskazania obiektu na żądaną metodę ( cat.sayMeow()), ale pracując z refleksją przekazujemy metodzie obiekt, na który chcemy wywołać ta metoda. Co jest na naszej konsoli?

Meow!
Wszystko działało! :) Teraz możesz zobaczyć ogromne możliwości, jakie daje nam mechanizm refleksji Javy. W trudnych i nieoczekiwanych sytuacjach (takich jak nasze przykłady z klasą z zamkniętej biblioteki) może nam to naprawdę bardzo pomóc. Ale, jak każda wielka potęga, wiąże się to z wielką odpowiedzialnością. Wady refleksji opisano w specjalnej sekcji na stronie internetowej Oracle. Istnieją trzy główne wady:
  1. Wydajność jest gorsza. Metody wywoływane za pomocą refleksji mają gorszą wydajność niż metody wywoływane w normalny sposób.

  2. Istnieją ograniczenia bezpieczeństwa. Mechanizm odbicia pozwala nam zmienić zachowanie programu w czasie wykonywania. Ale w swoim miejscu pracy, pracując nad prawdziwym projektem, możesz napotkać ograniczenia, które na to nie pozwalają.

  3. Ryzyko ujawnienia informacji wewnętrznych. Ważne jest, aby zrozumieć, że refleksja jest bezpośrednim naruszeniem zasady enkapsulacji: pozwala nam uzyskać dostęp do prywatnych pól, metod itp. Chyba nie muszę wspominać, że należy uciekać się do bezpośredniego i rażącego naruszenia zasad OOP tylko w najbardziej ekstremalnych przypadkach, gdy z przyczyn niezależnych od Ciebie nie ma innych sposobów rozwiązania problemu.

Używaj refleksji mądrze i tylko w sytuacjach, w których nie da się jej uniknąć i nie zapominaj o jej wadach. Na tym nasza lekcja dobiegła końca. Wyszło dość długo, ale dużo się dzisiaj nauczyliście :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION