Obsah
1. Novinky v JDK 7 aneb mírný pokrok v mezích zákona (2)
2. JDK 5 a generické datové typy
3. Generiky ve vygenerovaném bajtkódu
4. Nová syntaxe v JDK 7 – operátor <>
5. Je operátor <> skutečně nezbytný a/nebo dostatečně obecný?
6. Rozhraní Closeable a možné problémy při jeho použití
7. Automatická správa prostředků: rozhraní AutoCloseable a rozšíření možností bloku try
8. Vliv existence automatické správy prostředků na stávající aplikace
1. Novinky v JDK 7 aneb mírný pokrok v mezích zákona (2)
V úvodní části miniseriálu o nových vlastnostech, které byly zařazeny do JDK 7, jsme se alespoň stručně seznámili s tím, jaká vylepšení byla do JDK 7 plánována v minulosti (ještě firmou Sun Microsystems) a která z těchto vylepšení skutečně můžeme v JDK 7 najít. Některé původně navrhované změny byly v rámci plánu „B“ přesunuty do JDK 8 nebo JDK 9, ovšem i tak nabízí JDK 7 popř. OpenJDK 7 (založené na prakticky shodných zdrojových kódech a knihovnách) několik novinek v syntaxi i sémantice jazyka Java, s nimiž se průběžně seznámíme. Dvě implementačně nejjednodušší vlastnosti „nové“ Javy jsme si již popsali – jednalo se o možnost vkládaní podtržítek do numerických konstant (což je opravdu pouze syntaktický cukr) a taktéž o možnost používat řetězce v rozhodovací struktuře typu switch-case (tato vlastnost je již zajímavější, a to i z hlediska principu překladu zdrojových textů do bajtkódu).
Obě zmiňované novinky v syntaxi a sémantice Javy byly zavedeny v rámci projektu Coin, v němž však můžeme nalézt i mnohá další vylepšení. Jedno z těchto vylepšení se týká zjednodušení práce s generickými datovými typy – pro tyto účely byl nově vytvořen operátor nazvaný podle svého (vizuálního) tvaru diamant. Nemusíte se však bát – tvůrci Javy jsou dostatečně soudní, takže do jazyka NEzavedli nějaký cizokrajný znak z Unicode; vše se alespoň prozatím zapisuje ve staré dobré ASCII. Nejdříve se tedy budeme věnovat generickým datovým typům z JDK 5 a jejich souvislosti s novým operátorem diamant.
2. JDK 5 a generické datové typy
Jednou z novinek zavedených již v JDK verze 5 (J2SE 5.0) byla podpora generických datových typů (generik). Dokonce je možné říci, že se – společně s anotacemi – s velkou pravděpodobností jednalo o největší změny, kterými prozatím programovací jazyk Java při svém více než patnáctiletém vývoji prošel. Díky použití generických datových typů se zjednodušila jak práce programátorů, tak se současně i zvýšila bezpečnost jimi vytvářených aplikací, například při práci s kolekcemi, tj. s množinami, seznamy a asociativními poli (mapami).
Kolekce totiž byly ještě v JDK 1.4.x implementovány takovým způsobem, že jako své prvky mohly obsahovat pouze obecný typ Object (stojící, jak je známo, na vrcholu hierarchie tříd) a při každém čtení prvku uloženého v kolekci se tedy muselo přímo v programu provádět explicitní přetypování objektů získávaných z kolekcí. Toto přetypování bylo jak nepřehledné, tak i potenciálně nebezpečné (popř. samozřejmě bylo možné pomocí operátoru instanceof otestovat, zda objekt implementuje nějaké rozhraní či zda je instancí dané třídy nebo jejího potomka). Typově zabezpečené kolekce, které nebylo možné deklarovat přímo s využitím výrazových prostředků programovacího jazyka, se tedy musely tvořit ručně, což bylo pracné a v mnoha ohledech také nepřehledné, zejména v těch případech, když si vývojář vytvořil vlastní API s jinými názvy či významy metod, než byly použité v JCF – Java Collections Framework.
Pro připomenutí si ukažme, jak mohl vypadat (umělý) příklad přeložitelný v JDK 1.4.x (v novějších JDK použijte pro příklad přepínač -source 1.4):
import java.util.List; import java.util.ArrayList; public class TT { public static void main(String[] args) { List l = new ArrayList(); l.add("String"); l.add(Integer.valueOf(1)); l.add(new Object()); for (int i = 0; i < l.size(); i++) { Object o = l.get(i); System.out.println(o.getClass().getName() + "\t" + o.toString()); if (o instanceof Integer) { Integer integer = (Integer)o; System.out.println("Nasli jsme cislo :-) " + 100*integer.intValue()); } } } }
Výstup zobrazený po spuštění příkladu:
java.lang.String String java.lang.Integer 1 Nasli jsme cislo :-) 100 java.lang.Object java.lang.Object@42e816
3. Generiky ve vygenerovaném bajtkódu
Zajímavé a pro celou platformu Javy vlastně i typické je to, že se po přidání podpory pro generické datové typy v JDK 5 tato nová vlastnost nijak zásadním způsobem neprojevila na vygenerovaném bajtkódu. Překladač totiž – poněkud zjednodušeně řečeno – vygeneroval bajtkód obsahující konstrukci beztypové kolekce a při čtení objektů z kolekce automaticky doplnil instrukce pro kontrolu přetypování následovanou vlastním přetypováním (tj. v podstatě řešil stejný problém, jako my v předchozím příkladu). Strukturu bajtkódu si ostatně můžeme ukázat na jednoduchém příkladu. Po překladu následujícího zdrojového kódu používajícího kolekci (konkrétně seznam implementovaný polem) s generikami…:
import java.util.List; import java.util.ArrayList; public class T { public static void main(String[] args) { List<String> l = new ArrayList<String>(); l.add("www.root.cz"); String s = l.get(0); System.out.println(s); } }
…se vytvoří bajtkód, v němž se volá konstruktor obecného ArrayListu s metodami boolean add(Object o) a Object get(int index); viz též http://download.oracle.com/javase/1.4.2/docs/api/java/util/ArrayList.html:
Compiled from "T.java" public class T extends java.lang.Object{ public T(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2; //class java/util/ArrayList 3: dup 4: invokespecial #3; //Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #4; //String Hello // ----------------------------------------------------------- // volání metody boolean List.add(Object o) // ----------------------------------------------------------- 11: invokeinterface #5, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 16: pop 17: aload_1 18: iconst_0 // ----------------------------------------------------------- // volání metody Object List.get(int index) // ----------------------------------------------------------- 19: invokeinterface #6, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; // ----------------------------------------------------------- // kontrola typu získaného objektu za běhu programu (runtime) // - většinou ji HotSpot v dalším běhu eliminuje // Důležité je, že se tato kontrola skutečně provádí, // ale pouze do chvíle, kdy je již jasné, že nedojde // k předání objektu jiného typu. // ----------------------------------------------------------- 24: checkcast #7; //class java/lang/String 27: astore_2 28: getstatic #8; //Field java/lang/System.out:Ljava/io/PrintStream; 31: aload_2 32: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 35: return }
Poznámky ve výše uvedeném bajtkódu jsou samozřejmě dopsány ručně, program javap prozatím tak sofistikovaný není :-)
4. Nová syntaxe v JDK 7 – operátor <>
V demonstračním příkladu uvedeném v předchozí kapitole byl seznam, jenž může obsahovat pouze instance třídy String, vytvořen pomocí tohoto příkazu:
List<String> list = new ArrayList<String>();
Podobnou konstrukci používají vývojáři při práci v Javě denně a pro její jednodušší tvorbu existuje i podpora v mnoha vývojových prostředích – v nich postačuje zapsat pouze část výrazu a například typ prvků ukládaných do kolekce se již na pravé straně doplní automaticky, takže by se mohlo zdát, že zde k žádnému problému nedochází. V některých případech je však deklarace datového typu prvků na levé i pravé straně přiřazovacího příkazu složitější, což vede ke zbytečnému duplikování stejných částí zdrojového textu. Podívejme se například na následující (samozřejmě uměle vytvořené!) příklady:
List<String> list = new ArrayList<String>(); List<List<String>> list = new ArrayList<List<String>>(); List<List<List<String>>> list = new ArrayList<List<List<String>>>(); Map<String, Collection<Integer>> map = new LinkedHashMap<String, Collection<Integer>>();
Aby se na pravou stranu přiřazovacího příkazu nemusel kopírovat ten samý zdrojový text, který je na jeho levé straně, byl do JDK 7 přidán nový operátor nazvaný diamant, protože se skutečně diamantu podobá – zapisuje se totiž znakem „menší než“, za nímž je ihned zapsán znak „větší než“. Pokud překladač tento operátor ve zdrojovém textu uvidí, pokusí se odvodit správný datový typ z deklarace na levé straně výrazu. Uvažuje se ještě o složitějším algoritmu odvození typu podle parametrů předaných konstruktoru, ovšem současné demoverze JDK 7 a OpenJDK 7 v tomto ohledu nepracují zcela korektně (operátor diamant taktéž pracuje pouze při konstrukci objektu pomocí new, nikoli například při přetypování). Příklady uvedené před tímto odstavcem je tedy možné s využitím operátoru diamant přepsat následujícím způsobem:
List<String> list = new ArrayList<>(); List<List<String>> list = new ArrayList<>(); List<List<List<String>>> list = new ArrayList<>(); Map<String, Collection<Integer>> map = new LinkedHashMap<>();
Ukažme si celý demonstrační příklad:
import java.util.List; import java.util.ArrayList; public class DiamondTest1 { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("Hello"); list.add(null); list.add("World!"); for (String str : list) { System.out.println(str); } } }
Popř. poněkud složitější příklad, v němž je diamant použit na více místech označených poznámkami:
import java.util.*; public class DiamondTest3 { public static void main(String[] args) { // vvvv Set<Integer> set1 = new TreeSet<>(); // ^^^^ set1.add(1); set1.add(2); set1.add(3); // vvvv Set<Integer> set2 = new TreeSet<>(); // ^^^^ set2.add(100); set2.add(99); set2.add(88); // vvvv List<Set<Integer>> list = new ArrayList<>(); // ^^^^ list.add(set1); list.add(set2); // zde _nelze_ použít operátor diamant, tj. nejde napsat // například Set<> numbers : list for (Set<Integer> numbers : list) { for (Integer number : numbers) { System.out.print(number + "\t"); } System.out.println(); } } }
5. Je operátor <> skutečně nezbytný a/nebo dostatečně obecný?
Návrh na přidání operátoru diamant, který do Javy (dokonce již podruhé) přinesl algoritmy pro automatické odvozování typů objektů, se setkal s poměrně velkým ohlasem, a to jak ze strany vývojářů, kteří by tento prvek nejraději úplně zrušili, tak na druhé straně od vývojářů (majících evidentně zkušenosti z jiných programovacích jazyků), kteří naopak tvrdili, že je tento operátor někde na půli cesty k plné typové inferenci. Po prostudování demonstračních příkladů uvedených v předchozí kapitole se možná také ptáte, proč je vlastně nutné vůbec nový operátor do programovacího jazyka Java zavádět, když by postačovalo napsat něco podobného:
List<String> list = new ArrayList(); List<List<String>> list = new ArrayList(); List<List<List<String>>> list = new ArrayList(); Map<String, Collection<Integer>> map = new LinkedHashMap();
Podle autorů projektu Coin by však povolení tohoto ještě jednoduššího zápisu bylo v rozporu se zpětnou kompatibilitou s předchozími verzemi Javy, kde zápis new ArrayList() znamená vytvoření „beztypového“ seznamu s prvky typu Object. Zde se ukazuje, jak se závislost na původní syntaxi Javy negativně projevuje i v jejích moderních verzích (popravdě řečeno je však těžké přijít na to, kde by změna nikoli syntaxe ale pouze sémantiky v tomto případě mohla vadit).
Další často kladenou otázkou v souvislosti s novým operátorem diamant je, proč vlastně jeho tvůrci nešli v rozšíření syntaxe a sémantiky ještě dále a proč Java nepodporuje například následující způsoby vytváření objektů, v nichž je celá deklarace typu uvedena pouze na pravé straně přiřazovacího příkazu:
var list = new ArrayList<String>();
nebo s jiným klíčovým slovem:
auto map = new LinkedHashMap<String, Collection<Integer>>();
Jedním z důvodů, proč (prozatím) tento způsob zápisu není v Javě možný, spočívá v tom, že by se jednalo o poměrně citelný zásah do jazyka, který by neodpovídal filozofii projektu Coin a vlastně ani mottu tohoto článku: „mírný pokrok v mezích zákona“ :-) Operátor <> totiž pomáhá programátorům pouze v tom ohledu, že nemusí opisovat kód uvedený na levé straně přiřazovacího příkazu při vytváření objektů (a nikde jinde!), ale nesnaží se již například odvodit obecný typ vytvořeného objektu, tj. zda se jedná o obecný objekt implementující rozhraní Collection, obecný seznam List či o zcela konkrétní ArrayList. Nicméně v ostatních srovnatelných programovacích jazycích již podobné konstrukce existují a fungují ke spokojenosti programátorů, takže je možná pouze otázkou času, kdy se nová sémantika i syntaxe objeví v některé z dalších verzí JDK (ostatně i dnes existují projekty, které podobnou funkcionalitu do Javy přidávají, někdy příště se k těmto projektům ještě vrátíme).
6. Rozhraní Closeable a možné problémy při jeho použití
Další vylepšení, které se objevilo v JDK 7, se týká práce s objekty implementujícími nové rozhraní AutoCloseable, tj. metodu void close(). Aby se s těmito objekty pracovalo jednodušším způsobem, byla rozšířena syntaxe a sémantika bloku try. Nová funkcionalita se nazývá Automatic Resource Management neboli ARM. Připomeňme si jen, že tvůrci návrhů implementovaných v JDK 7 zavedli všechny změny v syntaxi bez přidání nových klíčových slov a tyto změny navíc nevedou k tomu, aby bylo nutné přepisovat stávající aplikace. To je pro mainstreamový jazyk velmi důležitá podmínka, která sice umožňuje zachovat téměř 100% zpětnou kompatibilitu (většinou až k JDK 1.0), na druhou stranu to však poněkud omezuje možné inovace jazyka. Změny zavedené především v JDK 5 a JDK 7 jsou ostatně pěknou ukázkou „balancování na hraně“ mezi novými vlastnostmi a vazbou na minulost.
Nejprve si připomeňme, že již v JDK 5 (1.5) bylo do API přidáno rozhraní Closeable s předpisem metody close() a že toto rozhraní implementuje poměrně velké množství tříd ze standardního API (typicky třídy pro práci se soubory či jinými prostředky). Toto rozhraní samozřejmě mohou implementovat i uživatelské objekty. Teoreticky je práce s tímto rozhraním velmi jednoduchá, zejména v případě, že při práci programu nedojde ke vzniku špatně ošetřených výjimek. Následuje jednoduchý příklad:
class T implements java.io.Closeable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { System.out.println(name + " -> close()"); } } public class CloseTest1 { public static void main(String[] args) { T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3"); try { System.out.println("zacatek bloku try"); t1.doSomething(); t2.doSomething(); t3.doSomething(); t1.close(); t2.close(); t3.close(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); } } }
Vzhledem k tomu, že při běhu velmi pravděpodobně nenastane žádná výjimka, pracuje program tak, jak bylo zamýšleno, což mj. znamená, že se korektně zavolají všechny metody close():
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try T1 -> doSomething() T2 -> doSomething() T3 -> doSomething() T1 -> close() T2 -> close() T3 -> close() konec bloku try blok finally
Horší je ovšem situace ve chvíli, kdy k výjimce dojde a kdy například metoda close() skutečně zavírá nějaký prostředek, třeba připojení do databáze (které bývá pro jednu aplikaci nebo pro jednoho klienta omezené):
class T implements java.io.Closeable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { System.out.println(name + " -> close()"); } } public class CloseTest2 { public static void main(String[] args) { T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3"); try { System.out.println("zacatek bloku try"); int i = Integer.parseInt(args[0]); t1.doSomething(); t2.doSomething(); t3.doSomething(); t1.close(); t2.close(); t3.close(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); } } }
Pokud není na jako první parametr programu zadáno korektně zapsané celé číslo, dojde k výjimce a metody close() se nezavolají – prostředek zůstal aplikaci již natrvalo přidělen:
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try java.lang.ArrayIndexOutOfBoundsException: 0 at CloseTest2.main(CloseTest2.java:32) blok finally
Mohlo by se zdát, že postačuje přesunout volání metod close() do bloku finally:
class T implements java.io.Closeable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { System.out.println(name + " -> close()"); } } public class CloseTest3 { public static void main(String[] args) { T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3"); try { System.out.println("zacatek bloku try"); int i = Integer.parseInt(args[0]); t1.doSomething(); t2.doSomething(); t3.doSomething(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); t1.close(); t2.close(); t3.close(); } } }
Což skutečně bude pracovat (alespoň zdánlivě) korektně:
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try java.lang.ArrayIndexOutOfBoundsException: 0 at CloseTest2.main(CloseTest2.java:32) blok finally T1 -> close() T2 -> close() T3 -> close()
Jenže tak jednoduché to není v případě, že metoda close() vyvolává výjimky, což samozřejmě dle její deklarace může. Zde se pokusíme výjimku nasimulovat pro objekt se jménem T2:
class T implements java.io.Closeable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { if ("T2".equals(this.name)) { throw new RuntimeException("T2 object throws exception!"); } System.out.println(name + " -> close()"); } } public class CloseTest4 { public static void main(String[] args) { T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3"); try { System.out.println("zacatek bloku try"); int i = Integer.parseInt(args[0]); t1.doSomething(); t2.doSomething(); t3.doSomething(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); t1.close(); t2.close(); t3.close(); } } }
Zase špatně :-( protože se nezavolají všechny metody close(), což by v praxi znamenalo, že aplikace po sobě neuklidí všechny prostředky:
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try java.lang.ArrayIndexOutOfBoundsException: 0 at CloseTest4.main(CloseTest4.java:36) blok finally T1 -> close() Exception in thread "main" java.lang.RuntimeException: T2 object throws exception! at T.close(CloseTest5.java:20) at CloseTest4.main(CloseTest4.java:50)
V JDK 6 nám tedy nezbude nic jiného, než vytvořit „špagetový“ kód s explicitním uzavíráním metod close() do bloků try-catch-finally:
class T implements java.io.Closeable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { if ("T2".equals(this.name)) { throw new RuntimeException("T2 object throws exception!"); } System.out.println(name + " -> close()"); } } public class CloseTest5 { public static void main(String[] args) { T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3"); try { System.out.println("zacatek bloku try"); int i = Integer.parseInt(args[0]); t1.doSomething(); t2.doSomething(); t3.doSomething(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); try { t1.close(); } catch (Exception e) { e.printStackTrace(); } try { t2.close(); } catch (Exception e) { e.printStackTrace(); } try { t3.close(); } catch (Exception e) { e.printStackTrace(); } } } }
Nyní se konečně všechny metody close() zavolají, a to i v případě, že některá z nich vyvolala výjimku:
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try java.lang.ArrayIndexOutOfBoundsException: 0 at CloseTest5.main(CloseTest5.java:36) blok finally T1 -> close() java.lang.RuntimeException: T2 object throws exception! at T.close(CloseTest5.java:20) at CloseTest5.main(CloseTest5.java:59) T3 -> close()
7. Automatická správa prostředků: rozhraní AutoCloseable a rozšíření možností bloku try
Ve všech předchozích příkladech jsme si situaci ještě zjednodušili v tom, že objekty t1 až t3 jsou vždy vytvořeny, tj. lze pro ně zavolat metodu close(). V praxi však může výjimka nastat již při konstrukci objektů, takže by příslušné reference na ně měly hodnotu null, která by se musela explicitně testovat. Nicméně i při použití zjednodušujících předpokladů je poslední kód velmi dlouhý a jeho vlastní výkonná část vlastně směšně krátká – jedná se o trojici řádků t?.doSomething();, okolo nichž se nachází obrovité programové konstrukce řešící pouze mezní stavy programu. Vývojáři, kteří nejsou placeni za počet vytvořených programových řádků :-), tedy hledali a hledají cesty, jak se tomuto kódu vyhnout. Kromě projektů třetích stran (opět se k nim ještě vrátíme) bylo jedno řešení implementováno i v JDK 7.
Jedná se o přidání nového rozhraní java.lang.AutoCloseable do API a taktéž o přidání „deklarační“ části do bloku try. Objekty, které jsou v této deklarační části vytvořeny a současně implementují rozhraní AutoCloseable, jsou automaticky při opuštění bloku „uzavřeny“, přesněji řečeno se pro ně zavolá metoda close(), a to nezávisle na tom, kdy a jaké výjimky při běhu nastanou. Metody close() se volají v opačném pořadí, než v jakém jsou objekty v bloku try deklarovány, což je praktické – například lze otevřít připojení do databáze (Connection), vytvořit v něm příkaz (Statement) a získat data z databáze (ResultSet). Vcelku oprávněně tedy očekáváme, že se nejdříve uzavře ResultSet, poté Statement a teprve pak Connection. Následuje ukázka možné úpravy předchozích příkladů tak, aby se využilo ARM:
// zde se implementuje nové rozhraní class T implements java.lang.AutoCloseable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { System.out.println(name + " -> close()"); } } public class CloseTest6 { public static void main(String[] args) { // nová deklarační část v kulatých závorkách // (za poslední deklarací NENÍ středník!) try ( T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3") ) { System.out.println("zacatek bloku try"); int i = Integer.parseInt(args[0]); t1.doSomething(); t2.doSomething(); t3.doSomething(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); } } }
A výstup:
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try T3 -> close() T2 -> close() T1 -> close() java.lang.ArrayIndexOutOfBoundsException: 0 at CloseTest6.main(CloseTest6.java:33) blok finally
Ještě zkusíme přidat umělé vyhození výjimky při volání metody close():
class T implements java.lang.AutoCloseable { private String name; public T(String name) { this.name = name; System.out.println(name + " -> constructor()"); } public void doSomething() { System.out.println(name + " -> doSomething()"); } public void close() { if ("T2".equals(this.name)) { throw new RuntimeException("T2 object throws exception!"); } System.out.println(name + " -> close()"); } } public class CloseTest7 { public static void main(String[] args) { try ( T t1 = new T("T1"); T t2 = new T("T2"); T t3 = new T("T3") ) { System.out.println("zacatek bloku try"); int i = Integer.parseInt(args[0]); t1.doSomething(); t2.doSomething(); t3.doSomething(); System.out.println("konec bloku try"); } catch (RuntimeException e) { e.printStackTrace(); } finally { System.out.println("blok finally"); } } }
Což po spuštění vede k následujícímu výstupu:
T1 -> constructor() T2 -> constructor() T3 -> constructor() zacatek bloku try T3 -> close() T1 -> close() java.lang.ArrayIndexOutOfBoundsException: 0 at CloseTest7.main(CloseTest7.java:37) Suppressed: java.lang.RuntimeException: T2 object throws exception! at T.close(CloseTest7.java:20) at CloseTest7.main(CloseTest7.java:42) blok finally
Metody close() se tedy zavolají a teprve poté je vyvolána původní výjimka (a navíc též výjimka vyhozená jednou z metod close). Toto chování nás však vyjde poměrně „draho“, alespoň co se týká objemu vygenerovaného bajtkódu, kde jsou explicitně zpracovány všechny možné eventuality. Postačuje se podívat, kolikrát a za jakých podmínek jsou volány jednotlivé metody close():
Compiled from "CloseTest6.java" public class CloseTest6 extends java.lang.Object { public CloseTest6(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #2 // class T 3: dup 4: ldc #3 // String T1 6: invokespecial #4 // Method T."<init>":(Ljava/lang/String;)V 9: astore_1 10: aconst_null 11: astore_2 12: new #2 // class T 15: dup 16: ldc #5 // String T2 18: invokespecial #4 // Method T."<init>":(Ljava/lang/String;)V 21: astore_3 22: aconst_null 23: astore 4 25: new #2 // class T 28: dup 29: ldc #6 // String T3 31: invokespecial #4 // Method T."<init>":(Ljava/lang/String;)V 34: astore 5 36: aconst_null 37: astore 6 39: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 42: ldc #8 // String zacatek bloku try 44: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 47: aload_0 48: iconst_0 49: aaload 50: invokestatic #10 // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I 53: istore 7 55: aload_1 56: invokevirtual #11 // Method T.doSomething:()V 59: aload_3 60: invokevirtual #11 // Method T.doSomething:()V 63: aload 5 65: invokevirtual #11 // Method T.doSomething:()V 68: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 71: ldc #12 // String konec bloku try 73: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 76: aload 6 78: ifnull 101 81: aload 5 83: invokevirtual #13 // Method T.close:()V 86: goto 153 89: astore 7 91: aload 6 93: aload 7 95: invokevirtual #15 // Method java/lang/Throwable.addSuppressedException:(Ljava/lang/Throwable;)V 98: goto 153 101: aload 5 103: invokevirtual #13 // Method T.close:()V 106: goto 153 109: astore 7 111: aload 7 113: astore 6 115: aload 7 117: athrow 118: astore 8 120: aload 6 122: ifnull 145 125: aload 5 127: invokevirtual #13 // Method T.close:()V 130: goto 150 133: astore 9 135: aload 6 137: aload 9 139: invokevirtual #15 // Method java/lang/Throwable.addSuppressedException:(Ljava/lang/Throwable;)V 142: goto 150 145: aload 5 147: invokevirtual #13 // Method T.close:()V 150: aload 8 152: athrow 153: aload 4 155: ifnull 177 158: aload_3 159: invokevirtual #13 // Method T.close:()V 162: goto 226 165: astore 5 167: aload 4 169: aload 5 171: invokevirtual #15 // Method java/lang/Throwable.addSuppressedException:(Ljava/lang/Throwable;)V 174: goto 226 177: aload_3 178: invokevirtual #13 // Method T.close:()V 181: goto 226 184: astore 5 186: aload 5 188: astore 4 190: aload 5 192: athrow 193: astore 10 195: aload 4 197: ifnull 219 200: aload_3 201: invokevirtual #13 // Method T.close:()V 204: goto 223 207: astore 11 209: aload 4 211: aload 11 213: invokevirtual #15 // Method java/lang/Throwable.addSuppressedException:(Ljava/lang/Throwable;)V 216: goto 223 219: aload_3 220: invokevirtual #13 // Method T.close:()V 223: aload 10 225: athrow 226: aload_2 227: ifnull 246 230: aload_1 231: invokevirtual #13 // Method T.close:()V 234: goto 289 237: astore_3 238: aload_2 239: aload_3 240: invokevirtual #15 // Method java/lang/Throwable.addSuppressedException:(Ljava/lang/Throwable;)V 243: goto 289 246: aload_1 247: invokevirtual #13 // Method T.close:()V 250: goto 289 253: astore_3 254: aload_3 255: astore_2 256: aload_3 257: athrow 258: astore 12 260: aload_2 261: ifnull 282 264: aload_1 265: invokevirtual #13 // Method T.close:()V 268: goto 286 271: astore 13 273: aload_2 274: aload 13 276: invokevirtual #15 // Method java/lang/Throwable.addSuppressedException:(Ljava/lang/Throwable;)V 279: goto 286 282: aload_1 283: invokevirtual #13 // Method T.close:()V 286: aload 12 288: athrow 289: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 292: ldc #16 // String blok finally 294: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 297: goto 329 300: astore_1 301: aload_1 302: invokevirtual #18 // Method java/lang/RuntimeException.printStackTrace:()V 305: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 308: ldc #16 // String blok finally 310: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 313: goto 329 316: astore 14 318: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 321: ldc #16 // String blok finally 323: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 326: aload 14 328: athrow 329: return Exception table: from to target type 81 86 89 Class java/lang/Throwable 39 76 109 Class java/lang/Throwable 39 76 118 any 125 130 133 Class java/lang/Throwable 109 120 118 any 158 162 165 Class java/lang/Throwable 25 153 184 Class java/lang/Throwable 25 153 193 any 200 204 207 Class java/lang/Throwable 184 195 193 any 230 234 237 Class java/lang/Throwable 12 226 253 Class java/lang/Throwable 12 226 258 any 264 268 271 Class java/lang/Throwable 253 260 258 any 0 289 300 Class java/lang/RuntimeException 0 289 316 any 300 305 316 any 316 318 316 any }
Na nové syntaxi a sémantice bloku try-catch-finally je zajímavá další věc, která pravděpodobně usnadní (či by alespoň měla usnadnit) tvorbu korektních programů v Javě především začátečníkům a taktéž programátorům, kteří dříve používali různé skriptovací jazyky. Blok try s deklarační a výkonnou částí je totiž pouze „vsunut“ mezi původní programový kód, což mj. znamená, že v běžných podmínkách není nutné přidávat žádné další příkazy ani měnit pořadí příkazů. Programátor se tedy může soustředit především na vytvoření aplikační logiky, která nebude – pokud to samozřejmě nebude nutné – zamořena explicitně rozepsaným voláním metod close() a zachytáváním výjimek, které mohou při volání těchto metod nastat. Tuto situaci si můžeme ukázat na velmi jednoduchém příkladu, v němž je do aplikační logiky (v tomto případě kopie dat) pouze „vsunut“ blok try s deklarační i výkonnou částí. Nejprve si vypíšeme vlastní logiku tak, jak ji programátor zamýšlel implementovat:
InputStream input = new FileInputStream(source); OutputStream output = new FileOutputStream(destination); byte[] buffer = new byte[8192]; int n; while ((n = input.read(buffer)) >= 0) { output.write(buffer, 0, n); }
Použitím bloku try(){} se zajistí uzavření jak vstupního, tak i výstupního proudu, a to nezávisle na tom, jestli dojde při operaci input.read() nebo output.write() k nějaké chybě, která je samozřejmě při práci se soubory poměrně častá, zejména v případech, kdy je vstupní soubor zadáván uživatelem nebo se jedná o soubor kopírovaný ze sítě:
try( InputStream input = new FileInputStream(source); OutputStream output = new FileOutputStream(destination) ) { byte[] buffer = new byte[8192]; int n; while ((n = input.read(buffer)) >= 0) { output.write(buffer, 0, n); } }
8. Vliv existence automatické správy prostředků na stávající aplikace
V souvislosti s rozšířením možností bloku try-catch-finally o (polo)automatickou správu objektů implementujících rozhraní AutoCloseable v JDK 7 samozřejmě programátory větších aplikací napadne, jakým způsobem toto nové chování ovlivní jejich stávající a mnoha roky provozu otestované aplikace. V tomto případě je odpověď poměrně jednoduchá – pokud není nový objekt vytvořen v deklarační části bloku try, tj. v kulatých závorkách, chová se stejně jako v předchozích verzích JDK 5 a JDK 6, takže stávající zdrojové kódy není zapotřebí měnit (ostatně překvapivé množství zdrojových kódů dodnes ani nevyužívá možností zavedených v JDK 5). Naopak přidání této podpory pro vlastní třídy je velmi jednoduché – ke třídě implementující rozhraní Closeable (a tím pádem i metodu close) postačuje přidat další rozhraní AutoCloseable. Nic dalšího není většinou zapotřebí.
Pro zjištění, které třídy jsou vhodnými „kandidáty“ pro implementaci rozhraní AutoCloseable, je možné využít například jednoduchou utilitku Joea Darcyho z firmy Oracle, která je dostupná na adrese http://blogs.sun.com/darcy/entry/project_coin_bring_close. V souvislosti se zavedením rozhraní AutoCloseable v JDK 7 došlo i k úpravě velkého množství tříd ze standardních knihoven takovým způsobem, aby kromě rozhraní Closeable implementovaly i rozhraní AutoCloseable. Jedná se o třídy pracující se soubory, sockety, obsahem archivů, databázemi atd. Pro ilustraci jsou v následujícím seznamu uvedeny všechny třídy, které v JDK 7 nové rozhraní AutoCloseable implementují, tj. třídy, s jejichž instancemi je možné pracovat zjednodušeným způsobem – bez explicitního volání metod close():
AbstractInterruptibleChannel, AbstractSelectableChannel, AbstractSelector, AsynchronousFileChannel, AsynchronousServerSocketChannel, AsynchronousSocketChannel, AudioInputStream, BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, ByteArrayInputStream, ByteArrayOutputStream, CharArrayReader, CharArrayWriter, CheckedInputStream, CheckedOutputStream, CipherInputStream, CipherOutputStream, DatagramChannel, DatagramSocket, DataInputStream, DataOutputStream, DeflaterInputStream, DeflaterOutputStream, DigestInputStream, DigestOutputStream, FileCacheImageInputStream, FileCacheImageOutputStream, FileChannel, FileImageInputStream, FileImageOutputStream, FileInputStream, FileLock, FileOutputStream, FileReader, FileSystem, FileWriter, FilterInputStream, FilterOutputStream, FilterReader, FilterWriter, Formatter, ForwardingJavaFileManager, GZIPInputStream, GZIPOutputStream, ImageInputStreamImpl, ImageOutputStreamImpl, InflaterInputStream, InflaterOutputStream, InputStream, InputStream, InputStream, InputStreamReader, JarFile, JarInputStream, JarOutputStream, LineNumberInputStream, LineNumberReader, LogStream, MemoryCacheImageInputStream, MemoryCacheImageOutputStream, MLet, MulticastSocket, ObjectInputStream, ObjectOutputStream, OutputStream, OutputStream, OutputStream, OutputStreamWriter, Pipe.SinkChannel, Pipe.SourceChannel, PipedInputStream, PipedOutputStream, PipedReader, PipedWriter, PrintStream, PrintWriter, PrivateMLet, ProgressMonitorInputStream, PushbackInputStream, PushbackReader, RandomAccessFile, Reader, RMIConnectionImpl, RMIConnectionImpl_Stub, RMIConnector, RMIIIOPServerImpl, RMIJRMPServerImpl, RMIServerImpl, Scanner, SecureDirectoryStream, SelectableChannel, Selector, SequenceInputStream, ServerSocket, ServerSocketChannel, Socket, SocketChannel, SSLServerSocket, SSLSocket, StringBufferInputStream, StringReader, StringWriter, URLClassLoader, WatchService, Writer, XMLDecoder, XMLEncoder, ZipFile, ZipInputStream, ZipOutputStream
9. Odkazy na Internetu
- Control Flow in the Java Virtual Machine
http://www.artima.com/underthehood/flowP.html - Java Virtual Machine
http://en.wikipedia.org/wiki/Java_virtual_machine - ==, .equals(), compareTo(), and compare()
http://leepoint.net/notes-java/data/expressions/22compareobjects.html - New JDK7 features
http://openjdk.java.net/projects/jdk7/features/ - Project Coin: Bringing it to a Close(able)
http://blogs.sun.com/darcy/entry/project_coin_bring_close - ClosableFinder source code
http://blogs.sun.com/darcy/resource/ProjectCoin/CloseableFinder.java - Joe Darcy blog about JDK
http://blogs.sun.com/darcy - Java 7 – more dynamics
http://www.baptiste-wicht.com/2010/04/java-7-more-dynamics/ - ArrayList (JDK 1.4)
http://download.oracle.com/javase/1.4.2/docs/api/java/util/ArrayList.html