JavaRush /Blog Java /Random-PL /Refleksja w Javie – przykłady użycia

Refleksja w Javie – przykłady użycia

Opublikowano w grupie Random-PL
Być może spotkałeś się z pojęciem „odbicia” w życiu codziennym. Zwykle to słowo odnosi się do procesu studiowania siebie. W programowaniu ma to podobne znaczenie - jest to mechanizm badania danych o programie, a także zmiany struktury i zachowania programu podczas jego wykonywania. Ważną rzeczą jest to, że robi się to w czasie wykonywania, a nie w czasie kompilacji. Ale po co sprawdzać kod w czasie wykonywania? Już to widzisz :/ Przykłady użycia Odbicia - 1Idea refleksji może nie być od razu jasna z jednego powodu: do tego momentu zawsze znałeś klasy, z którymi pracowałeś. Cóż, możesz na przykład napisać klasę 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 +
           '}';
}

}
Wiesz o nim wszystko, widzisz jakie ma pola i metody. Z pewnością można dla wygody stworzyć system dziedziczenia ze wspólną klasą Animal, jeśli nagle program będzie potrzebował innych klas zwierząt. Wcześniej stworzyliśmy nawet klasę kliniki weterynaryjnej, w której można było przekazać obiekt nadrzędny Animal, a program leczył zwierzę w zależności od tego, czy był to pies, czy kot. Chociaż zadania te nie są bardzo proste, program uczy się wszystkich potrzebnych informacji o klasach w czasie kompilacji. Dlatego też, gdy main()przekazujemy obiekt w metodzie Catdo metod klasy kliniki weterynaryjnej, program już wie, że jest to kot, a nie pies. A teraz wyobraźmy sobie, że stoimy przed innym zadaniem. Naszym celem jest napisanie analizatora kodu. Musimy utworzyć klasę CodeAnalyzerza pomocą jednej metody - void analyzeClass(Object o). Ta metoda powinna:
  • określić do jakiej klasy przekazano mu obiekt i wyświetlić nazwę klasy w konsoli;
  • określ nazwy wszystkich pól tej klasy, także prywatnych, i wyświetl je w konsoli;
  • określ nazwy wszystkich metod tej klasy, łącznie z prywatnymi, i wyświetl je w konsoli.
Będzie to wyglądać mniej więcej tak:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит obiekt o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
Teraz widoczna jest różnica między tym problemem a resztą problemów, które rozwiązałeś wcześniej. W tym przypadku trudność polega na tym, że ani Ty, ani program nie wiecie, co dokładnie zostanie przekazane do metody analyzeClass(). Piszesz program, inni programiści zaczną go używać, którzy mogą przekazać do tej metody wszystko – dowolną standardową klasę Java lub dowolną napisaną przez siebie klasę. Klasa ta może posiadać dowolną liczbę zmiennych i metod. Innymi słowy, w tym przypadku my (i nasz program) nie mamy pojęcia, z jakimi klasami będziemy pracować. A jednak musimy rozwiązać ten problem. I tu z pomocą przychodzi nam standardowa biblioteka Java – Java Reflection API. Interfejs API Reflection to zaawansowana funkcja językowa. Oficjalna dokumentacja Oracle stwierdza, że ​​mechanizm ten zalecany jest do stosowania wyłącznie przez doświadczonych programistów, którzy bardzo dobrze rozumieją, co robią. Wkrótce zrozumiecie, dlaczego nagle otrzymujemy takie ostrzeżenia z wyprzedzeniem :) Oto lista tego, co można zrobić za pomocą Reflection API:
  1. Znajdź/określ klasę obiektu.
  2. Uzyskaj informacje o modyfikatorach klas, polach, metodach, stałych, konstruktorach i superklasach.
  3. Dowiedz się, które metody należą do zaimplementowanego interfejsu/interfejsów.
  4. Utwórz instancję klasy, której nazwa klasy jest nieznana do czasu wykonania programu.
  5. Pobierz i ustaw wartość pola obiektu według nazwy.
  6. Wywołaj metodę obiektu po nazwie.
Imponująca lista, prawda? :) Zwróć uwagę:Mechanizm refleksji jest w stanie zrobić to wszystko „w locie” niezależnie od tego, jaki obiekt klasy przekażemy do naszego analizatora kodu! Przyjrzyjmy się możliwościom Reflection API na przykładach.

Jak poznać/określić klasę obiektu

Zacznijmy od podstaw. Punktem wejścia do mechanizmu refleksji w Javie jest plik Class. Tak, wygląda to naprawdę śmiesznie, ale po to właśnie jest refleksja :) Używając klasy Class, najpierw określamy klasę dowolnego obiektu przekazanego naszej metodzie. Spróbujmy tego:
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));
   }
}
Wyjście konsoli:

class learn.javarush.Cat
Zwróć uwagę na dwie rzeczy. Po pierwsze, celowo umieściliśmy klasę Catw osobnym pakiecie, learn.javarush;teraz możesz zobaczyć, że getClass()zwraca ona pełną nazwę klasy. Po drugie, nazwaliśmy naszą zmienną clazz. Wygląda trochę dziwnie. Oczywiście powinno się to nazywać „klasa”, ale „klasa” jest słowem zastrzeżonym w języku Java i kompilator nie pozwoli na wywoływanie zmiennych w ten sposób. Musiałem się z tego wydostać :) No cóż, niezły początek! Co jeszcze mieliśmy na liście możliwości?

Jak uzyskać informacje o modyfikatorach klas, polach, metodach, stałych, konstruktorach i nadklasach

To już jest ciekawsze! W bieżącej klasie nie mamy żadnych stałych ani klasy nadrzędnej. Dodajmy je dla kompletności. Stwórzmy najprostszą klasę nadrzędną Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
Dodajmy także Catdziedziczenie z Animali jedną stałą do naszej klasy:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
Teraz mamy komplet! Wypróbujmy możliwości refleksji :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Nazwa класса: " + 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));
   }
}
Oto co dostajemy w konsoli:
Nazwa класса: 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)]
Otrzymaliśmy mnóstwo szczegółowych informacji na temat zajęć! I to nie tylko o części publicznej, ale także o części prywatnej. Zwróć uwagę: private-zmienne są również wyświetlane na liście. Właściwie „analizę” zajęć można w tym momencie uznać za zakończoną: teraz, korzystając z metody, analyzeClass()dowiemy się wszystkiego, co się da. Ale to nie wszystkie możliwości, jakie mamy podczas pracy z refleksją. Nie ograniczajmy się do prostej obserwacji i przejdźmy do aktywnego działania! :)

Jak utworzyć instancję klasy, jeśli nazwa klasy jest nieznana przed wykonaniem programu

Zacznijmy od domyślnego konstruktora. Nie ma go jeszcze w naszej klasie Cat, więc dodajmy go:
public Cat() {

}
Oto jak wyglądałby kod tworzący obiekt Catza pomocą odbicia (metoda 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());
   }
}
Wpisz w konsoli:

learn.javarush.Cat
Wyjście konsoli:

Cat{name='null', age=0}
To nie jest błąd: wartości namei agesą wyświetlane w konsoli, ponieważ zaprogramowaliśmy ich wyjście w metodzie toString()klasy Cat. Tutaj czytamy nazwę klasy, której obiekt utworzymy z konsoli. Działający program poznaje nazwę klasy, której obiekt utworzy. Przykłady wykorzystania Refleksji - 3Dla zachowania zwięzłości pominęliśmy kod do prawidłowej obsługi wyjątków, aby nie zajmował więcej miejsca niż sam przykład. W prawdziwym programie oczywiście warto sobie poradzić z sytuacjami, w których wpisane są nieprawidłowe nazwy itp. Domyślny konstruktor jest rzeczą dość prostą, dlatego stworzenie przy jego pomocy instancji klasy jak widać nie jest trudne :) I korzystając z metody newInstance()tworzymy nowy obiekt tej klasy. Inną sprawą jest to, czy konstruktor klasy Catprzyjmuje parametry jako dane wejściowe. Usuńmy domyślny konstruktor z klasy i spróbujmy ponownie uruchomić nasz kod.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Coś poszło nie tak! Otrzymaliśmy błąd, ponieważ wywołaliśmy metodę tworzenia obiektu za pomocą domyślnego konstruktora. Ale teraz nie mamy takiego projektanta. Oznacza to, że gdy metoda zadziała, newInstance()mechanizm odbicia użyje naszego starego konstruktora z dwoma parametrami:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Ale z parametrami nic nie robiliśmy, jakbyśmy zupełnie o nich zapomnieli! Aby przekazać je konstruktorowi za pomocą odbicia, będziesz musiał go trochę zmodyfikować:
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());
   }
}
Wyjście konsoli:

Cat{name='Barsik', age=6}
Przyjrzyjmy się bliżej temu, co dzieje się w naszym programie. Stworzyliśmy tablicę obiektów Class.
Class[] catClassParams = {String.class, int.class};
Odpowiadają one parametrom naszego konstruktora (mamy tylko parametry Stringi int). Przekazujemy je do metody clazz.getConstructor()i uzyskujemy dostęp do wymaganego konstruktora. Następnie pozostaje już tylko wywołać metodę newInstance()z niezbędnymi parametrami i nie zapomnieć o jawnym rzucie obiektu na potrzebną nam klasę - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
Dzięki temu nasz obiekt zostanie pomyślnie utworzony! Wyjście konsoli:

Cat{name='Barsik', age=6}
Przejdźmy dalej :)

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

Wyobraź sobie, że używasz klasy napisanej przez innego programistę. Nie masz jednak możliwości jego edycji. Na przykład gotowa biblioteka klas spakowana w pliku JAR. Możesz przeczytać kod zajęć, ale nie możesz go zmienić. Programista, który stworzył klasę w tej bibliotece (niech to będzie nasza stara klasa Cat), nie wyspał się przed ostatecznym projektem i usunął moduły pobierające i ustawiające dla pola age. Teraz ta klasa przyszła do Ciebie. W pełni odpowiada Twoim potrzebom, bo potrzebujesz po prostu obiektów w programie Cat. Ale potrzebujesz ich z tym samym polem age! To jest problem: nie możemy dotrzeć do pola, bo ma ono modyfikator private, a gettery i settery zostały usunięte przez niedoszłego programistę tej klasy :/ Cóż, refleksja też może nam pomóc w tej sytuacji! CatMamy dostęp do kodu klasy : możemy przynajmniej dowiedzieć się, jakie ma pola i jak się nazywają. Uzbrojeni w te informacje rozwiązujemy nasz problem:
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());
   }
}
Jak stwierdzono w komentarzu, namew przypadku pola wszystko jest proste: twórcy klasy zapewnili dla niego seter. Wiesz już także, jak tworzyć obiekty z domyślnych konstruktorów: istnieje na to metoda newInstance(). Ale będziesz musiał majstrować przy drugim polu. Zastanówmy się o co tutaj chodzi :)
Field age = clazz.getDeclaredField("age");
Tutaj za pomocą naszego obiektu Class clazzuzyskujemy dostęp do pola agemetodą getDeclaredField(). Daje nam to możliwość uzyskania pola wieku jako obiektu Field age. Ale to jeszcze nie wystarczy, ponieważ privatepolom nie można po prostu przypisać wartości. W tym celu należy udostępnić pole metodą setAccessible():
age.setAccessible(true);
Pola, dla których jest to wykonywane, można przypisać wartości:
age.set(cat, 6);
Jak widać mamy do czynienia z czymś w rodzaju setera odwróconego do góry nogami: przypisujemy polu Field agejego wartość, a także przekazujemy mu obiekt, do którego to pole ma być przypisane. Uruchommy naszą metodę main()i zobaczmy:

Cat{name='Barsik', age=6}
Świetnie, zrobiliśmy to wszystko! :) Zobaczmy jakie mamy inne możliwości...

Jak wywołać metodę obiektu po nazwie

Zmieńmy nieco sytuację z poprzedniego przykładu. Załóżmy, że twórca klasy Catpomylił się z polami - oba są dostępne, są dla nich gettery i settery, wszystko jest w porządku. Problem jest inny: stworzył metodę prywatną, której zdecydowanie potrzebujemy:
private void sayMeow() {

   System.out.println("Meow!");
}
W rezultacie utworzymy Catw naszym programie obiekty, ale nie będziemy mogli wywołać ich metody sayMeow(). Czy będziemy mieć koty, które nie miauczą? Dość dziwne :/ Jak mogę to naprawić? Po raz kolejny na ratunek przychodzi Reflection API! Znamy nazwę wymaganej metody. Reszta to kwestia techniki:
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();
   }
}
Tutaj postępujemy bardzo podobnie jak w sytuacji z dostępem do pola prywatnego. Najpierw otrzymujemy potrzebną nam metodę, która jest zamknięta w obiekcie klasy Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Z pomocą getDeclaredMethod()można „dotrzeć” do metod prywatnych. Następnie sprawiamy, że metoda jest wywoływalna:
sayMeow.setAccessible(true);
Na koniec wywołujemy metodę na żądanym obiekcie:
sayMeow.invoke(cat);
Wywołanie metody wygląda również jak „wywołanie odwrotne”: przyzwyczajamy się do wskazywania obiektu za pomocą kropki ( cat.sayMeow()), a pracując z odbiciem przekazujemy do metody obiekt, z którego ma zostać wywołana . Co mamy w konsoli?

Meow!
Wszystko się udało! :) Teraz widzisz, jakie szerokie możliwości daje nam mechanizm refleksji w Javie. W trudnych i nieoczekiwanych sytuacjach (jak w przykładach z klasą z zamkniętej biblioteki) może nam to naprawdę bardzo pomóc. Jednakże, jak każda wielka moc, wiąże się to również z wielką odpowiedzialnością. O wadach refleksji opisano w specjalnej sekcji witryny Oracle. Istnieją trzy główne wady:
  1. Produktywność spada. Metody wywoływane przy użyciu refleksji mają niższą wydajność niż metody wywoływane normalnie.

  2. Istnieją ograniczenia bezpieczeństwa. Mechanizm refleksji umożliwia zmianę zachowania programu w czasie jego wykonywania. Jednak w Twoim środowisku pracy nad prawdziwym projektem mogą istnieć ograniczenia, które nie pozwalają Ci na to.

  3. Ryzyko ujawnienia informacji poufnych. Ważne jest, aby zrozumieć, że użycie refleksji bezpośrednio narusza zasadę enkapsulacji: umożliwia nam dostęp do prywatnych pól, metod itp. Chyba nie trzeba tłumaczyć, że na bezpośrednie i rażące naruszenie zasad OOP należy sięgać jedynie w najbardziej skrajnych przypadkach, gdy z przyczyn od nas niezależnych nie ma innych możliwości rozwiązania problemu.

Korzystaj z mechanizmu refleksji mądrze i tylko w sytuacjach, w których nie da się go uniknąć, i nie zapominaj o jego mankamentach. Na tym kończymy nasz wykład! Okazał się dość duży, ale dzisiaj nauczyliście się wielu nowych rzeczy :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION