CodeGym /Java blogg /Slumpmässig /Reflection API: Reflection. Den mörka sidan av Java
John Squirrels
Nivå
San Francisco

Reflection API: Reflection. Den mörka sidan av Java

Publicerad i gruppen
Hälsningar, unge Padawan. I den här artikeln ska jag berätta om Force, en kraft som Java-programmerare bara använder i till synes omöjliga situationer. Den mörka sidan av Java är Reflection API. I Java implementeras reflektion med hjälp av Java Reflection API.

Vad är Java-reflektion?

Det finns en kort, korrekt och populär definition på Internet. Reflektion ( från sent latin reflexio - att vända tillbaka ) är en mekanism för att utforska data om ett program medan det körs. Reflektion låter dig utforska information om fält, metoder och klasskonstruktorer. Reflection låter dig arbeta med typer som inte fanns vid kompileringstiden, men som blev tillgängliga under körtiden. Reflektion och en logiskt konsekvent modell för att utfärda felinformation gör det möjligt att skapa korrekt dynamisk kod. Med andra ord, en förståelse för hur reflektion fungerar i Java kommer att öppna upp ett antal fantastiska möjligheter för dig. Du kan bokstavligen jonglera med klasser och deras komponenter. Här är en grundläggande lista över vad reflektion tillåter:
  • Lär dig/bestäm ett objekts klass;
  • Få information om en klasss modifierare, 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 är okänt fram till körningstid;
  • Hämta och ställ in värden för ett objekts fält efter namn;
  • Kalla ett objekts metod efter namn.
Reflektion används i nästan all modern Java-teknik. Det är svårt att föreställa sig att Java, som plattform, skulle ha kunnat uppnå en sådan utbredd användning utan eftertanke. Troligtvis skulle det inte ha gjort det. Nu när du är allmänt bekant med reflektion som ett teoretiskt begrepp, låt oss gå vidare till dess praktiska tillämpning! Vi kommer inte att lära oss alla Reflection API:s metoder – bara de som du faktiskt kommer att stöta på i praktiken. Eftersom reflektion innebär att arbeta med klasser börjar vi med en enkel klass som heter MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Som du kan se är detta en väldigt grundläggande klass. Konstruktören med parametrar kommenteras medvetet. Vi återkommer till det senare. Om du tittade noga på innehållet i klassen märkte du förmodligen frånvaron av en getter för namnfältet . Själva namnfältet är markerat med modifieraren för privat åtkomst: vi kan inte komma åt det utanför själva klassen vilket betyder att vi inte kan hämta dess värde . " Så vad är problemet ?" du säger. "Lägg till en getter eller ändra åtkomstmodifieraren". Och du skulle ha rätt, om inteMyClassfanns i ett kompilerat AAR-bibliotek eller i en annan privat modul utan möjlighet att göra ändringar. I praktiken händer detta hela tiden. Och någon slarvig programmerare glömde helt enkelt att skriva en getter . Det här är hög tid att minnas reflektion! Låt oss försöka komma till klassens privata namnfält MyClass:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Låt oss analysera vad som just hände. I Java finns det en underbar klass som heter Class. Den representerar klasser och gränssnitt i en körbar Java-applikation. Vi kommer inte att täcka förhållandet mellan Classoch ClassLoadereftersom det inte är ämnet för den här artikeln. Därefter måste du anropa getFields()metoden för att hämta den här klassens fält. Denna metod kommer att returnera alla denna klasss tillgängliga fält. Detta fungerar inte för oss, eftersom vårt område är privat , så vi använder getDeclaredFields()metoden. Denna metod returnerar också en rad klassfält, men nu inkluderar den privata och skyddade fält. I det här fallet vet vi namnet på fältet vi är intresserade av, så vi kan använda metoden, getDeclaredField(String)varStringär det önskade fältets namn. Notera: getFields()och getDeclaredFields()returnera inte fälten i en föräldraklass! Bra. Vi har ett Fieldobjekt som refererar till vårt namn . Eftersom fältet inte var offentligt måste vi ge tillgång till att arbeta med det. Metoden setAccessible(true)låter oss gå vidare. Nu är namnfältet under vår fullständiga kontroll! Du kan hämta dess värde genom att anropa Fieldobjektets get(Object)metod, där Objectär en instans av vår MyClassklass. Vi konverterar typen till Stringoch tilldelar värdet till vår namnvariabel . Om vi ​​inte kan hitta en sättare för att ställa in ett nytt värde i namnfältet, kan du använda inställningsmetoden :

field.set(myClass, (String) "new value");
Grattis! Du har precis bemästrat grunderna för reflektion och fått tillgång till ett privat fält! Var uppmärksam på try/catchblockeringen och vilka typer av undantag som hanteras. IDE kommer att berätta för dig att deras närvaro krävs på egen hand, men du kan tydligt se med deras namn varför de är här. Gå vidare! Som du kanske har märkt MyClasshar vår klass redan en metod för att visa information om klassdata:

private void printData(){
       System.out.println(number + name);
   }
Men den här programmeraren lämnade sina fingeravtryck här också. Metoden har en modifierare för privat åtkomst, och vi måste skriva vår egen kod för att visa data varje gång. Vilken röra. Vart tog vår reflektion vägen? Skriv följande funktion:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
Proceduren här är ungefär densamma som den som används för att hämta ett fält. Vi kommer åt den önskade metoden med namn och ger åtkomst till den. Och på Methodobjektet kallar vi invoke(Object, Args)metoden, där Objectfinns också en instans av MyClassklassen. Argsär metodens argument, även om vårt inte har några. Nu använder vi printDatafunktionen för att visa information:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
Hurra! Nu har vi tillgång till klassens privata metod. Men vad händer om metoden har argument, och varför kommenteras konstruktören? Allt i sin egen tid. Det är tydligt från definitionen i början att reflektion låter dig skapa instanser av en klass vid körning (medan programmet körs)! Vi kan skapa ett objekt med klassens fullständiga namn. Klassens fullständiga namn är klassnamnet, inklusive sökvägen till dess paket .
Reflection API: Reflection.  Den mörka sidan av Java - 2
I min pakethierarki skulle det fullständiga namnet på MyClass vara "reflection.MyClass". Det finns också ett enkelt sätt att lära sig klassens namn (återställ klassens namn som en sträng):

MyClass.class.getName()
Låt oss använda Java-reflektion för att skapa en instans av klassen:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
När en Java-applikation startar läses inte alla klasser in i JVM. Om din kod inte hänvisar till MyClassklassen, kommer ClassLoader, som är ansvarig för att ladda klasser i JVM, aldrig ladda klassen. Det betyder att du måste tvinga ClassLoaderatt ladda den och få en klassbeskrivning i form av en Classvariabel. Det är därför vi har forName(String)metoden, där Stringär namnet på klassen vars beskrivning vi behöver. Efter att ha hämtat Сlassobjektet kommer ett anrop av metoden newInstance()att returnera ett Objectobjekt som skapats med den beskrivningen. Allt som återstår är att leverera detta föremål till vårMyClassklass. Häftigt! Det var svårt, men förståeligt, hoppas jag. Nu kan vi skapa en instans av en klass på bokstavligen en rad! Tyvärr kommer det beskrivna tillvägagångssättet bara att fungera med standardkonstruktorn (utan parametrar). Hur anropar man metoder och konstruktörer med parametrar? Det är dags att avkommentera vår konstruktör. Kan som förväntat newInstance()inte hitta standardkonstruktorn och fungerar inte längre. Låt oss skriva om klassens instansiering:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
Metoden getConstructors()ska anropas på klassdefinitionen för att erhålla klasskonstruktörer, och getParameterTypes()ska sedan anropas för att få en konstruktors parametrar:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Det får oss alla konstruktörer och deras parametrar. I mitt exempel syftar jag på en specifik konstruktör med specifika, tidigare kända parametrar. Och för att kalla denna konstruktor använder vi newInstancemetoden, till vilken vi skickar värdena för dessa parametrar. Det kommer att vara samma när du använder invokemetoder för att anropa. Detta väcker frågan: när är det praktiskt att anropa konstruktörer genom reflektion? Som redan nämndes i början kan modern Java-teknik inte klara sig utan Java Reflection API. Till exempel Dependency Injection (DI), som kombinerar kommentarer med reflektion av metoder och konstruktörer för att bilda den populära Darerbibliotek för Android-utveckling. Efter att ha läst den här artikeln kan du med säkerhet betrakta dig själv som utbildad i Java Reflection API:s sätt. De kallar inte reflektion för den mörka sidan av Java för ingenting. Det bryter fullständigt OOP-paradigmet. I Java döljer och begränsar inkapsling andras åtkomst till vissa programkomponenter. När vi använder den privata modifieraren avser vi att det fältet endast ska nås från klassen där det finns. Och vi bygger programmets efterföljande arkitektur utifrån denna princip. I den här artikeln har vi sett hur du kan använda reflektion för att tvinga dig fram var som helst. Det kreativa designmönstret Singletonär ett bra exempel på detta som en arkitektonisk lösning. Grundidén är att en klass som implementerar detta mönster bara kommer att ha en instans under körning av hela programmet. Detta uppnås genom att lägga till modifieraren för privat åtkomst till standardkonstruktorn. Och det skulle vara mycket dåligt om en programmerare använde reflektion för att skapa fler instanser av sådana klasser. Förresten, jag hörde nyligen en kollega ställa en mycket intressant fråga: kan en klass som implementerar Singleton-mönstret ärvas? Kan det vara så att ens reflektion i detta fall skulle vara maktlös? Lämna din feedback om artikeln och ditt svar i kommentarerna nedan och ställ dina egna frågor där!

Mer läsning:

Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION