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 Applet
og 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æssergetName
, som ville returnere navnet på klassen som enSnor
objekt, som var nyttigt til at identificere objektreferencer efter deres klassenavnnewInstance
, 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.