Pentru ce este API-ul Reflection?

Mecanismul de reflectare al Java permite unui dezvoltator să facă modificări și să obțină informații despre clase, interfețe, câmpuri și metode în timpul execuției fără a le cunoaște numele.

API-ul Reflection vă permite, de asemenea, să creați noi obiecte, să apelați metode și să obțineți sau să setați valori de câmp.

Să facem o listă cu tot ce poți face folosind reflectarea:

  • Identificați/determinați clasa unui obiect
  • Obțineți informații despre modificatorii de clasă, câmpuri, metode, constante, constructori și superclase
  • Aflați ce metode aparțin interfețelor implementate
  • Creați o instanță a unei clase al cărei nume de clasă nu este cunoscut până când programul este executat
  • Obțineți și setați valoarea unui câmp de instanță după nume
  • Apelați o metodă de instanță după nume

Aproape toate tehnologiile moderne Java folosesc reflexia. Acesta stă la baza majorității cadrelor și bibliotecilor Java/Java EE de astăzi, de exemplu:

  • Cadre de primăvară pentru construirea de aplicații web
  • cadrul de testare JUnit

Dacă nu ați mai întâlnit niciodată aceste mecanisme, probabil că vă întrebați de ce sunt necesare toate acestea. Răspunsul este destul de simplu, dar și foarte vag: reflectarea crește dramatic flexibilitatea și capacitatea de a personaliza aplicația și codul nostru.

Dar există întotdeauna argumente pro și contra. Deci, să menționăm câteva dezavantaje:

  • Încălcări ale securității aplicației. Reflection ne permite să accesăm un cod pe care nu ar trebui (încălcarea încapsulării).
  • Restricții de securitate. Reflection necesită permisiuni de rulare care nu sunt disponibile pentru sistemele care rulează un manager de securitate.
  • Performanta scazuta. Reflection în Java determină tipurile în mod dinamic prin scanarea classpath pentru a găsi clasa de încărcat. Acest lucru reduce performanța programului.
  • Greu de întreținut. Codul care utilizează reflectarea este greu de citit și de depanat. Este mai puțin flexibil și mai greu de întreținut.

Lucrul cu clase folosind API-ul Reflection

Toate operațiunile de reflectare încep cu un obiect java.lang.Class . Pentru fiecare tip de obiect, este creată o instanță imuabilă a java.lang.Class . Oferă metode pentru obținerea proprietăților obiectului, crearea de noi obiecte și apelarea metodelor.

Să ne uităm la lista metodelor de bază pentru lucrul cu java.lang.Class :

Metodă Acțiune
String getName(); Returnează numele clasei
int getModifiers(); Returnează modificatorii de acces
Pachetul getPackage(); Returnează informații despre un pachet
Clasa getSuperclass(); Returnează informații despre o clasă părinte
Clasa[] getInterfaces(); Returnează o serie de interfețe
Constructor[] getConstructors(); Returnează informații despre constructorii de clasă
Câmpuri[] getFields(); Returnează câmpurile unei clase
Câmp getField(String fieldName); Returnează un câmp specific al unei clase după nume
Metoda[] getMethods(); Returnează o serie de metode

Acestea sunt cele mai importante metode pentru obținerea de date despre clase, interfețe, câmpuri și metode. Există, de asemenea, metode care vă permit să obțineți sau să setați valorile câmpurilor și să accesați câmpuri private . Ne vom uita la ele puțin mai târziu.

Chiar acum vom vorbi despre obținerea java.lang.Class în sine. Avem trei moduri de a face asta.

1. Folosind Class.forName

Într-o aplicație care rulează, trebuie să utilizați metoda forName(String className) pentru a obține o clasă.

Acest cod demonstrează cum putem crea clase folosind reflectarea. Să creăm o clasă Person cu care putem lucra:

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

Și a doua parte a exemplului nostru este codul care folosește reflectarea:

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

Această abordare este posibilă dacă este cunoscut numele complet al clasei. Apoi puteți obține clasa corespunzătoare folosind metoda statică Class.forName() . Această metodă nu poate fi utilizată pentru tipurile primitive.

2. Folosind .class

Dacă un tip este disponibil, dar nu există nicio instanță a acestuia, atunci puteți obține clasa adăugând .class la numele tipului. Acesta este cel mai simplu mod de a obține clasa unui tip primitiv.

Class aClass = Person.class;

3. Folosind .getClass()

Dacă un obiect este disponibil, atunci cea mai ușoară modalitate de a obține o clasă este să apelați object.getClass() .

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

Care este diferența dintre ultimele două abordări?

Utilizați A.class dacă știți de ce obiect de clasă sunteți interesat la momentul codificării. Dacă nu este disponibilă nicio instanță, atunci ar trebui să utilizați .class .

Obținerea metodelor unei clase

Să ne uităm la metodele care returnează metodele clasei noastre: getDeclaredMethods() și getMethods() .

getDeclaredMethods() returnează o matrice care conține obiecte Method pentru toate metodele declarate ale clasei sau interfeței reprezentate de obiectul Class, inclusiv metodele publice, private, implicite și protejate, dar nu metodele moștenite.

getMethods() returnează o matrice care conține obiecte Method pentru toate metodele publice ale clasei sau interfeței reprezentate de obiectul Class — cele declarate de clasă sau interfață, precum și cele moștenite de la superclase și superinterfețe.

Să aruncăm o privire la modul în care funcționează fiecare dintre ele.

Să începem cu getDeclaredMethods() . Pentru a ne ajuta din nou să înțelegem diferența dintre cele două metode, mai jos vom lucra cu clasa abstractă Numbers . Să scriem o metodă statică care va converti matricea noastră Method în 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());
    }
}

Iată rezultatul rulării acestui cod:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue

Acestea sunt metodele declarate în interiorul clasei Number . Ce returnează getMethods() ? Să schimbăm două rânduri în exemplu:

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

Făcând acest lucru, vom vedea următorul set de metode:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
wait
wait
wait
este egal cu
String
hashCode
getClass
notify
notifyAll

Deoarece toate clasele moștenesc Object , metoda noastră returnează și metodele publice ale clasei Object .

Obținerea câmpurilor unei clase

Metodele getFields și getDeclaredFields sunt folosite pentru a obține câmpurile unei clase. De exemplu, să ne uităm la clasa LocalDateTime . Ne vom rescrie codul:

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

Ca rezultat al executării acestui cod, obținem setul de câmpuri conținute în clasa LocalDateTime.

MIN
MAX
serialVersionUID
data
ora

Prin analogie cu examinarea noastră anterioară a metodelor, să vedem ce se întâmplă dacă schimbăm puțin codul:

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

Ieșire:

MIN
MAX

Acum să ne dăm seama care este diferența dintre aceste metode.

Metoda getDeclaredFields returnează o matrice de obiecte Field pentru toate câmpurile declarate de clasa sau interfața reprezentată de acestClasăobiect.

Metoda getFields returnează o matrice de obiecte Field pentru toate câmpurile publice ale clasei sau interfeței reprezentate deClasăobiect.

Acum să ne uităm în interiorul LocalDateTime .

Al claseiMINșiMAXcâmpurile sunt publice, ceea ce înseamnă că vor fi vizibile prin metoda getFields . Prin contrast, celData,timp,serialVersionUIDmetodele au modificatorul privat , ceea ce înseamnă că nu vor fi vizibile prin metoda getFields , dar le putem obține folosind getDeclaredFields . Acesta este modul în care putem accesa obiectele Field pentru câmpurile private .

Descrierea altor metode

Acum este timpul să vorbim despre câteva metode ale clasei Class , și anume:

Metodă Acțiune
getModifiers Obținerea modificatorilor pentru clasa noastră
getPackage Obținerea pachetului care conține clasa noastră
getSuperclass Obținerea clasei pentru părinți
getInterfaces Obținerea unei serii de interfețe implementate de o clasă
getName Obținerea numelui de clasă complet calificat
getSimpleName Obținerea numelui unei clase

getModifiers()

Modificatorii pot fi accesați folosind aClasăobiect.

Modificatorii sunt cuvinte cheie precum public , static , interface etc. Obținem modificatori folosind metoda getModifiers() :

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

Acest cod stabilește valoarea lui anintvariabilă care este un câmp de biți. Fiecare modificator de acces poate fi activat sau dezactivat prin setarea sau ștergerea bitului corespunzător. Putem verifica modificatorii folosind metodele din clasa 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);
    }
}

Amintiți-vă cum arată declarația Persoanei noastre:

public class Person {}

Obținem următoarea ieșire:

Modificatori de clasă: 1
Este public: adevărat
Este static: fals
Este final: fals
Este abstract: fals
Este interfață: fals

Dacă facem clasa noastră abstractă, atunci avem:

public abstract class Person {}

și această ieșire:

Modificatori de clasă: 1025
Este public: adevărat
Este static: fals
Este final: fals
Este abstract: adevărat
Este interfață: fals

Am schimbat modificatorul de acces, ceea ce înseamnă că am schimbat și datele returnate prin metodele statice ale clasei Modifier .

getPackage()

Cunoscând doar o clasă, putem obține informații despre pachetul acesteia:

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

getSuperclass()

Dacă avem un obiect Class, atunci putem accesa clasa părinte:

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

Obținem binecunoscuta clasă Object :

class java.lang.Object

Dar dacă clasa noastră are o altă clasă părinte, atunci o vom vedea în schimb:

package com.company;

class Human {
    // Some info
}

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

    // Some info
}

Aici avem clasa noastră de părinți:

class com.company.Human

getInterfaces()

Iată cum putem obține lista de interfețe implementate de clasă:

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

Și să nu uităm să ne schimbăm clasa Persoană :

public class Person implements Serializable {}

Ieșire:

[interfață java.io.Serializable]

O clasă poate implementa multe interfețe. De aceea primim o serie deClasăobiecte. În API-ul Java Reflection, interfețele sunt reprezentate și deClasăobiecte.

Vă rugăm să rețineți: metoda returnează numai interfețele implementate de clasa specificată, nu clasa părinte. Pentru a obține o listă completă a interfețelor implementate de clasă, trebuie să vă referiți atât la clasa curentă, cât și la toți strămoșii ei din lanțul de moștenire.

getName() și getSimpleName() și getCanonicalName()

Să scriem un exemplu care implică o primitivă, o clasă imbricată, o clasă anonimă și clasa 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());
    }
}

Rezultatul programului nostru:

int clasa (primitiva):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (clasa obisnuita):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (clasa imbricată):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

nou java.io.Serializable(){}.getClass() (clasa interioara anonima):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Acum să analizăm rezultatul programului nostru:

  • getName() returnează numele entității.

  • getCanonicalName() returnează numele canonic al clasei de bază, așa cum este definit de specificația limbajului Java. Returnează null dacă clasa de bază nu are un nume canonic (adică dacă este o clasă locală sau anonimă sau un tablou al cărui tip de element nu are un nume canonic).

  • getSimpleName() returnează numele simplu al clasei de bază, așa cum este specificat în codul sursă. Returnează un șir gol dacă clasa de bază este anonimă.

  • getTypeName() returnează un șir informativ pentru numele acestui tip.