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
@Testannotatsiooniga, 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:
- Class-objekt - reflekstiooni alguspunkt
- Väljad - objekti oleku lugemine ja kirjutamine ilma selle tüüpi teadmata
- Instantsid ja meetodid - objektide loomine ja meetodite dünaamiline väljakutsumine
- Sisseehitatud annotatsioonid -
@Override,@Deprecatedja teised annotatsioonid, mida Java ise defineerib - Ise annotatsioonide loomine - oma annotatsioonide defineerimine ja neile parameetrite andmine
- Annotatsioonide lugemine - annotatsioonidega märgistatud elementide avastamine käitamise ajal ja loogika lisamine
- Praktiline näide - väljade, annotatsioonide ja reflektsiooni ühendamine tervikliku näite kaudu.
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.