Java (21) - datové typy

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 31666×

Jak nenarazit

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.

Primitivní typy

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

Referenční typy

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; 

Výjimka ClassCastException

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.

Mám správnou instanci?

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.

Třída Class

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.

Zjišťování informací

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:

Vytváření tříd a instancí

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í).

Přístup odepřen

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.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1025