Waar is de Reflection API voor?

Het reflectiemechanisme van Java stelt een ontwikkelaar in staat om tijdens runtime wijzigingen aan te brengen en informatie te verkrijgen over klassen, interfaces, velden en methoden zonder hun namen te kennen.

Met de Reflection API kunt u ook nieuwe objecten maken, methoden aanroepen en veldwaarden ophalen of instellen.

Laten we een lijst maken van alles wat je kunt doen met reflectie:

  • Identificeer/bepaal de klasse van een object
  • Krijg informatie over klassemodifiers, velden, methoden, constanten, constructors en superklassen
  • Ontdek welke methodes bij de geïmplementeerde interface(s) horen
  • Maak een instantie van een klasse waarvan de klassenaam pas bekend is als het programma wordt uitgevoerd
  • Haal de waarde van een instantieveld op en stel deze in op naam
  • Roep een instantiemethode bij naam aan

Bijna alle moderne Java-technologieën maken gebruik van reflectie. Het ligt ten grondslag aan de meeste hedendaagse Java / Java EE-frameworks en -bibliotheken, bijvoorbeeld:

  • Spring frameworks voor het bouwen van webapplicaties
  • het JUnit- testkader

Als je deze mechanismen nog nooit eerder bent tegengekomen, vraag je je waarschijnlijk af waarom dit allemaal nodig is. Het antwoord is vrij eenvoudig maar ook erg vaag: reflectie verhoogt de flexibiliteit en de mogelijkheid om onze applicatie en code aan te passen drastisch.

Maar er zijn altijd voor- en nadelen. Dus laten we een paar nadelen noemen:

  • Schendingen van applicatiebeveiliging. Reflectie geeft ons toegang tot code die we niet zouden moeten hebben (schending van inkapseling).
  • Beveiligingsbeperkingen. Reflection vereist runtimerechten die niet beschikbaar zijn voor systemen met een beveiligingsmanager.
  • Lage prestatie. Reflectie in Java bepaalt typen dynamisch door het klassenpad te scannen om de te laden klasse te vinden. Dit vermindert de prestaties van het programma.
  • Moeilijk te onderhouden. Code die reflectie gebruikt, is moeilijk te lezen en te debuggen. Het is minder flexibel en moeilijker te onderhouden.

Werken met klassen met behulp van de Reflection API

Alle reflectiebewerkingen beginnen met een java.lang.Class- object. Voor elk type object wordt een onveranderlijke instantie van java.lang.Class gemaakt. Het biedt methoden voor het verkrijgen van objecteigenschappen, het maken van nieuwe objecten en het aanroepen van methoden.

Laten we eens kijken naar de lijst met basismethoden voor het werken met java.lang.Class :

Methode Actie
Tekenreeks getName(); Retourneert de naam van de klasse
int getModifiers(); Retourneert toegangsmodifiers
Pakket getPackage(); Retourneert informatie over een pakket
Klasse getSuperclass(); Retourneert informatie over een bovenliggende klasse
Klasse[] getInterfaces(); Retourneert een reeks interfaces
Constructor[] getConstructors(); Retourneert informatie over klassenconstructors
Velden[] getFields(); Retourneert de velden van een klasse
Veld getField(String veldnaam); Retourneert een specifiek veld van een klasse op naam
Methode[] getMethods(); Retourneert een reeks methoden

Dit zijn de belangrijkste methoden om gegevens over klassen, interfaces, velden en methoden te verkrijgen. Er zijn ook methoden waarmee u veldwaarden kunt ophalen of instellen en toegang kunt krijgen tot privévelden . We zullen ze later bekijken.

Op dit moment zullen we het hebben over het verkrijgen van de java.lang.Class zelf. We hebben drie manieren om dit te doen.

1. Class.forName gebruiken

In een actieve toepassing moet u de methode forName(String className) gebruiken om een ​​klasse op te halen.

Deze code laat zien hoe we klassen kunnen maken met behulp van reflectie. Laten we een persoonsklasse maken waarmee we kunnen werken:

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

En het tweede deel van ons voorbeeld is de code die reflectie gebruikt:

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

Deze aanpak is mogelijk als de volledige naam van de klasse bekend is. Vervolgens kunt u de bijbehorende klasse ophalen met behulp van de statische methode Class.forName() . Deze methode kan niet worden gebruikt voor primitieve typen.

2. .class gebruiken

Als een type beschikbaar is maar er geen instantie van is, dan kunt u de klasse verkrijgen door .class aan de typenaam toe te voegen. Dit is de gemakkelijkste manier om de klasse van een primitief type te krijgen.

Class aClass = Person.class;

3. .getClass() gebruiken

Als er een object beschikbaar is, is de eenvoudigste manier om een ​​klasse te krijgen, object.getClass() aan te roepen .

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

Wat is het verschil tussen de laatste twee benaderingen?

Gebruik A.class als u weet in welk klasse-object u geïnteresseerd bent tijdens het coderen. Als er geen instantie beschikbaar is, moet u .class gebruiken .

De methoden van een klasse verkrijgen

Laten we eens kijken naar de methoden die de methoden van onze klasse retourneren: getDeclaredMethods() en getMethods() .

getDeclaredMethods() retourneert een array die Method- objecten bevat voor alle gedeclareerde methoden van de klasse of interface die wordt vertegenwoordigd door het Class-object, inclusief openbare, privé-, standaard- en beveiligde methoden, maar niet overgeërfde methoden.

getMethods() retourneert een array met Method- objecten voor alle openbare methoden van de klasse of interface die wordt vertegenwoordigd door het Class-object — zowel degene die zijn gedeclareerd door de klasse of interface als die welke zijn geërfd van superklassen en superinterfaces.

Laten we eens kijken hoe elk van hen werkt.

Laten we beginnen met getDeclaredMethods() . Om ons nogmaals te helpen het verschil tussen de twee methoden te begrijpen, zullen we hieronder werken met de abstracte klasse Numbers . Laten we een statische methode schrijven die onze Method- array zal converteren naar 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());
    }
}

Dit is het resultaat van het uitvoeren van deze code:

byteValue
shortValue
intValue
longValue
float floatValue;
dubbele waarde

Dit zijn de methoden die zijn gedeclareerd in de klasse Number . Wat retourneert getMethods() ? Laten we twee regels in het voorbeeld wijzigen:

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

Als we dit doen, zullen we de volgende reeks methoden zien:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
wacht
wacht
wacht
is gelijk
aanString
hashCode
getClass
notificatie
attendAll

Omdat alle klassen Object erven , retourneert onze methode ook de openbare methoden van de klasse Object .

De velden van een klasse krijgen

De methoden getFields en getDeclaredFields worden gebruikt om de velden van een klasse op te halen. Laten we als voorbeeld kijken naar de klasse LocalDateTime . We zullen onze code herschrijven:

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 resultaat van het uitvoeren van deze code krijgen we de set velden in de klasse LocalDateTime.

MIN
MAX
serialVersionUID
datum
tijd

Laten we naar analogie met ons eerdere onderzoek van methoden kijken wat er gebeurt als we de code een beetje veranderen:

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

Uitgang:

MIN
MAX

Laten we nu eens kijken wat het verschil is tussen deze methoden.

De methode getDeclaredFields retourneert een array van Field- objecten voor alle velden die zijn gedeclareerd door de klasse of interface die hierdoor wordt vertegenwoordigdKlasvoorwerp.

De methode getFields retourneert een array van Field- objecten voor alle openbare velden van de klasse of interface die wordt vertegenwoordigd door deKlasvoorwerp.

Laten we nu eens kijken in LocalDateTime .

Van de klasMINEnMAXvelden zijn openbaar, wat betekent dat ze zichtbaar zijn via de methode getFields . Daarentegen dedatum,tijd,seriëleVersieUIDmethoden hebben de private modifier, wat betekent dat ze niet zichtbaar zijn via de methode getFields , maar we kunnen ze krijgen met getDeclaredFields . Dit is hoe we toegang hebben tot Field-objecten voor privévelden .

Beschrijvingen van andere methoden

Nu is het tijd om te praten over enkele methoden van de klasse Class, namelijk:

Methode Actie
getModifiers De modifiers voor onze klas ophalen
pakPakket Het pakket krijgen dat onze klas bevat
krijg Superklasse De ouderklasse ophalen
krijgInterfaces Een reeks interfaces krijgen die door een klasse zijn geïmplementeerd
getNaam De volledig gekwalificeerde klassenaam verkrijgen
getSimpleName De naam van een klas krijgen

getModifiers()

Modifiers zijn toegankelijk via eenKlasvoorwerp.

Modifiers zijn trefwoorden zoals public , static , interface , enz. We krijgen modifiers met behulp van de getModifiers() methode:

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

Deze code bepaalt de waarde van eenintvariabele die een bitveld is. Elke toegangsmodificator kan worden in- of uitgeschakeld door de bijbehorende bit in te stellen of te wissen. We kunnen modifiers controleren met behulp van de methoden in de klasse 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);
    }
}

Bedenk hoe de verklaring van onze Persoon eruit ziet:

public class Person {}

We krijgen de volgende uitvoer:

Klassemodificaties: 1
Is openbaar: waar
Is statisch: onwaar
Is definitief: onwaar
Is abstract: onwaar
Is interface: onwaar

Als we onze klasse abstract maken, dan hebben we:

public abstract class Person {}

en deze uitvoer:

Klassemodificaties: 1025
Is openbaar: waar
Is statisch: onwaar
Is definitief: onwaar
Is abstract: waar
Is interface: onwaar

We hebben de toegangsmodifier gewijzigd, wat betekent dat we ook de gegevens hebben gewijzigd die worden geretourneerd via de statische methoden van de klasse Modifier .

pakPakket()

Als we alleen een klasse kennen, kunnen we informatie krijgen over het pakket:

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

haalSuperclass()

Als we een Class-object hebben, hebben we toegang tot de bovenliggende klasse:

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

We krijgen de bekende Object- klasse:

class java.lang.Object

Maar als onze klasse een andere bovenliggende klasse heeft, dan zien we die in plaats daarvan:

package com.company;

class Human {
    // Some info
}

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

    // Some info
}

Hier krijgen we onze ouderklasse:

class com.company.Human

getInterfaces()

Hier ziet u hoe we de lijst met interfaces kunnen krijgen die door de klasse zijn geïmplementeerd:

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

En laten we niet vergeten onze Person- klasse te wijzigen :

public class Person implements Serializable {}

Uitgang:

[interface java.io.Serializable]

Een klasse kan veel interfaces implementeren. Daarom krijgen we een reeks vanKlasvoorwerpen. In de Java Reflection API worden interfaces ook weergegeven doorKlasvoorwerpen.

Let op: de methode retourneert alleen de interfaces die zijn geïmplementeerd door de opgegeven klasse, niet de bovenliggende klasse. Om een ​​volledige lijst van interfaces te krijgen die door de klasse zijn geïmplementeerd, moet u verwijzen naar zowel de huidige klasse als al zijn voorouders in de overervingsketen.

getName() & getSimpleName() & getCanonicalName()

Laten we een voorbeeld schrijven met een primitieve, een geneste klasse, een anonieme klasse en de klasse 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());
    }
}

Resultaat van ons programma:

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

String.class (gewone klasse):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (geneste klasse):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (anonieme binnenklasse):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Laten we nu de uitvoer van ons programma analyseren:

  • getName() retourneert de naam van de entiteit.

  • getCanonicalName() retourneert de canonieke naam van de basisklasse, zoals gedefinieerd door de Java-taalspecificatie. Retourneert null als de basisklasse geen canonieke naam heeft (dat wil zeggen, als het een lokale of anonieme klasse is of een array waarvan het elementtype geen canonieke naam heeft).

  • getSimpleName() retourneert de eenvoudige naam van de basisklasse zoals gespecificeerd in de broncode. Retourneert een lege tekenreeks als de basisklasse anoniem is.

  • getTypeName() retourneert een informatieve string voor de naam van dit type.