Cat
klasę:
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. Animal
Prawdopodobnie 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 Cat
obiekt 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ć CodeAnalyzer
klasę 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.
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:
- Zidentyfikuj/określ klasę obiektu.
- Uzyskaj informacje o modyfikatorach klas, polach, metodach, stałych, konstruktorach i klasach nadrzędnych.
- Dowiedz się, które metody należą do zaimplementowanych interfejsów.
- Utwórz instancję klasy, której nazwa klasy nie jest znana, dopóki program nie zostanie wykonany.
- Pobierz i ustaw wartość pola instancji według nazwy.
- Wywołaj metodę instancji według nazwy.
Jak zidentyfikować/określić klasę obiektu
Zacznijmy od podstaw. Punktem wejścia do silnika refleksji Java jestClass
klasa. Tak, wygląda to naprawdę śmiesznie, ale na tym polega refleksja :) Korzystając z Class
klasy, 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ę Cat
klasę w osobnym learn.codegym
pakiecie. 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ąAnimal
klasę nadrzędną:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
Sprawimy, że nasza Cat
klasa będzie dziedziczyć Animal
i 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: private
zmienne 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. NaszaCat
klasa jeszcze go nie ma, więc dodajmy:
public Cat() {
}
Oto kod do tworzenia Cat
obiektu 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 name
i age
są 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. Dla 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 tzwCat
konstruktor 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ę Class
obiektów.
Class[] catClassParams = {String.class, int.class};
Odpowiadają one parametrom naszego konstruktora (który ma właśnie String
i int
parametry). 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 staraCat
klasa), 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 Cat
obiektów w swoim programie. Ale potrzebujesz ich, aby mieć age
pole! To jest problem: nie możemy dotrzeć na pole, bo maprivate
modyfikator 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 name
polem 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 clazz
obiektu uzyskujemy dostęp do age
pola za pomocą getDeclaredField()
metody. Pozwala nam uzyskać pole wieku jako Field age
obiekt. Ale to nie wystarczy, ponieważ nie możemy po prostu przypisać wartości do private
pó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 age
obiekt 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, żeCat
twó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 Cat
obiekty 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 Method
obiekcie:
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:
-
Wydajność jest gorsza. Metody wywoływane za pomocą refleksji mają gorszą wydajność niż metody wywoływane w normalny sposób.
-
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ą.
-
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.
GO TO FULL VERSION