Java je jazyk se silnou typovou kontrolou. To sice pomáhá eliminovat mnohé
problémy, přesto nás to nezbavuje nutnosti dbát určitých pravidel. Jejich
zvládnutí však poskytuje až netušené možnosti, co lze v Javě dělat.
29.11.2005 06:00 | Lukáš Jelínek | přečteno 32044×
Jsou dvě skupiny programátorů. Jedna preferuje co největší volnost v práci s datovými typy, druhá naopak co nejpřísnější pravidla. Jazyků bez typové kontroly je málo, stejně tak jazyků se zcela striktní kontrolou. Java se nachází v té přísnější oblasti, pravidla však nejsou až tak tuhá.
Jak známo, v Javě existují dva druhy datových typů: primitivní, a referenční.
Primitivních typů je jen několik, nemůžeme od nich odvozovat typy nové (něco
jako typedef
neexistuje), naopak v typech referenčních spočívá těžiště práce.
Teprve u nich se totiž uplatňuje objektové programování, které je v Javě jako
doma.
Tyto typy nesou pouze svoji hodnotu, podporují různé operátory pro operace
nad těmito typy, a jsou do jisté míry vzájemně přetypovatelné. Platí pravidlo,
že od typu s menším rozsahem nebo méně přesného k typu s větším rozsahem, resp.
přesnějšímu, můžeme provést implicitní přetypování, opačně nikoliv. Explicitně
lze přetypovávat i opačně. Pozor - typ boolean
není vzájemně přetypovatelný s žádným
datovým typem.
Oproti např. jazyku C v Javě nikdy nevíme, jaké množství paměti ten který typ zabere - to je otázka implementace a na použití typu se to neprojeví. Co naopak známe, jsou meze rozsahu dat. Proto se můžeme vždy spolehnout, že určitý typ bude mít daný rozsah, bez ohledu na implementaci. Následující příklad ukazuje, jak s primitivními typy lze a nelze pracovat:
int i = 10; // přiřazení hodnoty long l = i; // implicitní přetypování - lze byte b1 = l; // implicitní přetypování - nelze byte b2 = (byte) i; // explicitní přetypování - lze boolean f1 = i; // impl. přetypování na boolean - nelze boolean f2 = (boolean) i; // expl. přetypování na boolean - nelze double d = l; // implicitní přetypování - lze
Mezi referenční typy patří rozhraní, objektové třídy a pole.
Přistupuje se k nim zásadně pomocí referencí (které ale mají spíše charakter
ukazatelů). Při přiřazení hodnoty proměnné referenčního typu proto dojde vždy
k přiřazení reference (tzn. nová proměnná bude ukazovat na tentýž objekt jako
ta původní), nikoli hodnoty - pro předání hodnoty, tj. zkopírování objektu,
se musí použít buď kopírovací konstruktor (pokud ho třída poskytuje), anebo
klonování (pokud třída implementuje rozhraní Cloneable
a má přístupnou metodu
clone()
; tento postup se však nedoporučuje). U polí je trochu jiná situace,
už jsme se tomu věnovali v kapitole o polích.
Reference na hodnoty referenčních typů lze přetypovávat jen v případech, kdy jsou typy kompatibilní. Striktní typová kontrola vylučuje přetypování na nekompatibilní typy, bez ohledu na vnitřní datovou reprezentaci (oproti situaci v C++, kde si můžeme v tomto ohledu dělat prakticky cokoliv - samozřejmě na vlastní nebezpečí).
Znamená to, že přetypování projde jen v těchto dvou situacích (přetypování na totožný typ vynechávám):
Podívejme se tedy, co je a co není v tomto ohledu legální:
String s1 = "testovací objekt"; // lze - přetypování na nadtyp CharSequence cs = (CharSequence) s1; // lze - instance příslušného podtypu String s2 = (String) cs; // nelze - nekompatibilní typy // ohlásí kompilátor StringBuffer sb1 = (StringBuffer) s1; // nelze - instance nekompatibilního podtypu // vyhodí výjimku (viz dále) StringBuffer sb2 = (StringBuffer) cs;
Již mnohokrát v tomto seriálu jsem upozorňoval na nutnost použití správného typu dat
- s tím, že nedodržení bude "potrestáno" výjimkou ClassCastException
.
Je to asynchronní výjimka (běžně k ní nedochází), proto by se
v normálních případech ani neměla ošetřovat. Naopak, mělo by se jí předcházet,
takže její vyhození pak vždy signalizuje, že je někde něco (naprogramováno)
špatně.
Výjimky ClassCastException
se dočkáme vždy, když se pokusíme o nelegální
přetypování (takové, které nelze odhalit již při kompilaci). Musím
varovat hlavně před záludnými případy s přetypováním polí.
Více ukáže následující příklad:
ArrayList<String> al = new ArrayList<String>(); al.add("text"); String s1 = (String) al.toArray()[0]; // funguje String sa1[] = (String[]) al.toArray(); // nelze! String s2 = (String) al.toArray(new String[0])[0]; // funguje String sa2[] = al.toArray(new String[0]); // správně
První možnost (pro někoho možná překvapivě) vyvolá výjimku ClassCastException
.
V čem je problém? Obě funkce vrátí pole, jehož prvky jsou řetězce. Prvky
těchto polí můžeme tedy zcela bezpečně přetypovat na řetězce. Toto už ale
v žádném případě nelze říci o příslušných polích. Bez ohledu na to, jaké
prvky v poli jsou, se prostě nejedná kompatibilní typy (typ Object[]
není
přetypovatelný na String[]
) a proto nemůžeme tuto konverzi provést.
Kdo provádí operaci na datech, která získal odněkud "zvenku", a potřebuje
tato data přetypovat, musí si bezpodmínečně zjistit, že má správný typ.
Spoléhání na to, že "to vyjde", není dobré. A možná ještě horší je řešení
pomocí zachycování výjimky ClassCastException
. To je přímo cesta do pekel,
protože kromě velké režie na zpracování výjimky se může také stát,
že někdo později změní hierarchii tříd, a příslušné typy najednou budou
kompatibilní - výjimka se tedy nevyvolá, ale výsledkem bude nějaké nedefinované
(potenciálně chybné) chování.
Základním prostředkem pro zjištění správného typu je operátor instanceof
.
Ten použijeme v případech, kdy požadujeme instanci konkrétní třídy.
Například takto (úprava kusu příkladu z kapitoly o síťové komunikaci):
URL url = new URL(urlString); // urlString máme odjinud URLConnection con = url.openConnection(); // otevře se spojení if (con instanceof HttpURLConnection) { // test typu instance // bezpečné přetypování HttpURLConnection hcon = (HttpURLConnection) con; ... } else { con.disconnect(); System.err.println("Toto není adresa protokolu HTTP"); ... }
Tehdy jsem uvedl, že mlčky předpokládáme správný typ. Při adrese udané "natvrdo" v kódu to mohlo být použitelné (pro zkušební účely), ale obecně takové předpoklady dělat nesmíme. Zvlášť tehdy, má-li vliv na typ někdo jiný (např. uživatel, který někde zadá vstupní data). Kontrolu zkrátka nelze vynechávat.
Každý referenční typ má v Javě má svoji instanci třídy Class
. Tato instance se vytvoří
automaticky při načtení příslušného typu do JVM, a lze ji kdykoli získat
zavoláním getClass()
na instanci objektu. Stejná instance třídy Class
je k dispozici také jako proměnná třídy/rozhraní. Přiblížíme si to na příkladě
(zjednodušená verze výše uvedeného příkladu - s úpravou pro použití této cesty):
URL url = new URL(urlString); // urlString máme odjinud URLConnection con = url.openConnection(); // otevře se spojení if (HttpURLConnection.class.isInstance(con)) { // test typu instance ... } if (con.getClass() == HttpURLConnection.class) { // test typu instance ... }
Zbývá ještě vyřešit, co kdy použít. Operátor instanceof
se hodí v případech,
kdy požadovanou třídu známe již při psaní programu. Naopak metodu isInstance()
použijeme spíš tam, kde tuto třídu získáme až za běhu. Třetí způsob uvádím
proto, že to "také jde", ale moc se ve skutečnosti nepoužívá (je také poměrně
omezený, protože porovnává naprosto striktně, přímo instance třídy Class
).
Třída Class
toho ale umí mnohem víc.
Pusťme se tedy do toho - nestihneme sice všechno, ale i tak to stojí za to.
Třída Class
poskytuje základní aparát pro zjišťování informací o daném typu.
Podívejme se na některé z nabízených metod:
isInstance(Object o)
- Zjišťuje, zda je objekt předaný v argumentu
instancí dané třídy (viz výše). Je to vlastně dynamický ekvivalent operátoru
instanceof
isAssignableFrom(Class c)
- Zjišťuje, zda je objekt předávaný v argumentu
přiřaditelný tomuto typu.isArray()
- Zjišťuje, zda se jedná o pole.isInterface()
- Zjišťuje, zda se jedná o rozhraní.isPrimitive()
- Zjišťuje, zda se jedná o primitivní typ. Ono totiž úplně
neplatí, že Class
se váže jen k referenčním objektům. Zapouzdřující třídy
(např. Integer
pro int
) obsahují konstantu TYPE
, která nese právě objekt
třídy Class
pro příslušný primitivní typ.getName()
- Vrací název typu. Tento název je zvláštní tím, že v sobě obsahuje
"zakódovanou" dimenzi pole.getSimpleName()
- Vrací název typu tak, jak je definován ve zdrojovém kódu.
Metoda je k dispozici od Javy 5.0.
Nejen zjišťovat informace můžeme pomocí třídy Class
. Máme tu totiž k dispozici
nástroje mnohem silnějšího kalibru.
Za normálních okolností vytváříme objekty zavoláním jejich konstruktoru nebo
nějaké metody, která instanci vytvoří (zavoláním neveřejného konstruktoru).
Existuje ale ještě další cesta, a tou je metoda newInstance()
třídy Class
.
Ta se pokusí zavolat bezparametrický konstruktor - což samozřejmě nemusí
dopadnout dobře (konstruktor nemusí existovat apod.), a v takovém případě se vyvolá
příslušná výjimka (např. InstantiationException
). Metodu běžně nevyužijeme,
ale hodí se v kombinaci s druhou podobnou věcí - a tou je vytvoření třídy.
Za normálních okolností lze již při startu rozhodnout, které třídy se mají načíst a inicializovat. Jsou to jednoduše ty, které se někde v programu používají. Někdy bychom ale potřebovali za běhu načíst nějakou třídu, která třeba při spuštění ani neexistovala. Typicky to může být třeba nějaký plugin (zásuvný modul). Ale i to je možné.
Třída Class
má dvojici statických metod forName()
. My se nyní zaměříme jen na tu
jednodušší z nich, ale princip je stejný. Metodě předhodíme řetězec s názvem
třídy, a příslušná třída je načtena a inicializována. Pochopitelně jen tehdy,
je-li k dispozici. O načtení se postará systémový zavaděč tříd (classloader),
který soubor s třídou hledá v místech určených proměnnou CLASSPATH
. Nenajde-li
ho, způsobí to výjimku ClassNotFoundException
. Podívejme se na příklad:
try { Class c = Class.forName("MyDynamicClass"); // načtení třídy // lze přetypovat na Runnable? if (Runnable.class.isAssignableFrom(c)) { Runnable r = (Runnable) c.newInstance(); // vytvoření instance Thread t = new Thread(r); // použití t.start(); } } catch (Exception e) { System.err.println("Třída nebyla nalezena"); }
Příklad ukazuje, jak lze takto získaný objekt použít. Načteme třídu,
zjistíme, zda implementuje rozhraní Runnable
, a pokud ano, vytvoříme nové
vlákno, které začne vykonávat metodu run()
instance třídy MyDynamicClass
.
Podobnými mechanismy lze dělat ještě větší "psí kusy" (třeba načíst třídu
odněkud ze sítě nebo z databáze a provést s ní totéž). Podobně lze také používat metody
takových objektů, aniž bychom je předem znali - k tomu se používá postup
zvaný reflexe, a o něm bude řeč někdy později (pro běžné použití se z řady
důvodů nehodí).
Načítání tříd odněkud zvenku (ať už při startu nebo za běhu) skýtá poměrně velké nebezpečí. Do třídy totiž někdo může podstrčit nějaký nebezpečný kód, který snadno udělá v běžícím programu paseku - samozřejmě ale jen tehdy, pokud mu to dovolíme. Jak mu to nedovolit, tedy jak omezit oprávnění pro provádění různých potenciálně nebezpečných operací, si vyzkoušíme příště. Jen naťuknu, že takové mechanismy se používají např. ve webových prohlížečích, aby applety ze sítě nemohly sahat na lokální disk nebo dělat jiné zapovězené věci.