Obsah
1. Pohled pod kapotu JVM – vliv změn v syntaxi a sémantice Javy na strukturu bajtkódu
2. Změna syntaxe a sémantiky Javy od JDK 1.0 po Java SE 8
3. Autoboxing a unboxing: řešeno překladačem
4. Operátor == při používání autoboxingu a unboxingu
5. Generické datové typy a bajtkód vytvořený překladačem
6. Způsob překladu programové smyčky typu for-each
7. Způsob překladu příkazu switch(String)
8. Slavná nová instrukce invokedynamic
1. Pohled pod kapotu JVM – vliv změn v syntaxi a sémantice Javy na strukturu bajtkódu
V předchozích částech seriálu o programovacím jazyce Java i o vlastnostech virtuálního stroje tohoto jazyka jsme si postupně popsali prakticky všechny instrukce tvořící instrukční soubor virtuálního stroje Javy (JVM – Java Virtual Machine). Tyto až doposud popsané instrukce byly navrženy inženýry z firmy Sun Microsystems již v první polovině devadesátých let minulého století při vlastním návrhu programovacího jazyka Java i jeho virtuálního stroje (Java byla oficiálně představena v roce 1995 a vydána na začátku roku 1996, tj. je přibližně stejně stará jako Ruby a dokonce o čtyři roky mladší než Python). Zajímavé je, že i když se programovací jazyk Java od té doby vyvíjel a přidávaly se do něj nové vlastnosti i nové programové konstrukce, nebylo nutné instrukční soubor modifikovat; pouze se z důvodů lepší kontroly bajtkódu (přesněji řečeno pro zmenšení stavového prostoru při jeho běhové kontrole) přestaly využívat instrukce jsr, jsr_w a ret.
Když se však zamyslíme na změnami, kterými programovací jazyk Java za přibližně sedmnáct let své existence prošel, uvědomíme si, že vlastně k rozšíření instrukčního souboru ani nemuselo dojít, protože se vůbec nezměnily základní vlastnosti tohoto jazyka – především fakt, že se stále jedná o staticky typovaný jazyk využívající jak primitivní datové typy, tak i typy objektové. Například přidání klíčového slova strictfp v JDK 1.2 (může se to zdát jako nepodstatná maličkost, ale v některých oborech je to velmi důležitá změna dovolující pronikání Javy do hájemství Fortranu) ovlivnilo především příznaky přidávané ke třídám, metodám a atributům, tj. došlo pouze k přidání dalšího příznakového bitu k bitovému poli uloženému pro každou třídu či metodu v bajtkódu. Další změny, které Javu potkaly, se na první pohled zdají být poměrně podstatné, ale ve skutečnosti ani při jejich implementaci nemuselo dojít k rozšíření instrukčního souboru. Protože se jedná o zajímavé téma související částečně se zpětnou kompatibilitou bajtkódu (která je stále více na pořadu dne, jak nám Java postupně „COBOLovatí“), ukážeme si v navazujícím textu nějaké ukázky změn v Javě a jejich vliv (pokud je vůbec nějaký) na výsledný bajtkód.
2. Změna syntaxe a sémantiky Javy od JDK 1.0 po Java SE 8
V následující tabulce jsou vypsány nejdůležitější změny syntaxe a sémantiky programovacího jazyka Java, které proběhly mezi roky 1996 až 2011 (plus výhled do roku 2013). V této chvíli nás zajímají skutečně pouze změny provedené ve vlastním jazyku a nikoli rozšíření standardních knihoven Javy, protože to nemá žádný podstatný vliv na strukturu generovaného bajtkódu. Povšimněte si taktéž, jakým způsobem společnost Sun měnila označení verzí Javy:
Rok vydání | Označení JDK/JRE | Nové prvky jazyka |
---|---|---|
1996 | JDK 1.0 | první zveřejněná verze Javy |
1997 | JDK 1.1 | vnitřní třídy, podpora pro reflexi (nemá vliv na syntaxi) |
1998 | J2SE 1.2 | nové klíčové slovo: strictfp |
2000 | J2SE 1.3 | syntetické proxy třídy |
2002 | J2SE 1.4 | nové klíčové slovo: assert |
2004 | J2SE 5.0 | autoboxing, unboxing, smyčka typu for-each, výčtový typ, generika |
2006 | Java SE 6 | JSR 223 – podpora pro skriptovací jazyky (nemá vliv na syntaxi) |
2011 | Java SE 7 | nová instrukce: invokedynamic (v Javě nepoužito), switch (String) a další rozšíření provedené v rámci projektu Coin |
?2013? | Java SE 8 | lambda výrazy, zbylé části z projektu Coin |
Bližší informace o projektu Coin byly uvedeny v úvodních částech tohoto seriálu:
- Novinky v JDK 7 aneb mírný pokrok v mezích zákona (1)
http://www.root.cz/clanky/novinky-v-nbsp-jdk-7-aneb-mirny-pokrok-v-nbsp-mezich-zakona-1/ - Novinky v JDK 7 aneb mírný pokrok v mezích zákona (2)
http://www.root.cz/clanky/novinky-v-nbsp-jdk-7-aneb-mirny-pokrok-v-nbsp-mezich-zakona-2/ - Novinky v JDK 7 (3) + co v JDK 7 naopak nenajdeme
http://www.root.cz/clanky/novinky-v-nbsp-jdk-7–3-co-v-nbsp-jdk-7-naopak-nenajdeme/
3. Autoboxing a unboxing: řešeno překladačem
Jedním z příkladů, kdy sice došlo ke změně sémantiky programovacího jazyka Java, ale kdy tato změna neměla žádný dopad na strukturu bajtkódu, je přidání podpory pro takzvaný autoboxing a unboxing. O autoboxing a unboxing, tj. o automatické oboustranné převody mezi primitivním datovým typem a jeho obalovou třídou (wrapper class), se totiž stará překladač, který při konverzích volá příslušné metody obalové třídy, takže se na autoboxing a unboxing můžeme z tohoto hlediska dívat jako na většinou příjemný syntaktický cukr, který však může začínajícím programátorům v některých případech způsobit nepříjemnosti (například kvůli rozdílu mezi funkcí operátoru ekvivalence ==, viz též následující kapitolu). Podívejme se na jednoduchý příklad, kdy překladač automaticky provádí převody mezi primitivním datovým typem a obalovou třídou, popř. i převody opačným směrem:
public class Test { public static int add(Integer a, Integer b) { // zde se provádí unboxing: // převod instance třídy Integer na // primitivní datový typ int return a+b; } public static void main(String[] args) { // zde se provádí autoboxing: // převod int na Integer System.out.println(add(1,2)); } }
Tento zdrojový text se přeloží následujícím způsobem (komentáře jsou samozřejmě dopsány ručně):
public static int add(java.lang.Integer, java.lang.Integer); Code: // unboxing prvního parametru metody s využitím Integer.intValue() 0: aload_0 1: invokevirtual #2; //Method java/lang/Integer.intValue:()I // unboxing druhého parametru metody s využitím Integer.intValue() 4: aload_1 5: invokevirtual #2; //Method java/lang/Integer.intValue:()I // teď již lze provést součet dvou primitivních hodnot // pomocí jediné instrukce virtuálního stroje 8: iadd // a vrátit výsledek této operace 9: ireturn public static void main(java.lang.String[]); Code: 0: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; // autoboxing prvního parametru metody add() s využitím Integer.valueOf(int) 3: iconst_1 4: invokestatic #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // autoboxing druhého parametru metody add() s využitím Integer.valueOf(int) 7: iconst_2 8: invokestatic #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // nyní máme na zásobníku operandů uložené // dvě REFERENCE na instance třídy Integer // lze tedy volat metodu add(Integer, Integer) 11: invokestatic #5; //Method add:(Ljava/lang/Integer;Ljava/lang/Integer;)I // metoda System.out.println() existuje i ve variantě akceptující "int" // tudíž ji zavoláme (konkrétní metodu samozřejmě určil překladač, nejedná se // tedy o údaj zjišťovaný v čase běhu) 14: invokevirtual #6; //Method java/io/PrintStream.println:(I)V 17: return
4. Operátor == při používání autoboxingu a unboxingu
I když to přímo nesouvisí s tématem dnešního článku, je možná vhodné se zmínit o tom, že autoboxing a unboxing prováděný překladačem může na první pohled vypadat jako zcela transparentní postup, který zcela stírá rozdíl mezi primitivními datovými typy a jejich obalovými třídami (int<->Integer, float<->Float, boolean<->Boolean, char<->Character atd.). To je sice ve většině případů skutečně pravda, ale jednou z výjimek je chování operátoru ==, protože v případě, že je tento operátor aplikován pouze na instance obalových tříd nějakého primitivního datového typu (například Integer), tak se unboxing neprovede a prostě se porovnají reference obou objektů, což (většinou) není chování, které programátor vyžaduje (v dalších třech případech je konverze a následně porovnání provedeno korektně). Ukažme se příklad, který navíc odhalí i určitou „zradu“ spočívající v tom, že se chybné použití operátoru == nemusí projevit ve všech případech kvůli optimalizacím, které se při použití obalových tříd provádí:
public class Test { // porovnání dvou instancí třídy Integer: porovnání dvou referencí public static boolean equalInteger(Integer a, Integer b) { return a == b; } // porovnání dvou hodnot primitivního datového typu int public static boolean equalInt(int a, int b) { return a == b; } public static void main(String[] args) { // problém se zatím "skryl" System.out.println(equalInteger(42, 42)); System.out.println(equalInt(42, 42)); // ale zde se již projeví System.out.println(equalInteger(-1000, -1000)); System.out.println(equalInt(-1000,-1000)); } }
Tento příklad se po svém spuštění může chovat poměrně nekonzistentně (záleží ovšem na použitém virtuálním stroji), protože většinou vypíše následující čtveřici hodnot:
true true false true
Zajímavé přitom je, že jak metoda equalInteger(), tak i metoda equalInt() má prakticky zcela stejný bajtkód, který se liší jen rozdílem mezi instrukcemi if_acmpe a if_icmpe:
public static boolean equalInteger(java.lang.Integer, java.lang.Integer); Code: 0: aload_0 // uložit první argument na zásobník 1: aload_1 // uložit druhý argument na zásobník 2: if_acmpne 9 // porovnání dvou *referencí* 5: iconst_1 // převod výsledku na boolean (0/1) 6: goto 10 9: iconst_0 10: ireturn // na zásobníku je nyní výsledek 0/1, konec
public static boolean equalInt(int, int); Code: 0: iload_0 // uložit první argument na zásobník 1: iload_1 // uložit druhý argument na zásobník 2: if_icmpne 9 // porovnání dvou *celých čísel* 5: iconst_1 // převod výsledku na boolean (0/1) 6: goto 10 9: iconst_0 10: ireturn // na zásobníku je nyní výsledek 0/1, konec
Zmíněná „zrada“ spočívá v tom, že pro malá celá čísla se ve skutečnosti nevytváří metodou Integer.valueOf() nová instance třídy Integer, ale namísto toho se vrátí konstantní objekt (konkrétně se jedná o hodnoty ležící v rozsahu –128 až 127, což se ovšem může v dalších JVM změnit, protože to je implementační detail, který by neměl korektně napsané aplikace ovlivnit). To tedy znamená, že i porovnání referencí dvou shodných konstantních objektů samozřejmě vrátí hodnotu true, i když pro příliš velká či naopak malá čísla (-1000) se vytvoří nové objekty, jejichž reference jsou samozřejmě různé:
public static void main(java.lang.String[]); Code: 0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; // zde nedojde k vytvoření nového objektu 3: bipush 42 5: invokestatic #3; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // zde taktéž nedojde k vytvoření nového objektu 8: bipush 42 10: invokestatic #3; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // porovnají se dvě reference ukazující na stejný objekt 13: invokestatic #4; //Method equalInteger:(Ljava/lang/Integer;Ljava/lang/Integer;)Z 16: invokevirtual #5; //Method java/io/PrintStream.println:(Z)V 19: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 22: bipush 42 24: bipush 42 // zde je to jednoduché - porovnání dvou primitivních hodnot 26: invokestatic #6; //Method equalInt:(II)Z 29: invokevirtual #5; //Method java/io/PrintStream.println:(Z)V 32: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; // vytvoření nové instance třídy Integer 35: sipush -1000 38: invokestatic #3; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // vytvoření nové instance třídy Integer 41: sipush -1000 44: invokestatic #3; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; // porovnají se dvě reference ukazující na různé objekty (instance třídy Integer) 47: invokestatic #4; //Method equalInteger:(Ljava/lang/Integer;Ljava/lang/Integer;)Z 50: invokevirtual #5; //Method java/io/PrintStream.println:(Z)V 53: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 56: sipush -1000 59: sipush -1000 // zde je to jednoduché - porovnání dvou primitivních hodnot 62: invokestatic #6; //Method equalInt:(II)Z 65: invokevirtual #5; //Method java/io/PrintStream.println:(Z)V 68: return }
5. Generické datové typy a bajtkód vytvořený překladačem
Další – z hlediska programátorů poměrně zásadní změnou či přesněji řečeno rozšířením Javy – bylo přidání podpory pro generické datové typy (generik). Jedná se skutečně o dosti významnou změnu, která umožňuje tvorbu typově lépe zabezpečeného kódu a v mnoha případech je navíc výsledný kód i mnohem čitelnější, neboť není zapotřebí explicitně psát přetypování při získávání prvků z kolekcí apod. Tato změna si již vyžádala určité změny v bajtkódu, ale kupodivu se to netýká vlastního instrukčního souboru (nebyly přidány žádné nové instrukce) a dokonce se programový kód využívající generické datové typy mnohdy (ovšem ne vždycky) přeloží s využitím zcela stejných instrukcí jako kód, který generika nevyužívá. V tomto případě jsou veškeré informace o generických datových typech „pouze“ zapsány formou metadat do bajtkódu. Uveďme si nyní příklad dvou tříd, jejichž metody se přeloží do identické sekvence instrukcí, nezávisle na tom, zda jsou využity generické datové typy či nikoli:
import java.util.*; class A { // zde je deklarován "typovaný" seznam List<String> list = new ArrayList<String>(); public void AA() { list.add("foo"); list.add("bar"); } } class B { // zde je deklarován "beztypový" seznam List list = new ArrayList(); public void BB() { list.add("foo"); list.add("bar"); } }
Sekvence instrukcí pro metodu A.AA() i B.BB() bude v tomto případě identická, stejně jako inicializační kód zavolaný při konstrukci nové instance obou tříd:
A(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: new #2; //class java/util/ArrayList 8: dup 9: invokespecial #3; //Method java/util/ArrayList."<init>":()V 12: putfield #4; //Field list:Ljava/util/List; 15: return public void AA(); Code: 0: aload_0 1: getfield #4; //Field list:Ljava/util/List; 4: ldc #5; //String foo // zde se volá metoda List.add(Object) 6: invokeinterface #6, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 11: pop 12: aload_0 13: getfield #4; //Field list:Ljava/util/List; 16: ldc #7; //String bar // zde se volá metoda List.add(Object) 18: invokeinterface #6, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 23: pop 24: return }
B(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: new #2; //class java/util/ArrayList 8: dup 9: invokespecial #3; //Method java/util/ArrayList."<init>":()V 12: putfield #4; //Field list:Ljava/util/List; 15: return public void BB(); Code: 0: aload_0 1: getfield #4; //Field list:Ljava/util/List; 4: ldc #5; //String foo // zde se volá metoda List.add(Object) 6: invokeinterface #6, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 11: pop 12: aload_0 13: getfield #4; //Field list:Ljava/util/List; 16: ldc #7; //String bar // zde se volá metoda List.add(Object) 18: invokeinterface #6, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 23: pop 24: return }
Mohlo by se tedy zdát, že použití generických datových typů je opět jen záležitostí překladače, který tak může provádět lepší kontroly v čase překladu. To je skutečně do značné míry pravda, ovšem v některých případech překladač navíc do sekvence instrukcí může vložit například instrukci checkcast pro kontrolu, jaký typ objektu se skutečně v danou chvíli používá (bližší informace o instrukci checkcast jsme si řekli v osmé kapitole předchozí části tohoto seriálu). Tato kontrola je samozřejmě prováděna až v čase běhu programu, tj. v runtime. Opět se podívejme na jednoduchý příklad:
import java.util.*; class A { List<String> list = new ArrayList<String>(); public void AA() { list.add("foo"); System.out.println(list.get(1)); } } class B { List list = new ArrayList(); public void BB() { list.add("foo"); System.out.println(list.get(1)); } }
V metodě B.BB() nedochází k žádné kontrole typu objektu získaného ze seznamu:
public void BB(); Code: 0: aload_0 1: getfield #4; //Field list:Ljava/util/List; 4: ldc #5; //String foo 6: invokeinterface #6, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 11: pop 12: getstatic #7; //Field java/lang/System.out:Ljava/io/PrintStream; 15: aload_0 16: getfield #4; //Field list:Ljava/util/List; 19: iconst_1 20: invokeinterface #8, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 25: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 28: return
Ovšem v metodě A.AA() ke kontrole již dochází, a to právě s využitím instrukce checkcast (tato instrukce začíná na bajtu 25):
public void AA(); Code: 0: aload_0 1: getfield #4; //Field list:Ljava/util/List; 4: ldc #5; //String foo 6: invokeinterface #6, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 11: pop 12: getstatic #7; //Field java/lang/System.out:Ljava/io/PrintStream; 15: aload_0 16: getfield #4; //Field list:Ljava/util/List; 19: iconst_1 20: invokeinterface #8, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; // ********************************************** 25: checkcast #9; //class java/lang/String // ********************************************** 28: invokevirtual #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 31: return
6. Způsob překladu programové smyčky typu for-each
V J2SE 5.0 vydané již před osmi lety byl kromě autoboxingu, unboxingu, výčtových typů a generických datových typů do programovacího jazyka Java zaveden i nový typ programové smyčky nazývaný for-each podle podobnosti této smyčky s programovými smyčkami, které můžeme nalézt například i v mnoha skriptovacích jazycích. Smyčka for-each je velmi užitečná, neboť umožňuje jednoduše procházet všemi prvky pole (jakéhokoli typu), popř. všemi prvky prakticky libovolné kolekce. Ve skutečnosti jsou však možnosti tohoto typu programové smyčky mnohem větší, protože ji lze aplikovat na každou třídu implementující rozhraní Iterator, předepisující především metody Iterator.hasNext() a Iterator.next(). Způsob, jakým se programová smyčka for-each přeloží, závisí především na tom, zda se prochází (iteruje) přes pole či zda se prochází prvky kolekce (nebo též prvky dostupné přes již zmíněné rozhraní Iterator).
Nejdříve se podívejme na to, jakým způsobem se smyčka for-each přeloží v případě, že je naprogramován průchod polem celých čísel:
import java.util.*; public class ForEachTest1 { public static int sum(int[] pole) { // suma všech hodnot uložených v poli int sum = 0; // smyčka typu for-each // prováděná nad polem for (int x : pole) { sum += x; } return sum; } public static void main(String[] args) { int[] pole = {3, 4, 5, 6, 7, 8, 9}; System.out.println(sum(pole)); } }
Překladač výše uvedený průchod všemi prvky pole celých čísel musí přeložit podobným způsobem, jakoby se polem procházelo s využitím počítané smyčky for. Nejprve se tedy vytvoří několik pomocných proměnných (uložených v zásobníkovém rámci metody), z nichž jedna obsahuje hodnotu počitadla a další mezní hodnotu tohoto počitadla, tj. délku pole získanou instrukcí arraylength (prvky pole jsou samozřejmě indexovány od 0 do pole.length-1). V přeloženém bajtkódu je jasně patrné, jakým způsobem se celá smyčka zkonstruovala. Smyčka začíná od instrukce ležící na bajtu 10 a končí instrukcí ležící na bajtu 30:
public static int sum(int[]); Code: // první pomocná lokální proměnná bude obsahovat sumu 0: iconst_0 1: istore_1 // druhá pomocná lokální proměnná bude obsahovat referenci na pole 2: aload_0 3: astore_2 // třetí pomocná lokální proměnná bude obsahovat délku pole 4: aload_2 5: arraylength 6: istore_3 // čtvrtá pomocná lokální proměnná bude obsahovat počitadlo smyčky 7: iconst_0 8: istore 4 // začátek programové smyčky 10: iload 4 12: iload_3 // když počitadlo ≥ délka pole, ukonči smyčku 13: if_icmpge 33 16: aload_2 17: iload 4 19: iaload // načtený prvek pole do páté pomocné lokální proměnné 20: istore 5 22: iload_1 23: iload 5 // součet s průběžnou sumou 25: iadd 26: istore_1 // zvýšení počitadla smyčky o jedničku 27: iinc 4, 1 // další iterace 30: goto 10 // sumu uložit na zásobník operandů 33: iload_1 // protože se jedná o návratovou hodnotu metody // zpřístupněnou volajícímu kódu pomocí instrukce ireturn 34: ireturn
Zcela odlišným způsobem je programová smyčka typu for-each přeložena při průchodu kolekcemi, například seznamem (libovolnou kolekcí implementující rozhraní List). Podívejme se opět na zdrojový příklad, v němž je průchod seznamem zapsán formou této smyčky. Jako poměrně užitečná zajímavost je navíc v metodě nazvané arrayAsList() ukázáno, jak lze pracovat s polem podobně jako s neměnným seznamem, tj. se seznamem, který má pevný počet prvků a hodnoty těchto prvků je možné jen číst (relativně snadno lze však s využitím pole implementovat i modifikovatelný seznam, viz též dokumentace k abstraktní třídě AbstractList):
import java.util.*; public class ForEachTest2 { public static int sum(List<Integer> seznam) { int sum = 0; for (int x : seznam) { sum += x; } return sum; } // pravděpodobně nejjednodušší možnost, jak je možné // pracovat s polem jako s neměnným seznamem public static List<Integer> arrayAsList(final int[] pole) { return new AbstractList<Integer>() { public Integer get(int i) { return pole[i]; } public int size() { return pole.length; } }; } public static void main(String[] args) { int[] pole = {3, 4, 5, 6, 7, 8, 9}; System.out.println(sum(arrayAsList(pole))); } }
V tomto případě musí překladač přeložit programovou smyčku for-each podobně, jako by přeložil tento zdrojový kód:
while (Iterator.hasNext()) { int x = (Integer)Iterator.next(); sum += x; }
Z bajtkódu vypsaného pod tímto odstavcem je patrné, že se nejprve pro seznam získá instance třídy implementující rozhraní Iterator (sice to z názvu není patrné, ale skutečně se jedná o rozhraní). Posléze se na začátku každé iterace testuje návratová hodnota metody Iterator.hasNext() a pokud se ještě nedošlo za poslední prvek, je získána hodnota prvku s využitím metody Iterator.next(). Překladač samozřejmě provede test na typ hodnoty získané z kolekce a následně převede tuto hodnotu (typu Integer) na primitivní hodnotu int:
public static int sum(java.util.List); Code: // první pomocná lokální proměnná bude obsahovat sumu 0: iconst_0 1: istore_1 // druhá pomocná lokální proměnná bude instanci třídy // implementující rozhraní Iterator 2: aload_0 3: invokeinterface #2, 1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 8: astore_2 // začátek programové smyčky 9: aload_2 // test, zda kolekce obsahuje další prvek 10: invokeinterface #3, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z // pokud ne, programová smyčka se ukončí 15: ifeq 38 18: aload_2 // získání dalšího prvku s využitím metody Iterator.next() 19: invokeinterface #4, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; // a převod prvku na int s nezbytnou kontrolou 24: checkcast #5; //class java/lang/Integer 27: invokevirtual #6; //Method java/lang/Integer.intValue:()I // načtený prvek pole do třetí pomocné lokální proměnné 30: istore_3 31: iload_1 32: iload_3 // součet s průběžnou sumou 33: iadd 34: istore_1 // další iterace 35: goto 9 // sumu uložit na zásobník operandů 38: iload_1 // protože se jedná o návratovou hodnotu metody 39: ireturn
7. Způsob překladu příkazu switch(String)
Jedním z rozšíření syntaxe a sémantiky programovacího jazyka Java, na nějž museli programátoři čekat až do roku 2011, konkrétně až do oficiálního vydání Java SE 7, je podpora řetězců v programové konstrukci typu switch-case. Připomeňme si, že tato konstrukce je sice v Javě používána už od jejího vzniku, ovšem až do J2SE 1.4 bylo možné v příkazu switch-case použít pouze celočíselný výraz za switch a celočíselné konstanty v každé větvi case. To je na vyšší programovací jazyk poměrně málo (když pomineme fakt, že by se Java vlastně bez této „neobjektové“ konstrukce docela dobře obešla :-), proto se v J2SE 1.5 rozšířily možnosti switch-case o použití výčtového typu a v Java SE 7 i o možnost zápisu výrazu vyhodnoceného na řetězec ve switch a řetězcových literálů („řetězcových konstant“) v každé větvi case. Podívejme se na příklad, s nímž jsme se vlastně již seznámili v první části tohoto seriálu. Tento příklad vypíše počet dnů pro každý měsíc roku 2012:
public class StringSwitchTest { public static final int YEAR = 2012; public static int getDaysOfMonth(String month) { switch (month) { case "April": case "June": case "September": case "November": return 30; case "January": case "March": case "May": case "July": case "August": case "October": case "December": return 31; case "February": return 29; // nebudeme si to teď zbytečně komplikovat výpočtem :-) default: throw new RuntimeException("Unknown month name: " + month); } } public static void main(String[] args) { String[] months = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; for (String month : months) { int days = getDaysOfMonth(month); System.out.format("%-10s\t%d\n", month, days); } } }
Naivní překladač by konstrukci switch-case použitou v předchozím demonstračním příkladu mohl přeložit jako sekvenci if-else if-else if-…else, kde by se v každé podmínce provádělo testování dvou řetězců na rovnost s využitím metody String.equals() (ve skutečnosti by se zrovna v tomto případě mohly porovnávat přímo reference z constant poolu, ale nebudeme si situaci zbytečně komplikovat a ani překladač tuto možnost nikdy nevyužije, protože neví, v jakém kontextu se metoda getDaysOfMonth() může volat). Překladač JDK 7 však „řetězcovou variantu“ programové konstrukce switch-case přeloží poměrně efektivním způsobem, jehož výhody se projeví zejména tehdy, pokud by se tato konstrukce využila například v programové smyčce, nebo by bylo větví case velké množství: mohlo by se například jednat o nějaký jednoduchý lexikální analyzátor, já jsem například podobnou konstrukci použil při načítání souborů typu DXF obsahujících velké množství klíčových slov.
Překladač nejdříve vypočte otisk (hash) řetězce month (otiskem je 32bitové celé číslo). Tento otisk je použit v instrukci lookupswitch při porovnávání s otisky řetězcových literálů. Zde je důležitý především fakt, že tyto otisky jsou vypočteny již v době překladu (compile time), což znamená, že v době běhu (runtime) se jimi již virtuální stroj nemusí zdržovat. Pokud je otisk řetězce month shodný s otiskem nějakého literálu, je proveden skok do větve, v níž se zavolá metoda String.equals(), protože shoda otisků řetězců samozřejmě nemusí nutně znamenat to, že jsou řetězce skutečně identické (jinými slovy – při hešování nutně občas dochází ke kolizím). Důležité je, že se metoda String.equals() zavolá maximálně jedenkrát (přesněji řečeno v našem případě maximálně jedenkrát, protože u námi použitých řetězcových literálů nedošlo ke kolizi), což je podstatné pro výkonnost programu, jelikož volání metody String.equals() je obecně mnohem pomalejší, než pouhé porovnání dvou 32bitových otisků.
V každé větvi je na zásobník uložena celočíselná konstanta, která je následně použita v instrukci tableswitch pro vrácení správné hodnoty z celé funkce. Výsledný bajtkód je sice poněkud delší, ovšem je na druhou stranu proveden mnohem rychleji, a to zejména v případech, kdy, jak již bylo řečeno, počet větví příkazu switch roste:
public static int getDaysOfMonth(java.lang.String); Code: 0: aload_0 1: astore_1 // uložit -1 do pomocné proměnné použité v tableswitch 2: iconst_m1 3: istore_2 // uložit na zásobník referenci na řetězec month 4: aload_1 // výpočet hešovacího kódu tohoto řetězce 5: invokevirtual #2; //Method java/lang/String.hashCode:()I // provést rozeskok na základě vypočteného hešovacího kódu 8: lookupswitch{ //12 -199248958: 275; // otisky řetězců jednotlivých měsíců + cílová adresa skoku -162006966: 172; -25881423: 144; 77125: 200; 2320440: 215; 2320482: 130; 43165376: 245; 63478374: 116; 74113571: 186; 626483269: 260; 1703773522: 158; 1972131363: 230; default: 287 } // začátek větve case 116: aload_1 117: ldc #3; //String April 119: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 122: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 125: iconst_0 126: istore_2 // přeskok dalších větví 127: goto 287 // začátek větve case 130: aload_1 131: ldc #5; //String June 133: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 136: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 139: iconst_1 140: istore_2 // přeskok dalších větví 141: goto 287 // začátek větve case 144: aload_1 145: ldc #6; //String September 147: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 150: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 153: iconst_2 154: istore_2 // přeskok dalších větví 155: goto 287 // začátek větve case 158: aload_1 159: ldc #7; //String November 161: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 164: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 167: iconst_3 168: istore_2 // přeskok dalších větví 169: goto 287 // začátek větve case 172: aload_1 173: ldc #8; //String January 175: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 178: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 181: iconst_4 182: istore_2 // přeskok dalších větví 183: goto 287 // začátek větve case 186: aload_1 187: ldc #9; //String March 189: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 192: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 195: iconst_5 196: istore_2 // přeskok dalších větví 197: goto 287 // začátek větve case 200: aload_1 201: ldc #10; //String May 203: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 206: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 209: bipush 6 211: istore_2 // přeskok dalších větví 212: goto 287 // začátek větve case 215: aload_1 216: ldc #11; //String July 218: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 221: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 224: bipush 7 226: istore_2 // přeskok dalších větví 227: goto 287 // začátek větve case 230: aload_1 231: ldc #12; //String August 233: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 236: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 239: bipush 8 241: istore_2 // přeskok dalších větví 242: goto 287 // začátek větve case 245: aload_1 246: ldc #13; //String October 248: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 251: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 254: bipush 9 256: istore_2 // přeskok dalších větví 257: goto 287 // začátek větve case 260: aload_1 261: ldc #14; //String December 263: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 266: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 269: bipush 10 271: istore_2 // přeskok dalších větví 272: goto 287 // začátek větve case 275: aload_1 276: ldc #15; //String February 278: invokevirtual #4; //Method java/lang/String.equals:(Ljava/lang/Object;)Z 281: ifeq 287 // hodnota použitá instrukcí tableswitch pro druhý rozeskok 284: bipush 11 286: istore_2 // v pomocné proměnné na pozici 2 je nyní uloženo číslo -1 až 11 // na základě této hodnoty se provede druhý rozeskok do tří větví 287: iload_2 288: tableswitch{ //0 to 11 0: 352; // zde již máme všech 13 cílů skoku: 12 měsíců + větev default 1: 352; 2: 352; 3: 352; 4: 355; 5: 355; 6: 355; 7: 355; 8: 355; 9: 355; 10: 355; 11: 358; default: 361 } // větev pro měsíce s 30 dny 352: bipush 30 354: ireturn // větev pro měsíce s 31 dny 355: bipush 31 357: ireturn // větev pro Únor 358: bipush 29 360: ireturn
8. Slavná nová instrukce invokedynamic
Všechny instrukce virtuálního stroje Javy, které jsme si popsali v předchozích částech tohoto seriálu, byly skutečně navrženy s ohledem na možnosti a potřeby tohoto programovacího jazyka. Ovšem společně s neustálým rozšiřováním JVM do různých odvětví informatiky se objevila snaha o to, aby se nad virtuálním strojem Javy mohly ve skutečnosti spouštět i programy napsané v jiných programovacích jazycích. Vzhledem k neustále rostoucímu výpočetnímu výkonu mikroprocesorů, zvětšující se kapacitě operačních pamětí a současně i neklesající ceně za hodinu času práce programátorů je vlastně logické, že se stále více prosazují dynamicky typované programovací jazyky, například JavaScript, Python, Ruby či různé varianty Lispu. Tvůrci Javy na tento trend (relativně pozdě, ale přece) zareagovali, a to zpočátku vytvořením specifikace JSR 223: Scripting for the Java Platform a posléze i mnohem zásadnější specifikací JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform.
První zmíněná specifikace souvisí především se způsobem unifikované kooperace mezi skripty napsanými v některém podporovaném skriptovacím jazyku na jedné straně a programem napsaným v Javě na straně druhé. Teoreticky by tedy tato specifikace mohla dostačovat všem tvůrcům nových implementací programovacích jazyků běžících nad JVM. Ve skutečnosti se však při snaze o překlad skriptů (naprogramovaných dejme tomu v Jythonu) do bajtkódu JVM objevují některé problémy související především s tím, že bajtkód i instrukční soubor JVM byl navržen pro potřeby staticky typovaného programovacího jazyka, kde je například vždy zřejmé, jaká metoda se má zavolat – jakého typu jsou její parametry a jakého typu je její návratová hodnota (pozdní vazba se aplikuje pouze na třídu, jejíž metoda se má volat). V dynamicky typovaných programovacích jazycích je však situace poněkud složitější (a pro programátory používajícími tento jazyk zase jednodušší), protože typy jsou přiřazované nikoli k proměnným/atributům/parametrům, ale přímo k hodnotám.
Z tohoto důvodu není při překladu do bajtkódu JVM možné pro volání metod použít „klasické“ instrukce invokespecial/invokevirtual/invokeinterface, ale musela by se nejdříve vhodná metoda najít. Aby se tato práce převedla přímo na JVM, byla do bajtkódu přidána další instrukce nazvaná invokedynamic. Význam této instrukce i způsob jejího použití si popíšeme v navazující části seriálu.
9. Odkazy na Internetu
- Java Virtual Machine Support for Non-Java Languages
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html - New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
http://java.sun.com/developer/technicalArticles/DynTypeLang/ - JSR 223: Scripting for the JavaTM Platform
http://jcp.org/en/jsr/detail?id=223 - JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform
http://jcp.org/en/jsr/detail?id=292 - Java 7: A complete invokedynamic example
http://niklasschlimm.blogspot.com/2012/02/java-7-complete-invokedynamic-example.html - InvokeDynamic: Actually Useful?
http://blog.headius.com/2007/01/invokedynamic-actually-useful.html - A First Taste of InvokeDynamic
http://blog.headius.com/2008/09/first-taste-of-invokedynamic.html - Java 6 try/finally compilation without jsr/ret
http://cliffhacks.blogspot.com/2008/02/java-6-tryfinally-compilation-without.html - An empirical study of Java bytecode programs
http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/ - Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
http://www.mobilefish.com/tutorials/java/java_quickguide_jvm_instruction_set.html - The JVM Instruction Set
http://mpdeboer.home.xs4all.nl/scriptie/node14.html - Control Flow in the Java Virtual Machine
http://www.artima.com/underthehood/flowP.html - Root.cz: Využití komprimovaných ukazatelů na objekty v JVM
http://www.root.cz/clanky/vyuziti-komprimovanych-ukazatelu-na-objekty-v-nbsp-jvm/ - Root.cz: JamVM aneb alternativa k HotSpotu nejenom pro embedded zařízení a chytré telefony
http://www.root.cz/clanky/jamvm-aneb-alternativa-k-hotspotu-nejenom-pro-embedded-zarizeni-tablety-a-chytre-telefony/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - BCEL Home page
http://commons.apache.org/bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - FindBugs
http://findbugs.sourceforge.net/ - GNU Classpath
www.gnu.org/s/classpath/ - Java VMs Compared
http://bugblogger.com/java-vms-compared-160/ - JSRs: Java Specification Requests – JSR 223: Scripting for the Java Platform
http://www.jcp.org/en/jsr/detail?id=223 - Scripting for the Java Platform
http://java.sun.com/developer/technicalArticles/J2SE/Desktop/scripting/ - Scripting for the Java Platform (Wikipedia)
http://en.wikipedia.org/wiki/Scripting_for_the_Java_Platform - Java Community Process
http://en.wikipedia.org/wiki/Java_Specification_Request - Java HotSpot VM Options
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html - Great Computer Language Shootout
http://c2.com/cgi/wiki?GreatComputerLanguageShootout - Java performance
http://en.wikipedia.org/wiki/Java_performance - Trying the prototype
http://mail.openjdk.java.net/pipermail/lambda-dev/2010-August/002179.html - Better closures (for Java)
http://blogs.sun.com/jrose/entry/better_closures - Lambdas in Java: An In-Depth Analysis
http://www.infoq.com/articles/lambdas-java-analysis - Class ReflectiveOperationException
http://download.java.net/jdk7/docs/api/java/lang/ReflectiveOperationException.html - Proposal: Indexing access syntax for Lists and Maps
http://mail.openjdk.java.net/pipermail/coin-dev/2009-March/001108.html - Proposal: Elvis and Other Null-Safe Operators
http://mail.openjdk.java.net/pipermail/coin-dev/2009-March/000047.html - Java 7 : Oracle pushes a first version of closures
http://www.baptiste-wicht.com/2010/05/oracle-pushes-a-first-version-of-closures/ - Groovy: An agile dynamic language for the Java Platform
http://groovy.codehaus.org/Operators - Better Strategies for Null Handling in Java
http://www.slideshare.net/Stephan.Schmidt/better-strategies-for-null-handling-in-java - 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 - CloseableFinder 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/ - New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
http://java.sun.com/developer/technicalArticles/DynTypeLang/index.html