Do czego służy Reflection API?

Reflection w Javie to mechanizm, który umożliwia programiście wprowadzanie zmian i uzyskiwanie informacji o klasach, interfejsach, polach i metodach w czasie wykonywania bez znajomości ich nazw.

Interfejs API Reflection pomaga również tworzyć nowe instancje klas, wywoływać metody oraz pobierać lub ustawiać wartości pól.

Zbierzmy wszystkie możliwości wykorzystania refleksji na jednej liście:

  • Znajdź/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 zaimplementowanego interfejsu/interfejsów
  • Utwórz instancję klasy, gdy nazwa klasy nie jest znana do czasu wykonania programu
  • Pobierz i ustaw wartość pola obiektu według nazwy
  • Wywołaj metodę obiektu według nazwy

Odbicie jest używane w prawie wszystkich nowoczesnych technologiach Java i leży u podstaw większości nowoczesnych frameworków i bibliotek Java / Java EE, na przykład w:

  • Spring - frameworki do budowy aplikacji webowych
  • JUnit - framework testowy

Jeśli deweloper nigdy nie spotkał się z takimi mechanizmami, najprawdopodobniej będzie miał logiczne pytanie - dlaczego to wszystko jest konieczne? Odpowiedź w tym przypadku jest dość prosta, ale jednocześnie bardzo niejasna: dzięki refleksji radykalnie zwiększamy elastyczność i możliwość dostosowania aplikacji pod siebie i pod nasz kod.

Ale zawsze są plusy i minusy. Więc kilka minusów:

  • Naruszenia bezpieczeństwa aplikacji. Dzięki refleksji możemy uzyskać dostęp do fragmentu kodu, do którego nie powinniśmy (naruszenie enkapsulacji).
  • Ograniczenia bezpieczeństwa. Refleksja wymaga uprawnień wykonawczych, które nie są dostępne dla systemów z uruchomionym menedżerem bezpieczeństwa.
  • Niska wydajność. Reflection w Javie dynamicznie określa typy, skanując ścieżkę klas w celu znalezienia klasy do załadowania. Zmniejsza to wydajność programu.
  • Trudność we wsparciu. Kod napisany z refleksją jest trudny do odczytania i debugowania. Staje się mniej elastyczny i trudniejszy w utrzymaniu.

Praca z klasami przy użyciu Reflection API

Wszystkie operacje odbicia rozpoczynają się od obiektu java.lang.Class . Dla każdego typu obiektu tworzona jest niezmienna instancja java.lang.Class , która udostępnia metody pobierania właściwości obiektów, tworzenia nowych obiektów, wywoływania metod.

Spójrzmy na listę podstawowych metod pracy z java.lang.Class :

metoda żyć
Ciąg pobierzNazwę(); Zwraca nazwę klasy
int getModifiers(); Zwraca modyfikatory dostępu
Pakiet getPackage(); Zwraca informacje o paczce
klasa getNadklasa(); Zwraca informacje o klasie nadrzędnej
Klasa[] getInterfaces(); Zwraca tablicę interfejsów
Konstruktor[] getConstructors(); Zwraca informacje o konstruktorach klas
Pola[] getFields(); Zwraca pola klasy
Pliki getFiled(String nazwa_pola); Zwraca określone pole klasy według nazwy
Metoda[] getMethods(); Zwraca tablicę metod

Są to główne metody uzyskiwania danych o klasie i interfejsach, polach i metodach. Istnieją również metody, które pozwalają uzyskać lub ustawić wartości pól, dają dostęp do prywatnych pól klasy. Przyjrzyjmy się im nieco później.

Teraz porozmawiamy o uzyskaniu samej klasy java.lang.Class . Mamy na to trzy sposoby.

1. Korzystanie z Class.forName

W działającej aplikacji musisz użyć metody forName(String className) , aby uzyskać klasę .

Ten kod demonstruje możliwość tworzenia klas przy użyciu odbicia. Stwórzmy klasę Person do pracy z:


package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Druga część naszego przykładu to kod z odbiciem:


public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Takie podejście jest możliwe, jeśli znana jest pełna nazwa klasy. Następnie możesz uzyskać odpowiednią klasę za pomocą metody statycznej Class.forName() . Tej metody nie można używać w przypadku typów pierwotnych.

2. Korzystanie z klasy

Jeśli typ jest dostępny, ale nie ma instancji, możesz uzyskać klasę, dodając .class do nazwy typu. Jest to najłatwiejszy sposób na uzyskanie klasy dla typu pierwotnego.


Class aClass = Person.class;

3. Używanie .getClass()

Jeśli dostępna jest instancja obiektu, najłatwiejszym sposobem uzyskania jej klasy jest wywołanie metody object.getClass() .


Person person = new Person();
Class aClass = person.getClass();

Jaka jest różnica między dwoma ostatnimi podejściami?

Użyj A.class , jeśli wiesz z góry podczas pisania kodu, który obiekt klasy Cię interesuje. Jeśli nie ma instancji, należy użyć .class .

Pobieranie metod klasowych

Rozważ metody, które zwracają metody naszej klasy: getDeclaredMethods() i getMethods() .

Metoda getDeclaredMethods() zwraca tablicę zawierającą obiekty typu Method reprezentujące wszystkie zadeklarowane metody klasy lub interfejsu reprezentowanego przez ten obiekt klasy, w tym metody publiczne, prywatne, domyślne i chronione, ale z wyłączeniem metod dziedziczonych.

getMethods() zwraca tablicę zawierającą obiekty typu Method reprezentujące wszystkie metody publiczne klasy lub interfejsu reprezentowane przez ten obiekt klasy, w tym te zadeklarowane przez klasę lub interfejs oraz te odziedziczone z nadklas i superinterfejsów.

Przyjrzyjmy się, jak działa każdy z nich.

Zacznijmy od getDeclaredMethods() . Poniżej będziemy pracować z abstrakcyjną klasą Numbers , która pomoże nam jeszcze raz zrozumieć różnicę między tymi dwiema metodami. Napiszmy statyczną metodę, która przekonwertuje naszą tablicę Method na List<String> :


import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

Wynik działania kodu wygląda następująco:

byteValue
shortValue
intValue
longValue
floatValue
doubleValue

Są to metody zadeklarowane wewnątrz klasy Number . A co zwróci nam metoda getMethods() ? Zmieńmy dwie linie w przykładzie:


final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

W rezultacie zobaczymy następujący zestaw metod:

byteValue
shortValue
intValue
longValue
floatValue
doubleValue
wait
wait
wait
equals
toString
hashCode
getClass
powiadomić
powiadomić wszystko

Ponieważ wszystkie klasy dziedziczą po Object , nasza metoda zwróciła również publiczne metody klasy Object .

Pobieranie pól klasy

Metody getFields i getDeclaredFields służą do pobierania pól klasy. Spójrzmy na przykład klasy LocalDateTime . Przepiszmy nasz kod:


import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

W wyniku wykonania tego kodu otrzymamy zestaw pól, który zawiera klasę LocalDateTime.

MIN
MAX
serialVersionUID
data/godzina
_

Analogicznie do metod, zobaczmy, co się stanie, jeśli trochę zmienimy kod:


final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

Wyjście programu:

MIN
MAKS

Zobaczmy teraz, jaka jest różnica między naszymi metodami.

Metoda getDeclaredFields zwraca tablicę obiektów Field reprezentujących wszystkie pola zadeklarowane przez klasę lub interfejs reprezentowany przez ten obiektklasa.

Metoda getFields zwraca tablicę obiektów Field reprezentujących wszystkie pola publiczne klasy lub interfejsu reprezentowanego przez ten obiektklasa.

Teraz zajrzyjmy do naszego LocalDateTime .

Pola klasyMINIMAKSsą publiczne, co oznacza, że ​​będą widoczne poprzez metodę getFields . Z kolei poladata,czas,SerialVersionUIDmają modyfikator private , co oznacza , że nie będą widoczne za pomocą metody getFields , a możemy je uzyskać za pomocą metody getDeclaredFields . W ten sposób możemy uzyskać dostęp do pól (Field) dla pól prywatnych .

Inne metody i ich opis

Czas porozmawiać o niektórych metodach naszej klasy Class , a mianowicie:

metoda Działanie
getModifiers Pobieranie naszych modyfikatorów klas
pobierz pakiet Pobieranie pakietu zawierającego naszą klasę
getSuperclass Zdobądź nadklasę
pobierzInterfejsy Pobieranie tablicy interfejsów implementujących klasę
pobierzNazwę Uzyskiwanie w pełni kwalifikowanej nazwy klasy
pobierzProstąNazwę Pobieranie nazwy klasy

getModifiers()

Dostęp do modyfikatorów można uzyskać za pomocąklasaobiekt.

Modyfikatory to słowa kluczowe public , static , interface , itp. Modyfikatory uzyskujemy za pomocą metody getModifiers() :


Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

Wynik wykonania znajduje się w zmiennejint, gdzie każdy modyfikator jest flagą bitową, którą można ustawić lub wyczyścić. Możemy sprawdzić modyfikatory za pomocą metod w klasie java.lang.reflect.Modifier :


import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

Przypomnij sobie, jak wygląda nasza deklaracja klasy Person :


public class Person {
   …
}

W rezultacie na ekranie otrzymujemy następujący wynik:

Modyfikatory klasy: 1
Jest publiczna: prawda
Jest statyczna: fałsz
Jest ostateczna: fałsz
Jest abstrakcyjna: fałsz
Jest interfejsem: fałsz

Jeśli uczynimy naszą klasę abstrakcyjną, otrzymamy następujący wynik:


public abstract class Person { … }

i to wyjście:

Modyfikatory klasy: 1025
Jest publiczne: prawda
Jest statyczne: fałsz
Jest ostateczne: fałsz
Jest abstrakcyjne: prawda
Jest interfejsem: fałsz

Zmieniliśmy modyfikator dostępu, a co za tym idzie dane, które będą zwracane przez nasze statyczne metody klasy Modifier .

pobierz pakiet()

Znając tylko klasę możemy uzyskać informacje o paczce:


Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

Mając dostęp do obiektu klasy, możemy uzyskać dostęp do jego nadklasy:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

W rezultacie otrzymujemy dobrze znaną klasę Object :


class java.lang.Object

Ale jeśli nasza klasa ma klasę nadrzędną, zobaczymy dokładnie to:


package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

W rezultacie otrzymamy naszą nadklasę:


class com.company.Human

getInterfaces()

Listę interfejsów zaimplementowanych przez tę klasę można uzyskać w następujący sposób:


public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

I nie zapomnij zmodyfikować naszej klasy Person :


public class Person implements Serializable { … }

Wniosek:

[interfejs java.io.Serializable]

Klasa może implementować wiele interfejsów. Dlatego zwracana jest tablica obiektówklasa. W API Java Reflection interfejsy są również reprezentowane przez obiekty typuklasa.

Uwaga: Metoda zwróciła tylko interfejsy implementowane przez określoną klasę, a nie jej nadklasę. Aby uzyskać pełną listę interfejsów zaimplementowanych w tej klasie, należy odwołać się zarówno do bieżącej klasy, jak i do wszystkich jej nadklas w łańcuchu dziedziczenia.

getName() & getSimpleName() & getCanonicalName()

Napiszmy przykład prymitywu, klasy zagnieżdżonej, klasy anonimowej i klasy String :


public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

Wynik naszego programu:

int class (primitive):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class(zwykła klasa):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (klasa zagnieżdżona):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (anonimowa klasa wewnętrzna):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Przeanalizujmy teraz wyniki naszego programu:

  • getName() zwraca nazwę podmiotu.

  • getCanonicalName() zwraca nazwę kanoniczną klasy bazowej, zgodnie z definicją w specyfikacji języka Java. Zwraca wartość null, jeśli klasa bazowa nie ma nazwy kanonicznej (to znaczy, jeśli jest to lokalna lub anonimowa klasa lub tablica, której typ składnika nie ma nazwy kanonicznej).

  • getSimpleName() zwraca prostą nazwę klasy bazowej określoną w kodzie źródłowym. Zwraca pusty ciąg, jeśli klasa bazowa jest anonimowa.

  • getTypeName() zwraca łańcuch informacyjny dla nazwy tego typu.