Vad är Reflection API för?

Javas reflektionsmekanism tillåter en utvecklare att göra ändringar och få information om klasser, gränssnitt, fält och metoder vid körning utan att veta deras namn.

Reflection API låter dig också skapa nya objekt, anropsmetoder och hämta eller ställa in fältvärden.

Låt oss göra en lista över allt du kan göra med reflektion:

  • Identifiera/bestäm klassen för ett objekt
  • Få information om klassmodifierare, fält, metoder, konstanter, konstruktorer och superklasser
  • Ta reda på vilka metoder som hör till implementerade gränssnitt
  • Skapa en instans av en klass vars klassnamn inte är känt förrän programmet körs
  • Hämta och ställ in värdet på ett instansfält efter namn
  • Kalla en instansmetod vid namn

Nästan all modern Java-teknik använder reflektion. Det ligger till grund för de flesta av dagens Java / Java EE ramverk och bibliotek, till exempel:

  • Springramar för att bygga webbapplikationer
  • testramverket för JUnit

Om du aldrig har stött på dessa mekanismer tidigare, frågar du förmodligen varför allt detta är nödvändigt. Svaret är ganska enkelt men också väldigt vagt: reflektion ökar dramatiskt flexibiliteten och möjligheten att anpassa vår applikation och kod.

Men det finns alltid för- och nackdelar. Så låt oss nämna några nackdelar:

  • Brott mot applikationssäkerhet. Reflektion låter oss komma åt kod som vi inte borde (brott mot inkapsling).
  • Säkerhetsrestriktioner. Reflektion kräver körtidsbehörigheter som inte är tillgängliga för system som kör en säkerhetshanterare.
  • Låg prestanda. Reflektion i Java bestämmer typer dynamiskt genom att skanna klassvägen för att hitta klassen som ska laddas. Detta minskar programmets prestanda.
  • Svårt att underhålla. Kod som använder reflektion är svår att läsa och felsöka. Det är mindre flexibelt och svårare att underhålla.

Arbeta med klasser med hjälp av Reflection API

Alla reflektionsoperationer börjar med ett java.lang.Class -objekt. För varje typ av objekt skapas en oföränderlig instans av java.lang.Class . Den tillhandahåller metoder för att få objektegenskaper, skapa nya objekt och anropa metoder.

Låt oss titta på listan över grundläggande metoder för att arbeta med java.lang.Class :

Metod Handling
Sträng getName(); Returnerar namnet på klassen
int getModifiers(); Returnerar åtkomstmodifierare
Paket getPackage(); Returnerar information om ett paket
Klass getSuperclass(); Returnerar information om en överordnad klass
Klass[] getInterfaces(); Returnerar en rad gränssnitt
Konstruktör[] getConstructors(); Returnerar information om klasskonstruktörer
Fält[] getFields(); Returnerar fälten i en klass
Fält getField(String fieldName); Returnerar ett specifikt fält i en klass efter namn
Metod[] getMethods(); Returnerar en rad metoder

Dessa är de viktigaste metoderna för att få data om klasser, gränssnitt, fält och metoder. Det finns också metoder som låter dig få eller ställa in fältvärden och komma åt privata fält. Vi ska titta på dem lite senare.

Just nu ska vi prata om att skaffa själva java.lang.Class . Vi har tre sätt att göra detta.

1. Använda Class.forName

I ett program som körs måste du använda metoden forName(String className) för att få en klass.

Den här koden visar hur vi kan skapa klasser med hjälp av reflektion. Låt oss skapa en personklass som vi kan arbeta med:

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

Och den andra delen av vårt exempel är koden som använder reflektion:

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

Detta tillvägagångssätt är möjligt om klassens fullständiga namn är känt. Sedan kan du få motsvarande klass med den statiska metoden Class.forName() . Denna metod kan inte användas för primitiva typer.

2. Använda .class

Om en typ är tillgänglig men det inte finns någon instans av den, kan du få klassen genom att lägga till .class till typnamnet. Detta är det enklaste sättet att få klass av en primitiv typ.

Class aClass = Person.class;

3. Använda .getClass()

Om ett objekt är tillgängligt är det enklaste sättet att få en klass att anropa object.getClass() .

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

Vad är skillnaden mellan de två senaste tillvägagångssätten?

Använd A.class om du vet vilket klassobjekt du är intresserad av vid kodningstillfället. Om ingen instans är tillgänglig bör du använda .class .

Få metoderna för en klass

Låt oss titta på metoderna som returnerar metoderna i vår klass: getDeclaredMethods() och getMethods() .

getDeclaredMethods() returnerar en array som innehåller metodobjekt för alla deklarerade metoder i klassen eller gränssnittet som representeras av Class-objektet, inklusive offentliga, privata, standard- och skyddade metoder, men inte ärvda metoder.

getMethods() returnerar en array som innehåller Method- objekt för alla offentliga metoder i klassen eller gränssnittet som representeras av Class-objektet — de som deklareras av klassen eller gränssnittet, såväl som de som ärvts från superklasser och supergränssnitt.

Låt oss ta en titt på hur var och en av dem fungerar.

Låt oss börja med getDeclaredMethods() . För att återigen hjälpa oss att förstå skillnaden mellan de två metoderna kommer vi nedan att arbeta med den abstrakta Numbers -klassen. Låt oss skriva en statisk metod som konverterar vår metoduppsättning till 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());
    }
}

Här är resultatet av att köra den här koden:

byteValue
shortValue
intValue
longValue
float floatValue;
dubbelvärde

Det här är metoderna som deklareras i klassen Number . Vad returnerar getMethods() ? Låt oss ändra två rader i exemplet:

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

När vi gör detta kommer vi att se följande uppsättning metoder:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
vänta
vänta
vänta
är lika med
String
hashCode
getClass
notify
notifyAll

Eftersom alla klasser ärver Object returnerar vår metod också de offentliga metoderna för klassen Object .

Få fälten i en klass

Metoderna getFields och getDeclaredFields används för att hämta fälten i en klass. Som ett exempel, låt oss titta på klassen LocalDateTime . Vi kommer att skriva om vår 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());
    }
}

Som ett resultat av att köra den här koden får vi uppsättningen fält som finns i klassen LocalDateTime.

MIN
MAX
serialVersionUID
datum
tid

I analogi med vår tidigare undersökning av metoder, låt oss se vad som händer om vi ändrar koden lite:

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

Produktion:

MIN
MAX

Låt oss nu ta reda på skillnaden mellan dessa metoder.

Metoden getDeclaredFields returnerar en array av Field -objekt för alla fält som deklareras av klassen eller gränssnittet som representeras av dettaKlassobjekt.

Metoden getFields returnerar en array av Field -objekt för alla publika fält i klassen eller gränssnittet som representeras avKlassobjekt.

Låt oss nu titta inuti LocalDateTime .

KlassensMINochMAXfälten är offentliga, vilket innebär att de kommer att vara synliga genom getFields -metoden. Däremotdatum,tid,serialVersionUIDmetoder har den privata modifieraren, vilket innebär att de inte kommer att vara synliga genom getFields -metoden, men vi kan få dem med getDeclaredFields . Så här kan vi komma åt fältobjekt för privata fält.

Beskrivningar av andra metoder

Nu är det dags att prata om några metoder i klassklassen , nämligen:

Metod Handling
getModifiers Skaffar modifierare för vår klass
getPackage Får paketet som innehåller vår klass
få Superklass Skaffa föräldraklassen
getInterfaces Att få en uppsättning gränssnitt implementerade av en klass
hämta namn Att få det fullt kvalificerade klassnamnet
getSimpleName Få namnet på en klass

getModifiers()

Modifierare kan nås med enKlassobjekt.

Modifierare är nyckelord som public , static , interface , etc. Vi får modifierare med metoden getModifiers() :

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

Denna kod ställer in värdet på enintvariabel som är ett bitfält. Varje åtkomstmodifierare kan slås på eller av genom att ställa in eller radera motsvarande bit. Vi kan kontrollera modifierare med metoderna i klassen 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);
    }
}

Kom ihåg hur vår persons deklaration ser ut:

public class Person {}

Vi får följande utdata:

Klassmodifierare: 1
Är offentlig: sann
Är statisk: falsk
Är slutgiltig: falsk
Är abstrakt: falsk
Är gränssnitt: falsk

Om vi ​​gör vår klass abstrakt så har vi:

public abstract class Person {}

och denna utgång:

Klassmodifierare: 1025
Är offentlig: sann
Är statisk: falsk
Är slutgiltig: falsk
Är abstrakt: sann
Är gränssnitt: falsk

Vi ändrade åtkomstmodifieraren, vilket betyder att vi också ändrade data som returnerades genom de statiska metoderna för Modifier -klassen.

getPackage()

Genom att bara känna till en klass kan vi få information om dess paket:

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

getSuperclass()

Om vi ​​har ett Class-objekt kan vi komma åt dess överordnade klass:

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

Vi får den välkända Object- klassen:

class java.lang.Object

Men om vår klass har en annan föräldraklass, så ser vi den istället:

package com.company;

class Human {
    // Some info
}

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

    // Some info
}

Här får vi vår föräldraklass:

class com.company.Human

getInterfaces()

Så här kan vi få listan över gränssnitt implementerade av klassen:

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

Och låt oss inte glömma att ändra vår personklass :

public class Person implements Serializable {}

Produktion:

[gränssnitt java.io.Serialiserbart]

En klass kan implementera många gränssnitt. Det är därför vi får en mängdKlassföremål. I Java Reflection API representeras gränssnitt också avKlassföremål.

Observera: Metoden returnerar endast de gränssnitt som implementerats av den angivna klassen, inte dess överordnade klass. För att få en komplett lista över gränssnitt implementerade av klassen måste du referera till både den aktuella klassen och alla dess förfäder upp i arvskedjan.

getName() & getSimpleName() & getCanonicalName()

Låt oss skriva ett exempel som involverar en primitiv, en kapslad klass, en anonym klass och klassen 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());
    }
}

Resultatet av vårt program:

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

String.class (vanlig klass):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (kapslade klass):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (anonym inre klass):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Låt oss nu analysera vårt programs resultat:

  • getName() returnerar namnet på entiteten.

  • getCanonicalName() returnerar det kanoniska namnet på basklassen, enligt definitionen av Java Language Specification. Returnerar null om basklassen inte har ett kanoniskt namn (det vill säga om det är en lokal eller anonym klass eller en array vars elementtyp inte har ett kanoniskt namn).

  • getSimpleName() returnerar det enkla namnet på basklassen som specificerats i källkoden. Returnerar en tom sträng om basklassen är anonym.

  • getTypeName() returnerar en informativ sträng för namnet på denna typ.