Programmering

Ta en grundig titt på Java Reflection API

I forrige måneds "Java In-Depth" snakket jeg om introspeksjon og måter som en Java-klasse med tilgang til rå klassedata kunne se "inne" i en klasse og finne ut hvordan klassen ble konstruert. Videre viste jeg at med tillegg av en klasselaster, kunne disse klassene lastes inn i løpemiljøet og utføres. Det eksemplet er en form for statisk introspeksjon. Denne måneden tar jeg en titt på Java Reflection API, som gir Java-klasser muligheten til å utføre dynamisk introspeksjon: muligheten til å se på klasser som allerede er lastet.

Nytten av introspeksjon

En av Java's styrker er at den ble designet med forutsetningen om at miljøet den kjørte i ville endres dynamisk. Klasser lastes dynamisk, binding gjøres dynamisk, og objektforekomster opprettes dynamisk når de er nødvendige. Det som ikke har vært veldig dynamisk historisk er evnen til å manipulere "anonyme" klasser. I denne sammenheng er en anonym klasse en som lastes eller presenteres for en Java-klasse på kjøretid, og hvis type tidligere var ukjent for Java-programmet.

Anonyme klasser

Å støtte anonyme klasser er vanskelig å forklare og enda vanskeligere å designe for i et program. Utfordringen med å støtte en anonym klasse kan sies slik: "Skriv et program som, når det gis et Java-objekt, kan innlemme det objektet i den fortsatte operasjonen." Den generelle løsningen er ganske vanskelig, men ved å begrense problemet kan det opprettes noen spesialiserte løsninger. Det er to eksempler på spesialiserte løsninger på denne klassen av problemer i 1.0-versjonen av Java: Java-appletter og kommandolinjeversjonen av Java-tolken.

Java-appletter er Java-klasser som lastes inn av en Java-virtuell maskin som kjører i sammenheng med en nettleser og påkalt. Disse Java-klassene er anonyme fordi kjøretiden ikke på forhånd vet den nødvendige informasjonen for å påkalle hver enkelt klasse. Problemet med å påkalle en bestemt klasse løses imidlertid ved hjelp av Java-klassen java.applet.Applet.

Vanlige superklasser, som Applet, og Java-grensesnitt, som AppletContext, løse problemet med anonyme klasser ved å lage en tidligere avtalt kontrakt. Spesielt annonserer en leverandør av kjøretidsmiljø at hun kan bruke ethvert objekt som samsvarer med et spesifisert grensesnitt, og kjøretidsforbrukeren bruker det spesifiserte grensesnittet i ethvert objekt han har til hensikt å levere til kjøretiden. Når det gjelder appletter, eksisterer et godt spesifisert grensesnitt i form av en vanlig superklasse.

Ulempen med en vanlig superklasseløsning, spesielt i mangel av flere arv, er at gjenstandene som er bygget for å kjøre i miljøet, ikke kan brukes i et annet system med mindre systemet implementerer hele kontrakten. I tilfelle av Applet grensesnitt, må vertsmiljøet implementere AppletContext. Hva dette betyr for appletløsningen er at løsningen bare fungerer når du laster inn applets. Hvis du legger inn en forekomst av en Hashtable objektet på websiden din og pek nettleseren mot den, kan den ikke lastes inn fordi applettsystemet ikke kan fungere utenfor det begrensede området.

I tillegg til appleteksemplet hjelper introspeksjon med å løse et problem jeg nevnte i forrige måned: finne ut hvordan du kan starte utførelsen i en klasse som kommandolinjeversjonen av den virtuelle Java-maskinen nettopp har lastet inn. I dette eksemplet må den virtuelle maskinen påberope seg en statisk metode i den lastede klassen. Etter konvensjon er denne metoden navngitt hoved- og tar et enkelt argument - en rekke String gjenstander.

Motivasjonen for en mer dynamisk løsning

Utfordringen med den eksisterende Java 1.0-arkitekturen er at det er problemer som kan løses av et mer dynamisk introspeksjonsmiljø - som lastbare UI-komponenter, lastbare enhetsdrivere i et Java-basert OS og dynamisk konfigurerbare redigeringsmiljøer. "Killer-appen", eller problemet som forårsaket at Java Reflection API ble opprettet, var utviklingen av en objektkomponentmodell for Java. Den modellen er nå kjent som JavaBeans.

Brukergrensesnittkomponenter er et ideelt designpunkt for et introspeksjonssystem fordi de har to veldig forskjellige forbrukere. På den ene siden er komponentobjektene koblet sammen for å danne et brukergrensesnitt som en del av en applikasjon. Alternativt må det være et grensesnitt for verktøy som manipulerer brukerkomponenter uten å måtte vite hva komponentene er, eller, enda viktigere, uten tilgang til komponentenes kildekode.

Java Reflection API vokste ut av behovene til JavaBeans API for brukergrensesnittkomponent.

Hva er refleksjon?

Fundamentalt består Reflection API av to komponenter: objekter som representerer de forskjellige delene av en klassefil, og et middel for å trekke ut disse objektene på en trygg og sikker måte. Sistnevnte er veldig viktig, ettersom Java gir mange sikkerhetsgarantier, og det ikke vil være fornuftig å tilby et sett med klasser som ugyldiggjør disse beskyttelsene.

Den første komponenten i Reflection API er mekanismen som brukes til å hente informasjon om en klasse. Denne mekanismen er innebygd i klassen som heter Klasse. Spesialklassen Klasse er den universelle typen for metainformasjonen som beskriver objekter i Java-systemet. Klasselastere i Java-systemet returnerer objekter av typen Klasse. Fram til nå var de tre mest interessante metodene i denne klassen:

  • forName, som vil laste en klasse med et gitt navn, ved hjelp av gjeldende klasselaster

  • getName, som ville returnere navnet på klassen som en String objekt, som var nyttig for å identifisere objektreferanser etter klassenavnet

  • newInstance, som vil påkalle nullkonstruktøren på klassen (hvis den eksisterer) og gi deg en objektforekomst av den objektklassen

Til disse tre nyttige metodene legger Reflection API til noen flere metoder i klassen Klasse. Disse er som følger:

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

I tillegg til disse metodene ble det lagt til mange nye klasser for å representere objektene som disse metodene ville returnere. De nye klassene er for det meste en del av java.lang.refleksjon pakken, men noen av de nye grunnleggende typeklassene (Tomrom, Byte, og så videre) er i java.lang pakke. Beslutningen ble tatt om å plassere de nye klassene der de er ved å sette klasser som representerte metadata i refleksjonspakken og klasser som representerte typer i språkpakken.

Dermed representerer Reflection API et antall endringer i klassen Klasse som lar deg stille spørsmål om det indre av klassen, og en haug med klasser som representerer svarene som disse nye metodene gir deg.

Hvordan bruker jeg Reflection API?

Spørsmålet "Hvordan bruker jeg API?" er kanskje det mer interessante spørsmålet enn "Hva er refleksjon?"

Reflection API er symmetrisk, som betyr at hvis du holder en Klasse objekt, kan du spørre om det indre, og hvis du har en av det indre, kan du spørre hvilken klasse som erklærte det. Dermed kan du gå frem og tilbake fra klasse til metode til parameter til klasse til metode, og så videre. En interessant bruk av denne teknologien er å finne ut det meste av gjensidig avhengighet mellom en gitt klasse og resten av systemet.

Et fungerende eksempel

På et mer praktisk nivå kan du imidlertid bruke Reflection API til å dumpe ut en klasse, akkurat som min dumpklasse klasse gjorde i forrige måneds spalte.

For å demonstrere Reflection API, skrev jeg en klasse som heter ReflectClass det vil ta en klasse som er kjent for Java-kjøretiden (noe som betyr at den er i klassestien din et sted) og, gjennom Reflection API, dumpe strukturen til terminalvinduet. For å eksperimentere med denne klassen, må du ha en 1.1-versjon av JDK tilgjengelig.

Merk: Gjør ikke prøv å bruke en 1.0 kjøretid da det blir forvirret, noe som vanligvis resulterer i et inkompatibelt unntak for klasseendring.

Klassen ReflectClass begynner som følger:

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

Som du kan se ovenfor, er det første koden gjør å importere Reflection API-klassene. Deretter hopper den rett inn i hovedmetoden, som starter som vist nedenfor.

 public static void 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 ("Vennligst spesifiser et klassenavn på kommandolinjen."); System.exit (1); } prøv {c = Class.forName (args [0]); } catch (ClassNotFoundException ee) {System.out.println ("Kunne ikke finne klasse '" + args [0] + "'"); System.exit (1); } 

Metoden hoved- erklærer matriser av konstruktører, felt og metoder. Hvis du husker, er dette tre av de fire grunnleggende delene av klassefilen. Den fjerde delen er attributtene, som Reflection API dessverre ikke gir deg tilgang til. Etter arrangementene har jeg gjort noen kommandolinjebehandling. Hvis brukeren har skrevet et klassenavn, prøver koden å laste det inn ved hjelp av forName metode for klassen Klasse. De forName metoden tar Java-klassenavn, ikke filnavn, så for å se inne i java.math.BigInteger klasse, skriver du bare "java ReflectClass java.math.BigInteger", i stedet for å påpeke hvor klassefilen faktisk er lagret.

Identifisere klassens pakke

Forutsatt at klassefilen er funnet, fortsetter koden til trinn 0, som er vist nedenfor.

 / * * Trinn 0: Hvis navnet vårt inneholder prikker, er vi i en pakke, så legg * 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 trinnet hentes navnet på klassen ved hjelp av getName metode i klassen Klasse. Denne metoden returnerer det fullstendige navnet, og hvis navnet inneholder prikker, kan vi anta at klassen ble definert som en del av en pakke. Så trinn 0 er å skille pakkenavnens del fra klassenavnens del, og skrive ut pakkenavnens del på en linje som starter med "pakke ...."

Samler klassereferanser fra erklæringer og parametere

Med pakkeuttalelsen ivaretatt fortsetter vi til trinn 1, som er å samle alle annen klasse navn som det er referert til i denne klassen. Denne innsamlingsprosessen vises i koden nedenfor. Husk at de tre vanligste stedene der det refereres til klassenavn, er som typer for felt (forekomstvariabler), returtyper for metoder og som typene av parametere som 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 koden ovenfor, matrisen ff er initialisert til å være en rekke Felt gjenstander. Sløyfen samler typenavnet fra hvert felt og behandler det gjennom t Navn metode. De t Navn metoden er en enkel hjelper som returnerer forkortelsesnavnet for en type. Så java.lang.Streng blir til String. Og den noterer i en hashtabell hvilke gjenstander som er sett. På dette stadiet er koden mer interessert i å samle klassereferanser enn å trykke.

Den neste kilden til klassereferanser er parametrene som leveres til konstruktører. Den neste koden, vist nedenfor, behandler hver erklærte konstruktør og samler referansene fra parameterlistene.

 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 ser, har jeg brukt getParameterTypes metoden i Konstruktør klasse for å gi meg alle parametrene som en bestemt konstruktør tar. Disse blir deretter behandlet gjennom t Navn metode.

En interessant ting å merke seg her er forskjellen mellom metoden getDeclaredConstructors og metoden getConstructors. Begge metodene returnerer en rekke konstruktører, men getConstructors metoden returnerer bare de konstruktørene som er tilgjengelige for klassen din. Dette er nyttig hvis du vil vite om du faktisk kan påkalle konstruktøren du har funnet, men det er ikke nyttig for dette programmet fordi jeg vil skrive ut alle konstruktørene i klassen, offentlig eller ikke. Felt- og metodereflektorene har også lignende versjoner, en for alle medlemmer og en bare for offentlige medlemmer.

Det siste trinnet, vist nedenfor, er å samle inn referanser fra alle metodene. Denne koden må hente referanser fra både typen metode (ligner på feltene ovenfor) og fra parametrene (ligner på konstruktørene 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 koden ovenfor er det to anrop til t Navn - en for å samle returtypen og en for å samle inn hver parametertype.

$config[zx-auto] not found$config[zx-overlay] not found