Ušetřete

Hlavní navigace

Pohled pod kapotu JVM - vliv změn v syntaxi a sémantice Javy na strukturu bajtkódu

Ve dalším článku o jazyku Java a JVM navážeme na části předchozí, v nichž jsme si popsali instrukční soubor virtuálního stroje Javy. Řekneme si, jak se měnila struktura bajtkódu společně s rozšiřováním syntaxe a sémantiky Javy. Asi největší změnou je však přidání instrukce invokedynamic do JDK 7.

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

9. Odkazy na Internetu

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:

  1. Novinky v JDK 7 aneb mírný pokrok v mezích zákona (1)
    http://www.ro­ot.cz/clanky/no­vinky-v-nbsp-jdk-7-aneb-mirny-pokrok-v-nbsp-mezich-zakona-1/
  2. Novinky v JDK 7 aneb mírný pokrok v mezích zákona (2)
    http://www.ro­ot.cz/clanky/no­vinky-v-nbsp-jdk-7-aneb-mirny-pokrok-v-nbsp-mezich-zakona-2/
  3. Novinky v JDK 7 (3) + co v JDK 7 naopak nenajdeme
    http://www.ro­ot.cz/clanky/no­vinky-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()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

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.hasNex­t() 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.hasNex­t() 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/a­tributům/para­metrů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/in­vokevirtual/in­vokeinterface, 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

  1. Java Virtual Machine Support for Non-Java Languages
    http://docs.o­racle.com/java­se/7/docs/techno­tes/guides/vm/mul­tiple-language-support.html
  2. New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
    http://java.sun­.com/developer/techni­calArticles/Dyn­TypeLang/
  3. JSR 223: Scripting for the JavaTM Platform
    http://jcp.or­g/en/jsr/deta­il?id=223
  4. JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform
    http://jcp.or­g/en/jsr/deta­il?id=292
  5. Java 7: A complete invokedynamic example
    http://niklas­schlimm.blogspot­.com/2012/02/ja­va-7-complete-invokedynamic-example.html
  6. InvokeDynamic: Actually Useful?
    http://blog.he­adius.com/2007/01­/invokedynamic-actually-useful.html
  7. A First Taste of InvokeDynamic
    http://blog.he­adius.com/2008/09­/first-taste-of-invokedynamic.html
  8. Java 6 try/finally compilation without jsr/ret
    http://cliffhac­ks.blogspot.com/2008/02­/java-6-tryfinally-compilation-without.html
  9. An empirical study of Java bytecode programs
    http://www.men­deley.com/rese­arch/an-empirical-study-of-java-bytecode-programs/
  10. Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
    http://www.mo­bilefish.com/tu­torials/java/ja­va_quickguide_jvm_in­struction_set­.html
  11. The JVM Instruction Set
    http://mpdebo­er.home.xs4all­.nl/scriptie/no­de14.html
  12. Control Flow in the Java Virtual Machine
    http://www.ar­tima.com/under­thehood/flowP­.html
  13. Root.cz: Využití komprimovaných ukazatelů na objekty v JVM
    http://www.ro­ot.cz/clanky/vy­uziti-komprimovanych-ukazatelu-na-objekty-v-nbsp-jvm/
  14. Root.cz: JamVM aneb alternativa k HotSpotu nejenom pro embedded zařízení a chytré telefony
    http://www.ro­ot.cz/clanky/jam­vm-aneb-alternativa-k-hotspotu-nejenom-pro-embedded-zarizeni-tablety-a-chytre-telefony/
  15. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun­.com/docs/book­s/jvms/second_e­dition/html/VMSp­ecTOC.doc.html
  16. The class File Format
    http://java.sun­.com/docs/book­s/jvms/second_e­dition/html/Clas­sFile.doc.html
  17. javap – The Java Class File Disassembler
    http://docs.o­racle.com/java­se/1.4.2/docs/to­oldocs/window­s/javap.html
  18. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.di­e.net/man/1/j­avap-java-1.6.0-openjdk
  19. Using javap
    http://www.ide­velopment.info/da­ta/Programmin­g/java/miscella­neous_java/Usin­g_javap.html
  20. Examine class files with the javap command
    http://www.techre­public.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  21. BCEL Home page
    http://common­s.apache.org/bcel/
  22. BCEL Manual
    http://common­s.apache.org/bcel/ma­nual.html
  23. Byte Code Engineering Library (Wikipedia)
    http://en.wiki­pedia.org/wiki/BCEL
  24. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ib­m.com/developer­works/java/li­brary/j-dyn0414/
  25. Bytecode Engineering
    http://book.chi­naunix.net/spe­cial/ebook/Co­re_Java2_Volu­me2AF/0131118269/ch13lev­1sec6.html
  26. BCEL Tutorial
    http://www.smfsup­port.com/suppor­t/java/bcel-tutorial!/
  27. ASM Home page
    http://asm.ow2­.org/
  28. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2­.org/users.html
  29. ObjectWeb ASM (Wikipedia)
    http://en.wiki­pedia.org/wiki/Ob­jectWeb_ASM
  30. Java Bytecode BCEL vs ASM
    http://james.o­negoodcookie.com/2005/10­/26/java-bytecode-bcel-vs-asm/
  31. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2­.org/eclipse/in­dex.html
  32. aspectj (Eclipse)
    http://www.eclip­se.org/aspectj/
  33. Aspect-oriented programming (Wikipedia)
    http://en.wiki­pedia.org/wiki/As­pect_oriented_pro­gramming
  34. AspectJ (Wikipedia)
    http://en.wiki­pedia.org/wiki/As­pectJ
  35. EMMA: a free Java code coverage tool
    http://emma.sou­rceforge.net/
  36. Cobertura
    http://cobertu­ra.sourceforge­.net/
  37. FindBugs
    http://findbug­s.sourceforge­.net/
  38. GNU Classpath
    www.gnu.org/s/clas­spath/
  39. Java VMs Compared
    http://bugblog­ger.com/java-vms-compared-160/
  40. JSRs: Java Specification Requests – JSR 223: Scripting for the Java Platform
    http://www.jcp­.org/en/jsr/de­tail?id=223
  41. Scripting for the Java Platform
    http://java.sun­.com/developer/techni­calArticles/J2SE/Des­ktop/scriptin­g/
  42. Scripting for the Java Platform (Wikipedia)
    http://en.wiki­pedia.org/wiki/Scrip­ting_for_the_Ja­va_Platform
  43. Java Community Process
    http://en.wiki­pedia.org/wiki/Ja­va_Specificati­on_Request
  44. Java HotSpot VM Options
    http://www.ora­cle.com/technet­work/java/java­se/tech/vmopti­ons-jsp-140102.html
  45. Great Computer Language Shootout
    http://c2.com/cgi/wi­ki?GreatCompu­terLanguageSho­otout
  46. Java performance
    http://en.wiki­pedia.org/wiki/Ja­va_performance
  47. Trying the prototype
    http://mail.o­penjdk.java.net/pi­permail/lambda-dev/2010-August/002179.html
  48. Better closures (for Java)
    http://blogs.sun­.com/jrose/en­try/better_clo­sures
  49. Lambdas in Java: An In-Depth Analysis
    http://www.in­foq.com/articles/lam­bdas-java-analysis
  50. Class ReflectiveOpe­rationExcepti­on
    http://downlo­ad.java.net/jdk7/doc­s/api/java/lan­g/ReflectiveO­perationExcep­tion.html
  51. Proposal: Indexing access syntax for Lists and Maps
    http://mail.o­penjdk.java.net/pi­permail/coin-dev/2009-March/001108.html
  52. Proposal: Elvis and Other Null-Safe Operators
    http://mail.o­penjdk.java.net/pi­permail/coin-dev/2009-March/000047.html
  53. Java 7 : Oracle pushes a first version of closures
    http://www.bap­tiste-wicht.com/2010/05­/oracle-pushes-a-first-version-of-closures/
  54. Groovy: An agile dynamic language for the Java Platform
    http://groovy­.codehaus.org/O­perators
  55. Better Strategies for Null Handling in Java
    http://www.sli­deshare.net/Step­han.Schmidt/bet­ter-strategies-for-null-handling-in-java
  56. Control Flow in the Java Virtual Machine
    http://www.ar­tima.com/under­thehood/flowP­.html
  57. Java Virtual Machine
    http://en.wiki­pedia.org/wiki/Ja­va_virtual_machi­ne
  58. ==, .equals(), compareTo(), and compare()
    http://leepoin­t.net/notes-java/data/expres­sions/22compa­reobjects.html
  59. New JDK7 features
    http://openjdk­.java.net/pro­jects/jdk7/fe­atures/
  60. Project Coin: Bringing it to a Close(able)
    http://blogs.sun­.com/darcy/en­try/project_co­in_bring_close
  61. CloseableFinder source code
    http://blogs.sun­.com/darcy/re­source/Projec­tCoin/Closeable­Finder.java
  62. Joe Darcy blog about JDK
    http://blogs.sun­.com/darcy
  63. Java 7 – more dynamics
    http://www.bap­tiste-wicht.com/2010/04­/java-7-more-dynamics/
  64. New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
    http://java.sun­.com/developer/techni­calArticles/Dyn­TypeLang/index­.html

Pavel Tišnovský

Pavel Tišnovský

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro Customer Content Services.

Ohodnoťte jako ve škole:
Průměrná známka 1

Školení: Obsahová strategie a content marketing

  •  
    Proč je obsahový marketing výrazným trendem.
  • Jak navrhnout užitečnou obsahovou strategii.
  • Jak zlepšit workflow a výsledky copywritingu.

Detailní informace o školení content strategy »

       
17 názorů Vstoupit do diskuse
poslední názor přidán 1. 6. 2012 19:30

Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.

Zasílat nově přidané příspěvky e-mailem