Programmering

Se nærmere på Java Reflection API

I sidste måneds "Java dybdegående" talte jeg om introspektion og måder, hvorpå en Java-klasse med adgang til rå klassedata kunne se "inde" i en klasse og finde ud af, hvordan klassen var konstrueret. Yderligere viste jeg, at med tilføjelsen af ​​en klasselæsser kunne disse klasser indlæses i det kørende miljø og udføres. Dette eksempel er en form for statisk introspektion. Denne måned vil jeg se på Java Reflection API, som giver Java-klasser mulighed for at udføre dynamisk introspektion: evnen til at se inde i klasser, der allerede er indlæst.

Nytten af ​​introspektion

En af Java's styrker er, at den blev designet med den antagelse, at miljøet, hvor det kørte, ville ændre sig dynamisk. Klasser indlæses dynamisk, binding udføres dynamisk, og objektforekomster oprettes dynamisk, når de er nødvendige. Det, der historisk ikke har været meget dynamisk, er evnen til at manipulere "anonyme" klasser. I denne sammenhæng er en anonym klasse en, der indlæses eller præsenteres for en Java-klasse ved kørselstid, og hvis type tidligere ikke var kendt for Java-programmet.

Anonyme klasser

Det er svært at forklare og endnu sværere at designe i et program at støtte anonyme klasser. Udfordringen med at støtte en anonym klasse kan erklæres således: "Skriv et program, der, når det gives et Java-objekt, kan inkorporere det objekt i dets fortsatte drift." Den generelle løsning er ret vanskelig, men ved at begrænse problemet kan der oprettes nogle specialiserede løsninger. Der er to eksempler på specialiserede løsninger på denne problemklasse i 1.0-versionen af ​​Java: Java-applets og kommandolinieversionen af ​​Java-tolk.

Java-applets er Java-klasser, der indlæses af en kørende Java-virtuel maskine i forbindelse med en webbrowser og påberåbes. Disse Java-klasser er anonyme, fordi køretiden ikke på forhånd kender de nødvendige oplysninger til at påberåbe hver enkelt klasse. Problemet med at påberåbe sig en bestemt klasse løses dog ved hjælp af Java-klassen java.applet.Applet.

Almindelige superklasser, ligesom Appletog Java-grænseflader, ligesom AppletContext, løse problemet med anonyme klasser ved at oprette en tidligere aftalt kontrakt. Specifikt annoncerer en runtime-miljøleverandør, at hun kan bruge ethvert objekt, der overholder en bestemt grænseflade, og runtime-miljøforbrugeren bruger den specificerede interface i ethvert objekt, som han har til hensigt at levere til køretiden. I tilfælde af applets findes der en veldefineret grænseflade i form af en fælles superklasse.

Ulempen ved en fælles superklasseløsning, især i mangel af flere arv, er, at objekterne, der er bygget til at køre i miljøet, ikke også kan bruges i et andet system, medmindre systemet implementerer hele kontrakten. I tilfælde af Applet grænseflader, skal værtsmiljøet implementere AppletContext. Hvad dette betyder for appletløsningen er, at løsningen kun fungerer, når du indlæser applets. Hvis du sætter en forekomst af en Hashtable objekt på din webside og peg din browser til den, kan den ikke indlæses, fordi appletsystemet ikke kan fungere uden for dets begrænsede rækkevidde.

Ud over appleteksemplet hjælper introspektion med at løse et problem, jeg nævnte i sidste måned: At finde ud af, hvordan man starter udførelsen i en klasse, som kommandolinieversionen af ​​den virtuelle Java-maskine lige har indlæst. I dette eksempel skal den virtuelle maskine påberåbe sig en statisk metode i den indlæste klasse. Efter konvention navngives denne metode vigtigste og tager et enkelt argument - en matrix af Snor genstande.

Motivationen for en mere dynamisk løsning

Udfordringen med den eksisterende Java 1.0-arkitektur er, at der er problemer, der kan løses af et mere dynamisk introspektionsmiljø - såsom indlæselige UI-komponenter, indlæselige enhedsdrivere i et Java-baseret OS og dynamisk konfigurerbare redigeringsmiljøer. "Killer-appen" eller problemet, der fik Java Reflection API til at blive oprettet, var udviklingen af ​​en objektkomponentmodel til Java. Denne model er nu kendt som JavaBeans.

Komponenter til brugergrænseflader er et ideelt designpunkt for et introspektionssystem, fordi de har to meget forskellige forbrugere. På den ene side er komponentobjekterne forbundet sammen for at danne en brugergrænseflade som en del af et eller andet program. Alternativt skal der være en grænseflade til værktøjer, der manipulerer brugerkomponenter uden at skulle vide, hvad komponenterne er, eller, vigtigere, uden adgang til komponenternes kildekode.

Java Reflection API voksede ud fra behovene i JavaBeans brugergrænseflade-komponent API.

Hvad er refleksion?

Grundlæggende består Reflection API af to komponenter: objekter, der repræsenterer de forskellige dele af en klassefil, og et middel til at udtrække disse objekter på en sikker og sikker måde. Sidstnævnte er meget vigtigt, da Java giver mange sikkerhedsforanstaltninger, og det ville ikke give mening at give et sæt klasser, der annullerede disse sikkerhedsforanstaltninger.

Den første komponent i Reflection API er den mekanisme, der bruges til at hente oplysninger om en klasse. Denne mekanisme er indbygget i den valgte klasse Klasse. Den specielle klasse Klasse er den universelle type til metainformationen, der beskriver objekter i Java-systemet. Klasselæssere i Java-systemet returnerer objekter af typen Klasse. Indtil nu var de tre mest interessante metoder i denne klasse:

  • forName, der indlæser en klasse med et givet navn ved hjælp af den aktuelle klasselæsser

  • getName, som ville returnere navnet på klassen som en Snor objekt, som var nyttigt til at identificere objektreferencer efter deres klassenavn

  • newInstance, som påberåber nulkonstruktøren på klassen (hvis den findes) og returnerer en objektforekomst af den pågældende objektklasse

Til disse tre nyttige metoder tilføjer Reflection API nogle yderligere metoder til klassen Klasse. Disse er som følger:

  • getConstructor, getConstructors, getDeclaredConstructor
  • getMethod, getMethods, getDeclaredMethods
  • getField, getFields, getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

Ud over disse metoder blev der tilføjet mange nye klasser for at repræsentere de objekter, som disse metoder ville returnere. De nye klasser er for det meste en del af java.lang.reflekteret pakke, men nogle af de nye basistypeklasser (Ugyldig, Byte, og så videre) er i java.lang pakke. Beslutningen blev taget om at placere de nye klasser, hvor de er, ved at placere klasser, der repræsenterede metadata i refleksionspakken, og klasser, der repræsenterede typer i sprogpakken.

Således repræsenterer Reflection API et antal ændringer i klassen Klasse der giver dig mulighed for at stille spørgsmål om klassens indre og en masse klasser, der repræsenterer svarene, som disse nye metoder giver dig.

Hvordan bruger jeg Reflection API?

Spørgsmålet "Hvordan bruger jeg API'en?" er måske det mere interessante spørgsmål end "Hvad er refleksion?"

Reflection API er symmetrisk, hvilket betyder, at hvis du holder en Klasse objekt, kan du spørge om dens interne, og hvis du har en af ​​de interne, kan du spørge det, hvilken klasse der erklærede det. Således kan du bevæge dig frem og tilbage fra klasse til metode til parameter til klasse til metode osv. En interessant anvendelse af denne teknologi er at finde ud af det meste af den indbyrdes afhængighed mellem en given klasse og resten af ​​systemet.

Et fungerende eksempel

På et mere praktisk niveau kan du dog bruge Reflection API til at dumpe en klasse ud, ligesom min dumpklasse klasse gjorde i sidste måneds kolonne.

For at demonstrere Reflection API skrev jeg en klasse kaldet ReflectClass der ville tage en klasse, der er kendt for Java-kørselstiden (hvilket betyder, at den er i din klassesti et eller andet sted), og gennem Reflection API, dumpe dens struktur til terminalvinduet. For at eksperimentere med denne klasse skal du have en 1.1-version af JDK tilgængelig.

Bemærk: Gør ikke prøv at bruge en 1.0 kørselstid, da det bliver forvirret, hvilket normalt resulterer i en inkompatibel undtagelse for klasseændring.

Klassen ReflectClass begynder som følger:

import java.lang.reflect. *; importer java.util. *; offentlig klasse ReflectClass { 

Som du kan se ovenfor, er den første ting, koden gør, at importere Reflection API-klasser. Dernæst springer den lige ind i hovedmetoden, som starter som vist nedenfor.

 public static ugyldigt main (String args []) {Constructor cn []; Klasse cc []; Metode mm []; Felt ff []; Klasse c = null; Klasse supClass; Streng x, y, s1, s2, s3; Hashtable classRef = ny Hashtable (); if (args.length == 0) {System.out.println ("Angiv et klassenavn på kommandolinjen."); System.exit (1); } prøv {c = Class.forName (args [0]); } fange (ClassNotFoundException ee) {System.out.println ("Kunne ikke finde klasse '" + args [0] + "'"); System.exit (1); } 

Metoden vigtigste erklærer arrays af konstruktører, felter og metoder. Hvis du husker, er dette tre af de fire grundlæggende dele af klassefilen. Den fjerde del er attributterne, som Reflection API desværre ikke giver dig adgang til. Efter arrays har jeg foretaget nogle kommandolinjebehandlinger. Hvis brugeren har skrevet et klassenavn, forsøger koden at indlæse det ved hjælp af forName metode til klasse Klasse. Det forName Metoden tager Java-klassenavne, ikke filnavne, så for at se inde i java.math.BigInteger klasse, skal du blot skrive "java ReflectClass java.math.BigInteger" snarere end at påpege, hvor klassefilen faktisk er gemt.

Identificering af klassens pakke

Forudsat at klassefilen findes, fortsætter koden til trin 0, som er vist nedenfor.

 / * * Trin 0: Hvis vores navn indeholder prikker, er vi i en pakke, så sæt * det først. * / x = c.getName (); y = x.substring (0, x.lastIndexOf (".")); hvis (y.length ()> 0) {System.out.println ("pakke" + y + "; \ n \ r"); } 

I dette trin hentes klassens navn ved hjælp af getName metode i klassen Klasse. Denne metode returnerer det fuldt kvalificerede navn, og hvis navnet indeholder prikker, kan vi antage, at klassen blev defineret som en del af en pakke. Så trin 0 er at adskille pakkenavnedelen fra klassens navnedel og udskrive pakkenavndelen på en linje, der starter med "pakke ...."

Indsamling af klassereferencer fra erklæringer og parametre

Med pakkeerklæringen taget hånd om fortsætter vi til trin 1, som er at indsamle alle Andet klasse navne, som denne klasse refererer til. Denne indsamlingsproces vises i koden nedenfor. Husk, at de tre mest almindelige steder, hvor der refereres til klassenavne, er som typer for felter (instansvariabler), returtyper for metoder og som typerne af parametre, der sendes til metoder og konstruktører.

 ff = c.getDeclaredFields (); for (int i = 0; i <ff.length; i ++) {x = tName (ff [i] .getType (). getName (), classRef); } 

I ovenstående kode er arrayet ff initialiseres til at være en række af Mark genstande. Sløjfen samler typenavnet fra hvert felt og behandler det gennem t Navn metode. Det t Navn metoden er en simpel hjælper, der returnerer stenografienavnet for en type. Så java.lang.Streng bliver til Snor. Og det bemærker i en hashtable, hvilke objekter der er set. På dette tidspunkt er koden mere interesseret i at indsamle klassereferencer end at trykke.

Den næste kilde til klassereferencer er de parametre, der leveres til konstruktører. Det næste stykke kode, vist nedenfor, behandler hver erklæret konstruktør og samler referencerne fra parameterlisterne.

 cn = c.getDeclaredConstructors (); for (int i = 0; i 0) {for (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}} 

Som du kan se, har jeg brugt getParameterTypes metode i Konstruktør klasse for at give mig alle de parametre, som en bestemt konstruktør tager. Disse behandles derefter gennem t Navn metode.

En interessant ting at bemærke her er forskellen mellem metoden getDeclaredConstructors og metoden getConstructors. Begge metoder returnerer en række konstruktører, men getConstructors metode returnerer kun de konstruktører, der er tilgængelige for din klasse. Dette er nyttigt, hvis du vil vide, om du rent faktisk kan påkalde den konstruktør, du har fundet, men det er ikke nyttigt for denne applikation, fordi jeg vil udskrive alle konstruktørerne i klassen, offentligt eller ej. Felt- og metodereflekterne har også lignende versioner, en for alle medlemmer og en kun for offentlige medlemmer.

Det sidste trin, vist nedenfor, er at indsamle referencerne fra alle metoderne. Denne kode skal hente referencer fra både typen af ​​metode (svarende til felterne ovenfor) og fra parametrene (svarende til konstruktørerne ovenfor).

 mm = c.getDeclaredMethods (); for (int i = 0; i 0) {for (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}} 

I ovenstående kode er der to opkald til t Navn - en til at indsamle returtypen og en til at indsamle hver parameteres type.