Novinky v JDK 7 aneb mírný pokrok v mezích zákona (2)

Pavel Tišnovský 18. 11. 2010

Ve druhé části seriálu o nových vlastnostech programovacího jazyka Java i jeho virtuálního stroje, které jsou zařazeny do JDK 7, si popíšeme další rozšíření syntaxe a sémantiky Javy. Jedná se především o nový operátor nazvaný „diamant“ a taktéž o rozšířené možnosti bloku try-catch-delete.

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

9. Odkazy na Internetu

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://downlo­ad.oracle.com/ja­vase/1.4.2/doc­s/api/java/util/A­rrayList.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)

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 t1t3 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?.doSomethin­g();, 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.Auto­Closeable 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í AutoCloseableJDK 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/en­try/project_co­in_bring_close. V souvislosti se zavedením rozhraní AutoCloseableJDK 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

  1. Control Flow in the Java Virtual Machine
    http://www.ar­tima.com/under­thehood/flowP­.html
  2. Java Virtual Machine
    http://en.wiki­pedia.org/wiki/Ja­va_virtual_machi­ne
  3. ==, .equals(), compareTo(), and compare()
    http://leepoin­t.net/notes-java/data/expres­sions/22compa­reobjects.html
  4. New JDK7 features
    http://openjdk­.java.net/pro­jects/jdk7/fe­atures/
  5. Project Coin: Bringing it to a Close(able)
    http://blogs.sun­.com/darcy/en­try/project_co­in_bring_close
  6. ClosableFinder source code
    http://blogs.sun­.com/darcy/re­source/Projec­tCoin/Closeable­Finder.java
  7. Joe Darcy blog about JDK
    http://blogs.sun­.com/darcy
  8. Java 7 – more dynamics
    http://www.bap­tiste-wicht.com/2010/04­/java-7-more-dynamics/
  9. ArrayList (JDK 1.4)
    http://downlo­ad.oracle.com/ja­vase/1.4.2/doc­s/api/java/util/A­rrayList.html
Našli jste v článku chybu?
DigiZone.cz: Světlé zítřky gaučových sportovců

Světlé zítřky gaučových sportovců

120na80.cz: Cestovní nevolnost. Co pomůže?

Cestovní nevolnost. Co pomůže?

DigiZone.cz: Brexit na ČT 24? Skoro 2 miliony...

Brexit na ČT 24? Skoro 2 miliony...

DigiZone.cz: Prima Max bude mít letní kino. Na střeše...

Prima Max bude mít letní kino. Na střeše...

Lupa.cz: Zkoušeli operátoři manipulovat měření LTE?

Zkoušeli operátoři manipulovat měření LTE?

DigiZone.cz: Slováci první, Češi třetí. Krásný...

Slováci první, Češi třetí. Krásný...

Měšec.cz: Apple Pay je v Česku. Návod na aktivaci

Apple Pay je v Česku. Návod na aktivaci

DigiZone.cz: Nova: technické pauzy každé 1. pondělí

Nova: technické pauzy každé 1. pondělí

Vitalia.cz: 7 receptur z pohanky. Svědčí zdraví

7 receptur z pohanky. Svědčí zdraví

DigiZone.cz: HbbTV KinoSvět: už jede na dalších TV

HbbTV KinoSvět: už jede na dalších TV

120na80.cz: Krémy, nebo spreje na opalování?

Krémy, nebo spreje na opalování?

Lupa.cz: Jaké IoT tarify nabízejí mobilní operátoři?

Jaké IoT tarify nabízejí mobilní operátoři?

Vitalia.cz: Jíme přesolené potraviny. Zrovna tyhle

Jíme přesolené potraviny. Zrovna tyhle

Měšec.cz: TEST: Vyzkoušeli jsme pražské taxikáře

TEST: Vyzkoušeli jsme pražské taxikáře

Měšec.cz: Od kdy musí studenti platit pojistné?

Od kdy musí studenti platit pojistné?

Vitalia.cz: Epidemie: Klíšťová encefalitida po ovčím sýru

Epidemie: Klíšťová encefalitida po ovčím sýru

120na80.cz: Léky a dietní opatření při kopřivce

Léky a dietní opatření při kopřivce

DigiZone.cz: Mobilní aplikace pro DVTV je tady

Mobilní aplikace pro DVTV je tady

Měšec.cz: Ceny PHM v Evropě. Finty na úspory

Ceny PHM v Evropě. Finty na úspory

120na80.cz: Jsou opalovací krémy pro děti jiné?

Jsou opalovací krémy pro děti jiné?