Ohjelmointi

Tutustu perusteellisesti Java Reflection -sovellusliittymään

Viime kuussa julkaisussa "Java In-Depth" puhuin itsetarkastumisesta ja tavoista, joilla Java-luokka, jolla on pääsy raakaluokan tietoihin, voisi näyttää "luokan sisällä" ja selvittää, miten luokka rakennettiin. Lisäksi osoitin, että lisäämällä luokan kuormaaja, nämä luokat voitiin ladata juoksevaan ympäristöön ja suorittaa. Tuo esimerkki on eräänlainen staattinen itsetarkastelu. Tässä kuussa tarkastelen Java Reflection -sovellusliittymää, joka antaa Java-luokille mahdollisuuden suorittaa dynaaminen itsetarkastus: kyky katsoa jo ladattujen luokkien sisälle.

Itsetarkastelun hyödyllisyys

Yksi Java-vahvuuksista on, että se on suunniteltu olettaen, että ympäristö, jossa se toimi, muuttuisi dynaamisesti. Luokat ladataan dynaamisesti, sidonta tapahtuu dynaamisesti ja objektin esiintymät luodaan dynaamisesti lennossa, kun niitä tarvitaan. Mikä ei ole historiallisesti ollut kovin dynaamista, on kyky manipuloida "nimettömiä" luokkia. Tässä yhteydessä anonyymi luokka on luokka, joka ladataan tai esitetään Java-luokalle ajon aikana ja jonka tyyppi ei aiemmin ollut Java-ohjelman tiedossa.

Nimetön luokat

Anonyymien luokkien tukeminen on vaikea selittää ja sitä on vielä vaikeampi suunnitella ohjelmassa. Anonyymin luokan tukemisen haaste voidaan sanoa näin: "Kirjoita ohjelma, joka antaa Java-objektin voidessaan sisällyttää kyseisen objektin jatkuvaan toimintaansa." Yleinen ratkaisu on melko vaikea, mutta ongelmaa rajoittamalla voidaan luoda joitain erikoistuneita ratkaisuja. Java-version 1.0 versiossa on kaksi esimerkkiä tämän luokan ongelmien erikoisratkaisuista: Java-sovelmat ja Java-tulkin komentoriviversio.

Java-sovelmat ovat Java-luokkia, jotka käynnissä oleva Java-virtuaalikone lataa verkkoselaimen yhteydessä ja joita ne kutsutaan. Nämä Java-luokat ovat nimettömiä, koska ajoaika ei tiedä etukäteen tarvittavia tietoja kunkin luokan kutsumiseen. Tietyn luokan kutsumisen ongelma ratkaistaan ​​kuitenkin Java-luokan avulla java.applet.Applet.

Yleiset superluokat, kuten Applettija Java-käyttöliittymät, kuten AppletContext, käsittele nimettömien luokkien ongelmaa luomalla aiemmin sovittu sopimus. Erityisesti ajonaikaisen ympäristön toimittaja mainostaa voivansa käyttää mitä tahansa määritetyn käyttöliittymän mukaista objektia, ja ajonaikaisen ympäristön kuluttaja käyttää kyseistä määritettyä rajapintaa missä tahansa objektissa, jonka hän aikoo toimittaa ajoaikaan. Apletien tapauksessa on olemassa hyvin määritelty käyttöliittymä yhteisen superluokan muodossa.

Yhteisen superluokkaratkaisun haittapuoli, etenkin jos ei ole useita perintöjä, on se, että ympäristössä toimivia esineitä ei voida käyttää myöskään muussa järjestelmässä, ellei kyseinen järjestelmä toteuta koko sopimusta. Jos kyseessä on Appletti käyttöliittymiä, isäntäympäristön on toteutettava AppletContext. Tämä tarkoittaa applettiratkaisulle, että ratkaisu toimii vain ladattaessa sovelmia. Jos laitat esimerkiksi a Hashtable ja osoita selaimesi siihen, se ei lataudu, koska sovelmajärjestelmä ei voi toimia rajoitetun alueensa ulkopuolella.

Sovellusesimerkin lisäksi introspektio auttaa ratkaisemaan viime kuussa mainitsemani ongelman: selvittää kuinka aloittaa suoritus luokassa, jonka Java-virtuaalikoneen komentoriviversio on juuri ladattu. Tässä esimerkissä virtuaalikoneen on käytettävä jotakin staattista menetelmää ladatussa luokassa. Tavanomaisesti menetelmä nimetään tärkein ja ottaa yhden argumentin - joukon Merkkijono esineitä.

Motivaatio dynaamisemmalle ratkaisulle

Nykyisen Java 1.0 -arkkitehtuurin haasteena on, että on ongelmia, jotka voidaan ratkaista dynaamisemmalla introspektioympäristöllä - kuten ladattavat käyttöliittymäkomponentit, ladattavat laiteohjaimet Java-pohjaisessa käyttöjärjestelmässä ja dynaamisesti konfiguroitavat muokkausympäristöt. "Tappajasovellus" tai asia, joka aiheutti Java Reflection -sovellusliittymän luomisen, oli Java-objektikomponenttimallin kehittäminen. Tämä malli tunnetaan nyt nimellä JavaBeans.

Käyttöliittymäkomponentit ovat ihanteellinen suunnittelupiste itsetarkastelujärjestelmälle, koska niillä on kaksi hyvin erilaista kuluttajaa. Toisaalta komponenttiobjektit linkitetään yhteen muodostamaan käyttöliittymä osana jotakin sovellusta. Vaihtoehtoisesti on oltava käyttöliittymä työkaluille, jotka manipuloivat käyttäjän komponentteja tarvitsematta tietää mitä komponentit ovat, tai mikä tärkeintä, ilman pääsyä komponenttien lähdekoodiin.

Java Reflection -sovellusliittymä kasvoi JavaBeansin käyttöliittymäkomponentin sovellusliittymän tarpeista.

Mikä on heijastus?

Pohjimmiltaan Reflection-sovellusliittymä koostuu kahdesta komponentista: objekteista, jotka edustavat luokkatiedoston eri osia, ja keinosta purkaa nämä objektit turvallisella tavalla. Jälkimmäinen on erittäin tärkeää, koska Java tarjoaa monia suojaustoimia, eikä ole järkevää tarjota sarjaa luokkia, jotka mitätöivät nämä suojaukset.

Reflection API: n ensimmäinen komponentti on mekanismi, jota käytetään luokan tietojen hakemiseen. Tämä mekanismi on rakennettu nimettyyn luokkaan Luokka. Erityisluokka Luokka on yleistyyppi metatiedoille, jotka kuvaavat Java-järjestelmän objekteja. Luokkakuormaajat Java-järjestelmässä palauttavat tyyppisiä objekteja Luokka. Tähän asti tämän luokan kolme mielenkiintoisinta menetelmää olivat:

  • forName, joka lataa tietyn nimen luokan nykyisen luokan lataajan avulla

  • getName, joka palauttaisi luokan nimen a Merkkijono objekti, josta oli hyötyä objektiviittausten tunnistamisessa luokan nimen perusteella

  • newInstance, joka kutsuisi luokan null-konstruktorin (jos sellainen on olemassa) ja palauttaisi sinulle objektiluokan objektiluokan

Näihin kolmeen hyödylliseen menetelmään Reflection-sovellusliittymä lisää joitain lisämenetelmiä luokkaan Luokka. Nämä ovat seuraavat:

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

Näiden menetelmien lisäksi lisättiin monia uusia luokkia edustamaan objekteja, jotka nämä menetelmät palauttaisivat. Uudet luokat ovat pääosin osa java.lang.reflect paketti, mutta jotkut uusista perustyyppiluokista (Tarpeeton, Tavu, ja niin edelleen) ovat java.lang paketti. Uusien luokkien sijoittaminen päätettiin sijoittamalla metadataa edustavat luokat pohdintapakettiin ja tyypit edustavat luokat kielipakettiin.

Siten Reflection API edustaa useita muutoksia luokkaan Luokka jonka avulla voit esittää kysymyksiä luokan sisäosista ja joukosta luokkia, jotka edustavat vastauksia, joita nämä uudet menetelmät antavat sinulle.

Kuinka käytän Reflection API: ta?

Kysymys "Kuinka käytän sovellusliittymää?" on ehkä mielenkiintoisempi kysymys kuin "Mikä on pohdintaa?"

Reflection API on symmetrinen, mikä tarkoittaa, että jos sinulla on a Luokka esine, voit kysyä sen sisäosista, ja jos sinulla on jokin sisäpuolista, voit kysyä, mikä luokka ilmoitti sen. Näin voit siirtyä edestakaisin luokasta toiseen parametrista toiseen ja niin edelleen. Yksi tämän tekniikan mielenkiintoinen käyttö on selvittää suurin osa tietyn luokan ja muun järjestelmän välisistä riippuvuuksista.

Toimiva esimerkki

Käytännön tasolla voit kuitenkin käyttää Reflection-sovellusliittymää luokan poistamiseen, aivan kuten minunkin kaatopaikka luokka teki viime kuukauden sarakkeessa.

Reflection API -sovelluksen osoittamiseksi kirjoitin luokan nimeltä ReflectClass se vie luokan, joka tunnetaan Java-ajoajalla (eli se on luokkasi polulla jonnekin) ja heijastaa API: n kautta sen rakenteen pääteikkunaan. Jos haluat kokeilla tätä luokkaa, sinulla on oltava käytettävissä JDK: n 1.1-versio.

Huomaa: Tee ei yritä käyttää 1.0-ajoaikaa, koska se hämmentyy, mikä johtaa yleensä yhteensopimattomaan luokkamuutospoikkeukseen.

Luokka ReflectClass alkaa seuraavasti:

tuo java.lang.reflect. *; tuo java.util. *; julkinen luokka ReflectClass { 

Kuten yllä voit nähdä, koodin ensimmäinen asia on tuoda Reflection API -luokat. Seuraavaksi se hyppää suoraan päämenetelmään, joka alkaa kuten alla on esitetty.

 public static void main (String args []) {Rakentaja cn []; Luokka cc []; Menetelmä mm []; Kenttä ff []; Luokka c = nolla; Luokka supClass; Merkkijono x, y, s1, s2, s3; Hashtable classRef = uusi Hashtable (); if (args.length == 0) {System.out.println ("Määritä luokan nimi komentoriville."); System.exit (1); } kokeile {c = Class.forName (argumentit [0]); } catch (ClassNotFoundException ee) {System.out.println ("Luokkaa ei löytynyt" + args [0] + "'"); System.exit (1); } 

Menetelmä tärkein julistaa rakentajien, kenttien ja menetelmien taulukot. Jos muistat, nämä ovat kolme luokkatiedoston neljästä perusosasta. Neljäs osa on määritteet, joihin Reflection API ei valitettavasti anna pääsyä. Taulukoiden jälkeen olen suorittanut komentorivikäsittelyn. Jos käyttäjä on kirjoittanut luokan nimen, koodi yrittää ladata sen käyttämällä forName luokan menetelmä Luokka. forName menetelmä vie Java-luokkien nimet, ei tiedostojen nimiä java.math.BigInteger luokka, kirjoita yksinkertaisesti "java ReflectClass java.math.BigInteger" sen sijaan, että osoittaisit, mihin luokkatiedosto todella on tallennettu.

Tunnistaa luokan paketti

Olettaen, että luokkatiedosto löytyy, koodi etenee vaiheeseen 0, joka on esitetty alla.

 / * * Vaihe 0: Jos nimessämme on pisteitä, olemme paketissa, joten laita se ensin. * / x = c.getName (); y = xsubstring (0, x.lastIndexOf (".")); if (y.pituus ()> 0) {System.out.println ("paketti" + y + "; \ n \ r"); } 

Tässä vaiheessa luokan nimi haetaan käyttämällä getName menetelmä luokassa Luokka. Tämä menetelmä palauttaa täysin hyväksytyn nimen, ja jos nimi sisältää pisteitä, voimme olettaa, että luokka määritettiin osana pakettia. Joten vaihe 0 on erottaa paketin nimiosa luokan nimiosasta ja tulostaa paketin nimiosa rivillä, joka alkaa "paketti ...."

Luokkaviitteiden kerääminen ilmoituksista ja parametreista

Kun pakettilauseke on hoidettu, siirrymme vaiheeseen 1, joka on kerätä kaikki muut luokan nimet, joihin tämä luokka viittaa. Tämä keräysprosessi näkyy alla olevassa koodissa. Muista, että kolme yleisintä paikkaa, joihin luokkien nimiin viitataan, ovat kenttien tyypit (esiintymämuuttujat), menetelmien palautustyypit ja menetelmille ja konstruktoreille välitetyt parametrityypit.

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

Yllä olevassa koodissa taulukko ff on alustettu joukoksi Ala esineitä. Silmukka kerää tyypin nimen kustakin kentästä ja käsittelee sen tNimi menetelmä. tNimi method on yksinkertainen auttaja, joka palauttaa tyypin lyhenteen nimen. Niin java.lang.String tulee Merkkijono. Ja se merkitsee hashtabeliin, mitkä esineet on nähty. Tässä vaiheessa koodi on enemmän kiinnostunut luokaviitteiden keräämisestä kuin tulostamisesta.

Seuraava luokkaviitteiden lähde ovat rakentajille toimitetut parametrit. Seuraava alla oleva koodikappale käsittelee jokaisen ilmoitetun konstruktorin ja kerää viitteet parametriluetteloista.

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

Kuten näette, olen käyttänyt getParameterTypes menetelmä Rakentaja luokka syöttää minulle kaikki parametrit, jotka tietty rakentaja ottaa. Nämä käsitellään sitten tNimi menetelmä.

Mielenkiintoinen asia tässä on huomata menetelmän ero getDeclaredConstructors ja menetelmä getConstructors. Molemmat menetelmät palauttavat joukon rakentajia, mutta getConstructors method palauttaa vain ne rakentajat, jotka ovat luokkaasi käytettävissä. Tämä on hyödyllistä, jos haluat tietää, voitko todella kutsua löytämäsi rakentajan, mutta se ei ole hyödyllinen tälle sovellukselle, koska haluan tulostaa kaikki luokan rakentajat, julkiset vai ei. Kenttä- ja menetelmäheijastimilla on myös samanlaisia ​​versioita, yksi kaikille jäsenille ja toinen vain julkisille jäsenille.

Viimeinen vaihe, joka on esitetty alla, on kerätä viitteet kaikista menetelmistä. Tämän koodin on saatava viitteet sekä menetelmän tyypistä (samanlainen kuin yllä olevat kentät) että parametreista (samanlainen kuin yllä olevat rakentajat).

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

Yllä olevassa koodissa on kaksi puhelua tNimi - yksi kerää palautustyyppi ja yksi kunkin parametrin tyypin keräämiseksi.