Hva er Reflection API for?

Javas refleksjonsmekanisme lar en utvikler gjøre endringer og få informasjon om klasser, grensesnitt, felt og metoder under kjøring uten å vite navnene deres.

Reflection API lar deg også opprette nye objekter, kalle metoder og hente eller angi feltverdier.

La oss lage en liste over alt du kan gjøre ved å bruke refleksjon:

  • Identifiser/bestem klassen til et objekt
  • Få informasjon om klassemodifikatorer, felt, metoder, konstanter, konstruktører og superklasser
  • Finn ut hvilke metoder som tilhører implementerte grensesnitt(er)
  • Opprett en forekomst av en klasse hvis klassenavn ikke er kjent før programmet er kjørt
  • Hent og angi verdien til et forekomstfelt etter navn
  • Kall en forekomstmetode ved navn

Nesten alle moderne Java-teknologier bruker refleksjon. Det ligger til grunn for de fleste av dagens Java / Java EE-rammeverk og biblioteker, for eksempel:

  • Springrammer for å bygge webapplikasjoner
  • testrammeverket for JUnit

Hvis du aldri har møtt disse mekanismene før, spør du sannsynligvis hvorfor alt dette er nødvendig. Svaret er ganske enkelt, men også veldig vagt: refleksjon øker dramatisk fleksibiliteten og muligheten til å tilpasse applikasjonen og koden vår.

Men det er alltid fordeler og ulemper. Så la oss nevne noen ulemper:

  • Brudd på applikasjonssikkerhet. Refleksjon lar oss få tilgang til kode som vi ikke burde (brudd på innkapsling).
  • Sikkerhetsrestriksjoner. Refleksjon krever kjøretidstillatelser som ikke er tilgjengelige for systemer som kjører en sikkerhetsadministrator.
  • Lav ytelse. Refleksjon i Java bestemmer typer dynamisk ved å skanne klassebanen for å finne klassen som skal lastes. Dette reduserer programmets ytelse.
  • Vanskelig å vedlikeholde. Kode som bruker refleksjon er vanskelig å lese og feilsøke. Den er mindre fleksibel og vanskeligere å vedlikeholde.

Arbeide med klasser ved hjelp av Reflection API

Alle refleksjonsoperasjoner begynner med et java.lang.Class -objekt. For hver type objekt opprettes en uforanderlig forekomst av java.lang.Class . Det gir metoder for å få objektegenskaper, lage nye objekter og kalle metoder.

La oss se på listen over grunnleggende metoder for å jobbe med java.lang.Class :

Metode Handling
String getName(); Returnerer navnet på klassen
int getModifiers(); Returnerer tilgangsmodifikatorer
Pakke getPackage(); Returnerer informasjon om en pakke
Klasse getSuperclass(); Returnerer informasjon om en overordnet klasse
Klasse[] getInterfaces(); Returnerer en rekke grensesnitt
Konstruktør[] getConstructors(); Returnerer informasjon om klassekonstruktører
Felt[] getFields(); Returnerer feltene til en klasse
Felt getField(String fieldName); Returnerer et spesifikt felt i en klasse etter navn
Metode[] getMethods(); Returnerer en rekke metoder

Dette er de viktigste metodene for å få data om klasser, grensesnitt, felt og metoder. Det finnes også metoder som lar deg hente eller angi feltverdier og få tilgang til private felt. Vi skal se på dem litt senere.

Akkurat nå skal vi snakke om å få selve java.lang.Class . Vi har tre måter å gjøre dette på.

1. Bruke Class.forName

I en kjørende applikasjon må du bruke forName(String className) -metoden for å få en klasse.

Denne koden demonstrerer hvordan vi kan lage klasser ved hjelp av refleksjon. La oss lage en personklasse som vi kan jobbe 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;
    }
}

Og den andre delen av eksemplet vårt er koden som bruker refleksjon:


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

Denne tilnærmingen er mulig hvis hele navnet på klassen er kjent. Deretter kan du få den tilsvarende klassen ved å bruke den statiske Class.forName() -metoden. Denne metoden kan ikke brukes for primitive typer.

2. Bruke .class

Hvis en type er tilgjengelig, men det ikke finnes noen forekomst av den, kan du få klassen ved å legge til .class i typenavnet. Dette er den enkleste måten å få klassen til en primitiv type.


Class aClass = Person.class;

3. Bruke .getClass()

Hvis et objekt er tilgjengelig, er den enkleste måten å få en klasse på å kalle object.getClass() .


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

Hva er forskjellen mellom de to siste tilnærmingene?

Bruk A.class hvis du vet hvilket klasseobjekt du er interessert i på kodetidspunktet. Hvis ingen forekomst er tilgjengelig, bør du bruke .class .

Få metodene til en klasse

La oss se på metodene som returnerer metodene til klassen vår: getDeclaredMethods() og getMethods() .

getDeclaredMethods() returnerer en matrise som inneholder metodeobjekter for alle deklarerte metodene i klassen eller grensesnittet representert av klasseobjektet, inkludert offentlige, private, standard og beskyttede metoder, men ikke nedarvede metoder.

getMethods() returnerer en matrise som inneholder Method- objekter for alle offentlige metoder i klassen eller grensesnittet representert av Class-objektet - de som er deklarert av klassen eller grensesnittet, samt de som er arvet fra superklasser og supergrensesnitt.

La oss ta en titt på hvordan hver av dem fungerer.

La oss starte med getDeclaredMethods() . For igjen å hjelpe oss å forstå forskjellen mellom de to metodene, vil vi nedenfor jobbe med den abstrakte Numbers- klassen. La oss skrive en statisk metode som vil konvertere vår Metode- array til 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());
    }
}

Her er resultatet av å kjøre denne koden:

byteValue
shortValue
intValue
longValue
float floatValue;
dobbelverdi

Dette er metodene som er deklarert i Number- klassen. Hva returnerer getMethods() ? La oss endre to linjer i eksemplet:


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

Når du gjør dette, vil vi se følgende sett med metoder:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
vent
vent
vent
lik
String
hashCode
getClass
varsle
varsle Alle

Fordi alle klasser arver Object , returnerer metoden vår også de offentlige metodene til Object- klassen.

Få feltene til en klasse

Metodene getFields og getDeclaredFields brukes til å hente feltene til en klasse. Som et eksempel, la oss se på LocalDateTime -klassen. Vi skriver om koden vår:


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 et resultat av å kjøre denne koden, får vi settet med felter i LocalDateTime-klassen.

MIN
MAX
serialVersionUID
dato
klokkeslett

I analogi med vår tidligere undersøkelse av metoder, la oss se hva som skjer hvis vi endrer koden litt:


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

Produksjon:

MIN
MAKS

La oss nå finne ut forskjellen mellom disse metodene.

GetDeclaredFields - metoden returnerer en rekke feltobjekter for alle felt deklarert av klassen eller grensesnittet representert av denneKlassegjenstand.

GetFields - metoden returnerer en rekke feltobjekter for alle offentlige felt i klassen eller grensesnittet representert avKlassegjenstand.

La oss nå se på LocalDateTime .

KlassensMINogMAKSfelt er offentlige, noe som betyr at de vil være synlige gjennom getFields- metoden. DerimotDato,tid,serialVersionUIDmetoder har den private modifikatoren, noe som betyr at de ikke vil være synlige gjennom getFields- metoden, men vi kan få dem ved å bruke getDeclaredFields . Slik kan vi få tilgang til feltobjekter for private felt.

Beskrivelser av andre metoder

Nå er det på tide å snakke om noen metoder i klasseklassen , nemlig:

Metode Handling
getModifiers Henter modifikatorene for klassen vår
getPackage Får pakken som inneholder klassen vår
få superklasse Får foreldreklassen
getInterfaces Få en rekke grensesnitt implementert av en klasse
getName Får det fullt kvalifiserte klassenavnet
getSimpleName Får navnet på en klasse

getModifiers()

Modifikatorer kan nås ved å bruke enKlassegjenstand.

Modifikatorer er nøkkelord som public , static , interface , etc. Vi får modifikatorer ved å bruke getModifiers()- metoden:


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

Denne koden setter verdien av enintvariabel som er et bitfelt. Hver tilgangsmodifikator kan slås på eller av ved å stille inn eller slette den tilsvarende biten. Vi kan sjekke modifikatorer ved å bruke metodene i java.lang.reflect.Modifier -klassen:


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

Husk hvordan erklæringen til vår person ser ut:


public class Person {
   …
}

Vi får følgende utgang:

Klassemodifikatorer: 1
Er offentlig: sann
Er statisk: usann
Er endelig: usann
Er abstrakt: usann
Er grensesnitt: usann

Hvis vi gjør klassen abstrakt, har vi:


public abstract class Person { … }

og denne utgangen:

Klassemodifikatorer: 1025
Er offentlig: sann
Er statisk: usann
Er endelig: usann
Er abstrakt: sann
Er grensesnitt: usann

Vi endret tilgangsmodifikatoren, noe som betyr at vi også endret dataene som ble returnert gjennom de statiske metodene til Modifier -klassen.

getPackage()

Når vi bare kjenner en klasse, kan vi få informasjon om pakken:


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

getSuperclass()

Hvis vi har et klasseobjekt, kan vi få tilgang til dets overordnede klasse:


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 velkjente Objektklassen :


class java.lang.Object

Men hvis klassen vår har en annen foreldreklasse, vil vi se den i stedet:


package com.company;

class Human {
    // Some info
}

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

    // Some info
}

Her får vi foreldreklassen vår:


class com.company.Human

getInterfaces()

Slik kan vi få listen over grensesnitt implementert av klassen:


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

Og la oss ikke glemme å endre personklassen vår :


public class Person implements Serializable { … }

Produksjon:

[grensesnitt java.io.Serialiserbar]

En klasse kan implementere mange grensesnitt. Det er derfor vi får en rekkeKlassegjenstander. I Java Reflection API er grensesnitt også representert avKlassegjenstander.

Merk: Metoden returnerer bare grensesnittene implementert av den spesifiserte klassen, ikke dens overordnede klasse. For å få en fullstendig liste over grensesnitt implementert av klassen, må du referere til både gjeldende klasse og alle dens forfedre oppover i arvekjeden.

getName() & getSimpleName() & getCanonicalName()

La oss skrive et eksempel som involverer en primitiv, en nestet klasse, en anonym klasse og String- klassen:


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 programmet vårt:

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

String.class (ordinær klasse):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

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

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

La oss nå analysere programmets utgang:

  • getName() returnerer navnet på enheten.

  • getCanonicalName() returnerer det kanoniske navnet på basisklassen, som definert av Java Language Specification. Returnerer null hvis basisklassen ikke har et kanonisk navn (det vil si hvis det er en lokal eller anonym klasse eller en matrise hvis elementtype ikke har et kanonisk navn).

  • getSimpleName() returnerer det enkle navnet på basisklassen som spesifisert i kildekoden. Returnerer en tom streng hvis basisklassen er anonym.

  • getTypeName() returnerer en informativ streng for navnet på denne typen.