Bajtkód
Program v programovacom jazyku môže byť buď priamo vykonávaný interpretátorom, alebo je prekladaný do inej vykonateľnej podoby. V prípade jazykov ako C, C++, Go, Rust, alebo Haskell, je to natívny strojový kód, ktorý dokáže priamo vykonávať procesor počítača. Kód teda obsahuje postupnosť inštrukcie daného typu procesora. Vykonateľný súbor však okrem samotných inštrukcií obsahuje dáta, a tiež metadáta, ktoré operačný systém používa pri načítaní a spustení programu.
V prípade jazyka Java nejde priamo o inštrukcie procesora, ale o inštrukcie virtuálneho počítača – Java Virtual Machine (JVM). Tie sú definované v špecifikácii JVM.
Okrem formátu a významu inštrukcií, špecifikácia určuje aj formát súboru, v ktorom sú ukladané všetky informácie skompilovanej triede – class-súbore.
Načítavanie tried
Vráťme sa k téme reflexie. Doteraz sme sa venovali využitiu reflexie na analýzu štruktúry tried a na manipuláciu s objektmi — vytváranie inštancií, volanie metód a práca s členskými premennými. Teraz sa pozrieme na možnosti manipulácie so samotnými triedami.
V prvom rade vieme dynamicky načítať triedu pomocou statickej metódy
Class<?> Class.forName(String className)
Výsledkom volania tejto metódy je objekt triedy Class reprezentujúci triedu so zadaným názvom (plne kvalifikovaným). Rozdiel oproti zápisu NázovTriedy.class
je ten, že názov triedy nemusí byť importovaný, ale zadáva sa ako reťazec. To znamená, že tento názov nemusí byť známy v čase prekladu, a dokonca táto trieda ani nemusí v tom čase existovať.
Dynamické načítanie triedy
Napríklad si predstavte, že v aplikácii potrebujete používať rôzne ovládače databázy v závislosti od situácie: pripojenie k skutočnej databáze v produkčnom nasadení a simuláciu databázy pri vývoji a testovaní. Jednou z možnosti ako túto vlastnosť realizovať je čítať názov triedy ovládača z konfigurácie a dynamicky počas behu načítať príslušnú triedu a vytvoriť jej inštanciu:
String dbClassName = props.getProperty("dbClass", "sk.tuke.StubDB");
Class dbClass = Class.forName(dbClassName);
customerDB = (CustomerDatabase) dbClass.newInstance();
Toto riešenie je veľmi flexibilné, keďže umožňuje vytvárať nové triedy ovládačov bez toho aby sme museli modifikovať aplikáciu.
Dynamické načítavanie tried môže byť veľmi užitočné pre vytváranie rozšíriteľných aplikácií. Aby sme ho využili musíme dodržať niekoľko princípov:
- definovať rozhranie (interface) pre triedy,
- zaviesť dohodu o spôsobe inicializácie objektov načítaných tried.
Dohoda o inicializácií nemôže byť vynútená rozhraním, keďže rozhrania neumožňujú špecifikovať parametre konštruktora. Preto sa najčastejšie na vytváranie objektov takýchto tried používa bezparametrický konštruktor a samostatná metóda pre inicializáciu, ktorá je súčasťou rozhrania. Alternatívou je dohoda o parametroch konštruktora.
ClassLoader
Za načítavanie tried v Jave zodpovedá špeciálna trieda — ClassLoader. Každá trieda existujúca v systéme bola načítaná niektorou z inštancií tried dediacich od ClassLoader. Pritom každá trieda je jednoznačne identifikovaná svojim plným názvom a ClassLoaderom, to znamená že môžeme mať vo virtuálnom stroji načítané dve rozdielne triedy s rovnakým názvom, pokiaľ na ich načitanie použijeme rôzne loadery.
Pri volaní metódy Class.forName môžeme explicitne špecifikovať konkrétnu inštanciu ClassLoadera, ktorá sa má použíť:
ClassLoader loader = this.getClass().getClassLoader();
Class cls = Class.forName(name, true, loader);
Môžeme tiež priamo použiť ClassLoader pre načítanie triedy:
ClassLoader loader = this.getClass().getClassLoader();
Class cls = loader.loadClass(name);
Na rozdiel od predchádzajúceho kódu, v tomto prípade sa trieda načíta, ale neinicializuje. Inicializácia sa uskutoční až v momente, keď sa trieda prvý krát použije. Podrobnejšie informácie o tejto téme prečítate v článku Get a load of that name.
Inštanciu ClassLoadera môžeme získať viacerými spôsobmi, napríklad
this.getClass().getClassLoader()
— loader použitý pri načitávaní aktuálnej triedy,ClassLoader.getSystemClassLoader()
— systémový loader,Thread.currentThread().getContextClassLoader()
— loader pre aktuálne vlákno,new URLClassLoader(urls);
— loader schopný načítavať triedy zo zadanej URL (zo siete alebo lokálne).
Je možné implementovať aj vlastný ClassLoader, ktorý bude dediť od základnej abstraktnej triedy a preťaží niektoré jej metódy. Vďaka tomu je možné implementovať načítavanie tried z iných zdrojov ako zo súborov v CLASSPATH
a tiež dynamické modifikovanie bajtkódu načítavaných tried.
Dynamické vytváranie tried
Možnosť načítať triedu počas behu aplikácie otvára cestu k dynamickému vytváraniu nových tried priamo počas behu. Jednou z možností je vygenerovanie zdrojového kódu triedy, jej preloženie a načítanie. Nevýhodou tohto jednoduchého prístupu je nutnosť mať k dispozícii prekladač Javy aj počas behu programu. Inou cestou je generovanie priamo bajtkódu alebo jeho modifikácia. Na to existuje viacero knižníc:
Literatúra
- Bill Venners: The Java class file lifestyle
- Bill Venners: Bytecode basics
- Chuck Mcmanis: The basics of Java class loaders