Liigu peamise sisu juurde

Reflektsioon

Sissejuhatus

Siiamaani oleme koodi kirjutanud etteaimatavalt. See tähendab, et igal klassil, meetodil ja väljal on ette määratud nimi, mis on kompileerimise ajal teada. Näiteks kui BookService klass kutsub välja repository.findAll() meetodit, siis kompilaator saab kontrollida, et BookRepository sisaldab meetodit nimega findAll, ning et see tagastaks õiget tüüpi väärtust, mida väljakutsuja saaks kasutada. Kogu programm on nii-öelda suletud maailm - kompilaator näeb seda tervikuna.

Samas on olukordi, kus selline lähenemine on liiga piirav. Varasemalt oleme kokku puutunud näiteks JSON-it käsitlevate teekidega või testimisraamistikega (JUnit). Kõik sellised teegid käsitlevad olukordi ning töötavad koos koodiga, millest need ei ole teadlikud (ehk mida raamistiku autor pole ise kirjutanud). Samas kuidagi oskab testimisraamistik ette antud koodist üles otsida testimeetodid ning need käima panna.

Reflektsioon on Java standardteegi osa, mis teeb selle võimalikuks. See võimaldab töötaval programmil esitada küsimusi omaenda struktuuri kohta, näiteks:

  • Millised klassid eksisteerivad?
  • Millised väljad ja meetodid sellel klassil olemas on?
  • Millised annotatsioonid nendel küljes on?

Neist tulenevaid vastuseid kasutatakse järgnevate tegevuste jaoks, olgu selleks väljadelt andmete lugemine ja kirjutamine, objektide loomine, meetodite välja kutsumine jne. Kõike seda tehakse ainult ette antud nime järgi.

Motiveeriv näide

Oletame, et on vaja kirjutada funktsioon, mis prindib välja ette antud objekti hetkeoleku. Ilma objekti täpset tüüpi teadmata, on see tavapärase Javaga suhteliselt võimatu:

public static void dump(Object obj) {
// What do we even call on obj? We do not know its fields.
}

Reflektsiooni abil töötaks sama funktsioon iga klassi puhul ilma seda muutmata:

public static void dump(Object obj) throws IllegalAccessException {
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
System.out.println(field.getName() + " = " + field.get(obj));
}
}
dump(new Book("Clean Code", "Robert C. Martin", 2008));
// title = Clean Code
// author = Robert C. Martin
// year = 2008

See funktsioon ei maini kusagil Book klassi. See küsib objektilt, mis tüüpi klass see on, ning uurib kõiki välju, mida antud klass deklareerib. Kui ette anda mõni muu objekt, töötab see samamoodi.

Reflektsioon praktikas

Reflektsioon on alus paljudele laialt kasutusel olevatele teekidele:

  • Testimisraamistikud (JUnit, TestNG) leiavad klassidest meetodeid, mis on märgistatud @Test annotatsiooniga, loovad automaatselt nende klasside instantsid ja kutsuvaid neid meetodeid välja, ilma nendest klassidest varasemalt teadlik olles.
  • Serialiseerimisteegid (Jackson, Gson) loevad suvaliste objektide välju, et toota JSON-it, ning kirjutavad andmeid suvaliste klasside väljadesse, et JSON tagasi objektiks teisendada.
  • DI-raamistikud (Spring) loevad konfiguratsiooni, otsivad klasse nime järgi ning loovad instantse tüüpidest, mida ei ole kompileerimise ajal teada.
  • ORM-raamistikud (Hibernate) seovad andmebaasiridu objektiväljadega annotatsioonide põhjal, mis on väljadele lisatud.
  • Ehitusinstrumendid ja IDE-d skaneerivad kompileeritud klasse, et leida pluginaid, laienduspunkte ja annotatsioonidega märgistatud sisenemispunkte.

Kõigil neil juhtudel ei teadnud raamistike autorid, milliseid klasse nende kasutajad tulevikus kirjutavad. Reflektsioon võimaldab raamistikul nende klassidega siiski töötada.

Reflektsiooni hind

Reflektsioon võid esmapilgul tunduda võimsana, kuid sellega kaasnevad kompromissid, mida tavapärasel koodil ei ole:

Esmalt, reflektsiooni kasutades kaob igasugune kompilaatori poolne tugi. Kirjaviga "findAll" sõnes, mis antakse meetodile getMethod("findAll") ei avastata kompileerimise ajal. Viga ilmneb alles käitamise ajal, sageli segadust tekitava NoSuchMethodException erindina. Samuti kaob ära IDE poolne tugi (näiteks väljade ümbernimetamisel).

Reflektsiooni kaudu tehtud väljakutsed on aeglasemad kui otsesed meetodikutsed, sest JVM peab iga kord otsima meetodeid ja välju nime järgi ega saa rakendada optimiseerimist. Enamasti on see erinevus tühine, kuid tsüklites või koodis, kus jõudlus on tähtis, muutub see oluliseks.

Viimane kompromiss, mida reflektsioon teeb, on kapseldamisest mööda minek. private välju ja meetodeid saab reflektsiooni abil lugeda ja muuta. See on mõnikord vajalik - eriti testimise ja serialiseerimise puhul -, kuid nõrgestab garantiisid, mida keel muidu jõustab.

Neil põhjustel kasutatakse reflektsiooni tavapärases koodis viimase võimalusena. See on kasulik, kui kirjutatakse raamistikulaadset infrastruktuuri või käsitletakse tundmatuid sisendeid. Muul juhul on tavalised meetodikutsed lihtsamad, turvalisemad ja kiiremad.

Järgnevad peatükid

Järgnevad peatükid ehitavad reflektsiooni tööriistakomplekti samm-sammult üles:

Peatüki lõpuks oled kokku puutunud piisavalt tööriistadega, et aru saada, kuidas JUnit, Jackson ja Spring laadsed raamistikud toimivad ja kasutavad reflektsiooni sisemiselt. Samuti oskad ka ise ehitada väikseid raamistikulaadseid tööriistu.