Wozu dient die Reflection API?

Der Reflexionsmechanismus von Java ermöglicht es einem Entwickler, zur Laufzeit Änderungen vorzunehmen und Informationen über Klassen, Schnittstellen, Felder und Methoden abzurufen, ohne deren Namen zu kennen.

Mit der Reflection-API können Sie außerdem neue Objekte erstellen, Methoden aufrufen und Feldwerte abrufen oder festlegen.

Lassen Sie uns eine Liste aller Dinge erstellen, die Sie mithilfe von Reflektion tun können:

  • Identifizieren/bestimmen Sie die Klasse eines Objekts
  • Erhalten Sie Informationen zu Klassenmodifikatoren, Feldern, Methoden, Konstanten, Konstruktoren und Superklassen
  • Finden Sie heraus, welche Methoden zu implementierten Schnittstellen gehören.
  • Erstellen Sie eine Instanz einer Klasse, deren Klassenname erst bekannt ist, wenn das Programm ausgeführt wird
  • Rufen Sie den Wert eines Instanzfelds anhand des Namens ab und legen Sie ihn fest
  • Rufen Sie eine Instanzmethode nach Namen auf

Fast alle modernen Java-Technologien verwenden Reflektion. Es liegt den meisten heutigen Java-/Java-EE-Frameworks und -Bibliotheken zugrunde, zum Beispiel:

  • Spring- Frameworks zum Erstellen von Webanwendungen
  • das JUnit -Testframework

Wenn Sie diese Mechanismen noch nie kennengelernt haben, fragen Sie sich wahrscheinlich, warum das alles notwendig ist. Die Antwort ist recht einfach, aber auch sehr vage: Reflexion erhöht die Flexibilität und die Möglichkeit, unsere Anwendung und unseren Code anzupassen, erheblich.

Aber es gibt immer Vor- und Nachteile. Erwähnen wir also ein paar Nachteile:

  • Verstöße gegen die Anwendungssicherheit. Durch Reflection können wir auf Code zugreifen, den wir nicht haben sollten (Verletzung der Kapselung).
  • Sicherheitsbeschränkungen. Reflection erfordert Laufzeitberechtigungen, die für Systeme, auf denen ein Sicherheitsmanager ausgeführt wird, nicht verfügbar sind.
  • Schlechte Leistung. Reflection in Java bestimmt Typen dynamisch, indem es den Klassenpfad durchsucht , um die zu ladende Klasse zu finden. Dadurch verringert sich die Leistung des Programms.
  • Schwierig zu pflegen. Code, der Reflektion verwendet, ist schwer zu lesen und zu debuggen. Es ist weniger flexibel und schwieriger zu warten.

Arbeiten mit Klassen mithilfe der Reflection-API

Alle Reflexionsoperationen beginnen mit einem java.lang.Class- Objekt. Für jeden Objekttyp wird eine unveränderliche Instanz von java.lang.Class erstellt. Es stellt Methoden zum Abrufen von Objekteigenschaften, zum Erstellen neuer Objekte und zum Aufrufen von Methoden bereit.

Schauen wir uns die Liste der grundlegenden Methoden für die Arbeit mit java.lang.Class an :

Methode Aktion
String getName(); Gibt den Namen der Klasse zurück
int getModifiers(); Gibt Zugriffsmodifikatoren zurück
Paket getPackage(); Gibt Informationen zu einem Paket zurück
Klasse getSuperclass(); Gibt Informationen über eine übergeordnete Klasse zurück
Klasse[] getInterfaces(); Gibt ein Array von Schnittstellen zurück
Constructor[] getConstructors(); Gibt Informationen über Klassenkonstruktoren zurück
Felder[] getFields(); Gibt die Felder einer Klasse zurück
Feld getField(String fieldName); Gibt ein bestimmtes Feld einer Klasse nach Namen zurück
Methode[] getMethods(); Gibt ein Array von Methoden zurück

Dies sind die wichtigsten Methoden zum Abrufen von Daten über Klassen, Schnittstellen, Felder und Methoden. Es gibt auch Methoden, mit denen Sie Feldwerte abrufen oder festlegen und auf private Felder zugreifen können. Wir werden sie uns etwas später ansehen.

Im Moment werden wir darüber sprechen, wie wir die java.lang.Class selbst erhalten. Wir haben drei Möglichkeiten, dies zu tun.

1. Verwenden von Class.forName

In einer laufenden Anwendung müssen Sie die Methode forName(String className) verwenden , um eine Klasse abzurufen.

Dieser Code zeigt, wie wir mithilfe von Reflektion Klassen erstellen können. Erstellen wir eine Person- Klasse, mit der wir arbeiten können:


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

Und der zweite Teil unseres Beispiels ist der Code, der Reflektion verwendet:


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

Dieser Ansatz ist möglich, wenn der vollständige Name der Klasse bekannt ist. Anschließend können Sie die entsprechende Klasse mithilfe der statischen Methode Class.forName() abrufen . Diese Methode kann nicht für primitive Typen verwendet werden.

2. Verwenden von .class

Wenn ein Typ verfügbar ist, es aber keine Instanz davon gibt, können Sie die Klasse erhalten, indem Sie .class zum Typnamen hinzufügen. Dies ist der einfachste Weg, die Klasse eines primitiven Typs zu erhalten.


Class aClass = Person.class;

3. Verwendung von .getClass()

Wenn ein Objekt verfügbar ist, ist der Aufruf von object.getClass() der einfachste Weg, eine Klasse abzurufen .


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

Was ist der Unterschied zwischen den letzten beiden Ansätzen?

Verwenden Sie A.class, wenn Sie zum Zeitpunkt der Codierung wissen, an welchem ​​Klassenobjekt Sie interessiert sind. Wenn keine Instanz verfügbar ist, sollten Sie .class verwenden .

Abrufen der Methoden einer Klasse

Schauen wir uns die Methoden an, die die Methoden unserer Klasse zurückgeben: getDeclaredMethods() und getMethods() .

getDeclaredMethods() gibt ein Array zurück, das Methodenobjekte für alle deklarierten Methoden der Klasse oder Schnittstelle enthält, die durch das Klassenobjekt dargestellt wird, einschließlich öffentlicher, privater, Standard- und geschützter Methoden, jedoch nicht geerbter Methoden.

getMethods() gibt ein Array zurück, das Methodenobjekte für alle öffentlichen Methoden der Klasse oder Schnittstelle enthält, die durch das Klassenobjekt dargestellt wird – diejenigen, die von der Klasse oder Schnittstelle deklariert wurden, sowie diejenigen, die von Superklassen und Superschnittstellen geerbt wurden.

Werfen wir einen Blick darauf, wie jeder von ihnen funktioniert.

Beginnen wir mit getDeclaredMethods() . Um uns erneut zu helfen, den Unterschied zwischen den beiden Methoden zu verstehen, arbeiten wir im Folgenden mit der abstrakten Numbers- Klasse. Schreiben wir eine statische Methode, die unser Methodenarray in List<String> konvertiert :


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());
    }
}

Hier ist das Ergebnis der Ausführung dieses Codes:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue

Dies sind die in der Number- Klasse deklarierten Methoden. Was gibt getMethods() zurück? Lassen Sie uns im Beispiel zwei Zeilen ändern:


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

Dabei sehen wir die folgenden Methoden:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
warten
warten
warten
gleich
toString
hashCode
getClass
notify
notifyAll

Da alle Klassen Object erben , gibt unsere Methode auch die öffentlichen Methoden der Object- Klasse zurück.

Abrufen der Felder einer Klasse

Die Methoden getFields und getDeclaredFields werden verwendet, um die Felder einer Klasse abzurufen. Schauen wir uns als Beispiel die LocalDateTime- Klasse an. Wir werden unseren Code umschreiben:


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());
    }
}

Als Ergebnis der Ausführung dieses Codes erhalten wir den Satz von Feldern, die in der LocalDateTime-Klasse enthalten sind.

MIN
MAX
serialVersionUID
Datum
/Uhrzeit

Schauen wir uns analog zu unserer vorherigen Untersuchung der Methoden an, was passiert, wenn wir den Code ein wenig ändern:


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

Ausgang:

MIN
MAX

Lassen Sie uns nun den Unterschied zwischen diesen Methoden herausfinden.

Die getDeclaredFields- Methode gibt ein Array von Field- Objekten für alle Felder zurück, die von der dadurch dargestellten Klasse oder Schnittstelle deklariert werdenKlasseObjekt.

Die getFields- Methode gibt ein Array von Field- Objekten für alle öffentlichen Felder der Klasse oder Schnittstelle zurück, die durch die dargestellt wirdKlasseObjekt.

Schauen wir uns nun LocalDateTime an .

Die KlasseMINDESTUndMAXFelder sind öffentlich, was bedeutet, dass sie über die getFields -Methode sichtbar sind . Im Gegensatz dazu ist dieDatum,Zeit,serialVersionUIDMethoden haben den privaten Modifikator, was bedeutet, dass sie durch die getFields -Methode nicht sichtbar sind , aber wir können sie mit getDeclaredFields abrufen . Auf diese Weise können wir auf Feldobjekte für private Felder zugreifen.

Beschreibungen anderer Methoden

Jetzt ist es an der Zeit, über einige Methoden der Class- Klasse zu sprechen, nämlich:

Methode Aktion
getModifiers Die Modifikatoren für unsere Klasse abrufen
getPackage Holen Sie sich das Paket, das unsere Klasse enthält
getSuperclass Die übergeordnete Klasse abrufen
getInterfaces Abrufen eines Arrays von Schnittstellen, die von einer Klasse implementiert werden
getName Den vollständig qualifizierten Klassennamen abrufen
getSimpleName Den Namen einer Klasse abrufen

getModifiers()

Auf Modifikatoren kann über a zugegriffen werdenKlasseObjekt.

Modifikatoren sind Schlüsselwörter wie public , static , interface usw. Wir erhalten Modifikatoren mit der Methode getModifiers() :


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

Dieser Code legt den Wert von an festintVariable, die ein Bitfeld ist. Jeder Zugriffsmodifikator kann durch Setzen oder Löschen des entsprechenden Bits aktiviert oder deaktiviert werden. Wir können Modifikatoren mithilfe der Methoden in der Klasse java.lang.reflect.Modifier überprüfen :


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);
    }
}

Erinnern Sie sich, wie die Erklärung unserer Person aussieht:


public class Person {
   …
}

Wir erhalten die folgende Ausgabe:

Klassenmodifikatoren: 1
Ist öffentlich: wahr.
Ist statisch: falsch.
Ist endgültig: falsch.
Ist abstrakt: falsch.
Ist Schnittstelle: falsch

Wenn wir unsere Klasse abstrakt machen, dann haben wir:


public abstract class Person { … }

und diese Ausgabe:

Klassenmodifikatoren: 1025
Ist öffentlich: wahr
Ist statisch: falsch
Ist endgültig: falsch
Ist abstrakt: wahr
Ist Schnittstelle: falsch

Wir haben den Zugriffsmodifikator geändert, was bedeutet, dass wir auch die durch die statischen Methoden der Modifier- Klasse zurückgegebenen Daten geändert haben.

getPackage()

Wenn wir nur eine Klasse kennen, können wir Informationen über ihr Paket erhalten:


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

getSuperclass()

Wenn wir ein Klassenobjekt haben, können wir auf seine übergeordnete Klasse zugreifen:


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

Wir erhalten die bekannte Object- Klasse:


class java.lang.Object

Wenn unsere Klasse jedoch eine andere übergeordnete Klasse hat, wird diese stattdessen angezeigt:


package com.company;

class Human {
    // Some info
}

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

    // Some info
}

Hier bekommen wir unsere Elternklasse:


class com.company.Human

getInterfaces()

So erhalten wir die Liste der von der Klasse implementierten Schnittstellen:


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

Und vergessen wir nicht, unsere Personenklasse zu ändern :


public class Person implements Serializable { … }

Ausgang:

[Schnittstelle java.io.Serializable]

Eine Klasse kann viele Schnittstellen implementieren. Deshalb bekommen wir eine Reihe vonKlasseObjekte. In der Java Reflection API werden Schnittstellen auch durch dargestelltKlasseObjekte.

Bitte beachten Sie: Die Methode gibt nur die von der angegebenen Klasse implementierten Schnittstellen zurück, nicht deren übergeordnete Klasse. Um eine vollständige Liste der von der Klasse implementierten Schnittstellen zu erhalten, müssen Sie sowohl auf die aktuelle Klasse als auch auf alle ihre Vorgänger in der Vererbungskette verweisen.

getName() & getSimpleName() & getCanonicalName()

Schreiben wir ein Beispiel mit einem Grundelement, einer verschachtelten Klasse, einer anonymen Klasse und der String- Klasse:


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());
    }
}

Ergebnis unseres Programms:

int Klasse (primitiv):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (gewöhnliche Klasse):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (verschachtelte Klasse):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (anonyme innere Klasse):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Lassen Sie uns nun die Ausgabe unseres Programms analysieren:

  • getName() gibt den Namen der Entität zurück.

  • getCanonicalName() gibt den kanonischen Namen der Basisklasse zurück, wie in der Java-Sprachspezifikation definiert. Gibt null zurück, wenn die Basisklasse keinen kanonischen Namen hat (d. h. wenn es sich um eine lokale oder anonyme Klasse oder ein Array handelt, dessen Elementtyp keinen kanonischen Namen hat).

  • getSimpleName() gibt den einfachen Namen der Basisklasse zurück, wie im Quellcode angegeben. Gibt eine leere Zeichenfolge zurück, wenn die Basisklasse anonym ist.

  • getTypeName() gibt einen informativen String für den Namen dieses Typs zurück.