Programování

Jaká verze je váš kód Java?

23. května 2003

Otázka:

A:

public class Hello {public static void main (String [] args) {StringBuffer pozdrav = nový StringBuffer ("ahoj,"); StringBuffer who = new StringBuffer (args [0]). Append ("!"); pozdrav.append (kdo); System.out.println (pozdrav); }} // Konec třídy 

Zpočátku se otázka zdá být poněkud triviální. Do kódu je zahrnuto tak málo kódu Ahoj třída a cokoli existuje, používá pouze funkčnost sahající až k Javě 1.0. Třída by tedy měla běžet téměř v jakémkoli JVM bez problémů, že?

Nebuďte si tak jistí. Zkompilujte jej pomocí javacu z platformy Java 2 Platform, Standard Edition (J2SE) 1.4.1 a spusťte ji v dřívější verzi prostředí Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Výjimka ve vlákně "main" java.lang.NoSuchMethodError na Hello.main (Hello.java:20 ) 

Místo očekávaného „ahoj, svět!“, Tento kód vyvolá runtime chybu, přestože je zdroj stoprocentně kompatibilní s Java 1.0! A chyba není přesně to, co byste očekávali: místo nesouladu verze třídy si nějak stěžuje na chybějící metodu. Zmatený? Pokud ano, úplné vysvětlení najdete dále v tomto článku. Nejprve pojďme rozšířit diskusi.

Proč se obtěžovat s různými verzemi Java?

Java je zcela nezávislá na platformě a většinou kompatibilní směrem vzhůru, takže je běžné sestavit část kódu pomocí dané verze J2SE a očekávat, že bude fungovat v pozdějších verzích JVM. (Ke změnám syntaxe Java obvykle dochází bez jakýchkoli podstatných změn v sadě instrukcí bajtového kódu.) Otázka v této situaci zní: Můžete vytvořit nějaký druh základní verze Java podporované vaší kompilovanou aplikací, nebo je přijatelné výchozí chování kompilátoru? Později vysvětlím své doporučení.

Další poměrně běžnou situací je použití kompilátoru s vyšší verzí než zamýšlená platforma nasazení. V tomto případě nepoužíváte žádná nedávno přidaná rozhraní API, ale chcete pouze těžit z vylepšení nástroje. Podívejte se na tento fragment kódu a pokuste se uhodnout, co by mělo dělat za běhu:

veřejná třída ThreadSurprise {public static void main (String [] args) vyvolá výjimku {Thread [] threads = new Thread [0]; vlákna [-1]. spánek (1); // Mělo by to házet? }} // Konec třídy 

Pokud tento kód hodí ArrayIndexOutOfBoundsException nebo ne? Pokud kompilujete ThreadSurprise s použitím různých verzí Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit) nebude chování konzistentní:

  • Verze 1.1 a dřívější překladače generují kód, který nevyvolává
  • Verze 1.2 hodí
  • Verze 1.3 nehází
  • Verze 1.4 hodí

Jemným bodem je to Thread.sleep () je statická metoda a nepotřebuje Vlákno instance vůbec. Specifikace jazyka Java přesto vyžaduje, aby kompilátor nejen odvodil cílovou třídu z levého výrazu vlákna [-1]. spánek (1);, ale také vyhodnotit samotný výraz (a zahodit výsledek takového hodnocení). Odkazuje na index -1 z vlákna část takového hodnocení? Znění ve specifikaci jazyka Java je poněkud vágní. Souhrn změn pro J2SE 1.4 naznačuje, že nejednoznačnost byla nakonec vyřešena ve prospěch úplného vyhodnocení výrazu na levé straně. Skvělý! Jelikož se kompilátor J2SE 1.4 jeví jako nejlepší volba, chci jej použít pro všechny své programování v Javě, i když je moje cílová runtime platforma dřívější verzí, abych mohl těžit z takových oprav a vylepšení. (Pamatujte, že v době psaní tohoto článku nejsou všechny aplikační servery certifikovány na platformě J2SE 1.4.)

Ačkoli poslední příklad kódu byl poněkud umělý, sloužil k ilustraci bodu. Mezi další důvody, proč použít nejnovější verzi J2SDK, patří chtít těžit z javadocu a dalších vylepšení nástrojů.

A konečně, křížová kompilace je docela způsob života ve vestavěném vývoji Java a vývoji her Java.

Ahoj třídní hádanka vysvětlena

The Ahoj příklad, který zahájil tento článek, je příkladem nesprávné křížové kompilace. J2SE 1.4 přidal do metody novou metodu StringBuffer API: připojit (StringBuffer). Když se javac rozhodne, jak přeložit pozdrav.append (kdo) do bajtového kódu vyhledá StringBuffer definice třídy v bootstrap classpath a vybere tuto novou metodu místo připojit (objekt). I když je zdrojový kód plně kompatibilní s Java 1.0, výsledný bajtový kód vyžaduje běhový modul J2SE 1.4.

Všimněte si, jak snadné je tuto chybu udělat. Neexistují žádná upozornění na kompilaci a chyba je zjistitelná pouze za běhu. Správný způsob použití javacu z J2SE 1.4 ke generování Java 1.1 kompatibilního Ahoj třída je:

> ... \ jdk1.4.1 \ bin \ javac -target 1.1 -bootclasspath ... \ jdk1.1.8 \ lib \ classes.zip Hello.java 

Správné zaklínadlo javac obsahuje dvě nové možnosti. Podívejme se, co dělají a proč jsou nezbytné.

Každá třída Java má razítko verze

Možná si toho neuvědomujete, ale každý .třída soubor, který vygenerujete, obsahuje razítko verze: dvě krátká celá čísla bez znaménka začínající na posunu bajtu 4, hned za 0xCAFEBABE magické číslo. Jedná se o hlavní / vedlejší čísla verzí formátu třídy (viz Specifikace formátu souboru třídy) a kromě toho, že jsou rozšiřujícími body pro tuto definici formátu, mají také užitečnost. Každá verze platformy Java specifikuje řadu podporovaných verzí. Zde je tabulka podporovaných rozsahů v této době psaní (moje verze této tabulky se mírně liší od dat v dokumentech Sunu - rozhodl jsem se odstranit některé hodnoty rozsahů, které jsou relevantní pouze pro extrémně staré (před 1.0.2) verze kompilátoru Sun) :

Platforma Java 1.1: 45,3-45,65535 Platforma Java 1.2: 45,3-46,0 Platforma Java 1.3: 45,3-47,0 Platforma Java 1.4: 45,3-48,0 

Kompatibilní JVM odmítne načíst třídu, pokud je známka verze třídy mimo rozsah podpory JVM. Z předchozí tabulky si všimněte, že novější JVM vždy podporují celý rozsah verzí z předchozí úrovně verzí a také jej rozšiřují.

Co to pro vás jako vývojáře Java znamená? Vzhledem k možnosti ovládat toto razítko verze během kompilace můžete vynutit minimální běhovou verzi Java požadovanou vaší aplikací. To je přesně to, co -cílová možnost kompilátoru ano. Zde je seznam známek verzí vydávaných překladači javac z různých JDK / J2SDK ve výchozím stavu (pozor, J2SDK 1.4 je první J2SDK, kde javac mění svůj výchozí cíl z 1,1 na 1,2):

JDK 1,1: 45,3 J2SDK 1,2: 45,3 J2SDK 1,3: 45,3 J2SDK 1,4: 46,0 

A tady je účinek specifikování různých -cílovás:

-target 1.1: 45.3 -target 1.2: 46.0 -target 1.3: 47.0 -target 1.4: 48.0 

Jako příklad používá následující URL.getPath () metoda přidaná do J2SE 1.3:

 URL url = nová URL ("//www.javaworld.com/columns/jw-qna-index.shtml"); System.out.println ("cesta URL:" + url.getPath ()); 

Protože tento kód vyžaduje alespoň J2SE 1.3, měl bych použít - cíl 1.3 při stavbě. Proč nutit mé uživatele, aby se vypořádali java.lang.NoSuchMethodError překvapení, ke kterým dojde pouze v případě, že omylem naložili třídu do 1,2 JVM? Jistě, mohl bych dokument že moje aplikace vyžaduje J2SE 1.3, ale bylo by to čistší a robustnější prosadit totéž na binární úrovni.

Nemyslím si, že praxe stanovení cílového JVM je při vývoji podnikového softwaru široce používána. Napsal jsem jednoduchou třídu nástrojů DumpClassVersions (k dispozici při stahování tohoto článku), který dokáže skenovat soubory, archivy a adresáře s třídami Java a hlásit všechna nalezená razítka verzí tříd. Některé rychlé procházení populárních open source projektů nebo dokonce základních knihoven z různých JDK / J2SDK neukáže žádný konkrétní systém pro verze tříd.

Vyhledávací cesty bootstrapu a třídy rozšíření

Při překladu zdrojového kódu Java potřebuje překladač znát definici typů, které dosud neviděl. To zahrnuje vaše třídy aplikací a základní třídy jako java.lang.StringBuffer. Jak jsem si jist, že víte, druhá třída se často používá k překladu výrazů obsahujících Tětiva zřetězení a podobně.

Proces povrchně podobný normálnímu načítání tříd aplikací vyhledá definici třídy: nejprve v bootstrap classpath, potom v rozšíření classpath a nakonec v uživatelské classpath (-classpath). Pokud necháte vše na výchozí hodnoty, použijí se definice z J2SDK „domácího“ javacu - což nemusí být správné, jak ukazuje Ahoj příklad.

Chcete-li přepsat vyhledávací cesty třídy bootstrap a třídy rozšíření, použijete -bootclasspath a -dále možnosti javacu. Tato schopnost doplňuje -cílová volba v tom smyslu, že zatímco druhá nastavuje minimální požadovanou verzi JVM, první vybírá API základní třídy dostupné pro generovaný kód.

Pamatujte, že samotný javac byl napsán v Javě. Dvě možnosti, které jsem právě zmínil, ovlivňují vyhledávání třídy pro generování bajtového kódu. Dělají ne ovlivnit bootstrap a rozšíření classpaths používané JVM k provádění javac jako program Java (druhý by mohl být proveden pomocí -J možnost, ale to je docela nebezpečné a vede k nepodporovanému chování). Jinými slovy, javac ve skutečnosti nenačte žádné třídy -bootclasspath a - další; pouze odkazuje na jejich definice.

S nově získaným porozuměním pro podporu křížové kompilace javacu se podívejme, jak to lze použít v praktických situacích.

Scénář 1: Cílení na jednu základní platformu J2SE

Toto je velmi běžný případ: několik verzí J2SE podporuje vaši aplikaci, a tak se stane, že můžete implementovat vše pomocí základních API určitého (budu to nazývat základna) Verze platformy J2SE. O zbytek se postará vyšší kompatibilita. Přestože je J2SE 1.4 nejnovější a nejlepší verzí, nevidíte důvod vyloučit uživatele, kteří zatím J2SE 1.4 nemohou spustit.

Ideální způsob kompilace aplikace je:

\ bin \ javac -target -bootclasspath \ jre \ lib \ rt.jar -classpath 

Ano, z toho vyplývá, že ve svém sestavovacím stroji možná budete muset použít dvě různé verze J2SDK: tu, kterou si vyberete pro svůj javac, a tu, která je vaší základnou podporovanou platformou J2SE. Vypadá to jako extra instalační úsilí, ale ve skutečnosti je to malá cena za robustní sestavení. Klíčem je zde výslovně ovládání jak známek verze třídy, tak bootstrap classpath a nespoléhání se na výchozí hodnoty. Použijte -verbose možnost ověřit, odkud pocházejí definice základních tříd.

Jako vedlejší poznámku zmíním, že je běžné vidět vývojáře rt.jar z jejich J2SDK na -classpath řádek (to by mohl být zvyk z JDK 1,1 dne, kdy jste museli přidat classes.zip do kompilační třídy). Pokud jste postupovali podle výše uvedené diskuse, nyní chápete, že je to zcela nadbytečné a v nejhorším případě by to mohlo narušit správné pořadí věcí.

Scénář 2: Přepnout kód na základě verze Java zjištěné za běhu

Zde chcete být sofistikovanější než ve scénáři 1: Máte verzi platformy Java podporující základnu, ale pokud by váš kód běžel ve vyšší verzi Java, dáváte přednost využití novějších API. Můžete si například vystačit s java.io. * API, ale nevadilo by jim těžit java.nio. * vylepšení novějšího JVM, pokud se příležitost naskytne.

V tomto scénáři se základní přístup ke kompilaci podobá přístupu scénáře 1, kromě toho, že bootstrap J2SDK by měl být nejvyšší verzi, kterou potřebujete použít:

\ bin \ javac -target -bootclasspath \ jre \ lib \ rt.jar -classpath 

To však nestačí; musíte také udělat něco chytrého ve svém Java kódu, aby to dělalo správnou věc v různých verzích J2SE.

Jednou z alternativ je použití preprocesoru Java (alespoň s # ifdef / # else / # endif podpora) a ve skutečnosti generovat různé verze pro různé verze platformy J2SE. Ačkoli J2SDK postrádá správnou podporu předzpracování, na webu není takových nástrojů nedostatek.

Správa několika distribucí pro různé platformy J2SE je však vždy další zátěží. S trochou předvídavosti se můžete dostat pryč s distribucí jediné verze vaší aplikace. Zde je příklad toho, jak to udělat (URLTest1 je jednoduchá třída, která extrahuje různé zajímavé bity z adresy URL):