Programovanie

Aká verzia je váš kód Java?

23. mája 2003

Otázka:

A:

verejná trieda Dobrý deň {public static void main (String [] args) {StringBuffer pozdrav = nový StringBuffer ("ahoj,"); StringBuffer who = nový StringBuffer (args [0]). Append ("!"); pozdrav.append (kto); System.out.println (pozdrav); }} // Koniec triedy 

Otázka sa spočiatku zdá byť dosť triviálna. Do kódu je zapojených tak málo kódu Ahoj triedy a všetko, čo existuje, používa iba funkcionalitu staršiu ako Java 1.0. Trieda by teda mala bežať takmer v akomkoľvek JVM bez problémov, však?

Nebuď si taký istý. Zkompilujte ho pomocou javacu z platformy Java 2 Platform, Standard Edition (J2SE) 1.4.1 a spustite ho v staršej verzii prostredia Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Výnimka vo vlákne "main" java.lang.NoSuchMethodError na Hello.main (Hello.java:20 ) 

Namiesto očakávaného „ahoj, svet!“, Tento kód vyvolá chybu za behu, aj keď je zdroj stopercentne kompatibilný s Java 1.0! A chyba tiež nie je presne to, čo by ste čakali: namiesto nesúladu triednej verzie sa akosi sťažuje na chýbajúcu metódu. Zmätený? Ak je to tak, úplné vysvetlenie nájdete ďalej v tomto článku. Najskôr si rozšírime diskusiu.

Prečo sa obťažovať s rôznymi verziami Java?

Java je úplne nezávislá na platforme a väčšinou kompatibilná smerom nahor, takže je bežné zostaviť časť kódu pomocou danej verzie J2SE a očakávať, že bude fungovať v neskorších verziách JVM. (Zmeny syntaxe Java sa zvyčajne vyskytujú bez akýchkoľvek podstatných zmien v súbore inštrukcií bajtového kódu.) Otázka v tejto situácii znie: Môžete vytvoriť nejaký druh základnej verzie Java podporovanej vašou kompilovanou aplikáciou alebo je predvolené správanie kompilátora prijateľné? Svoje odporúčanie vysvetlím neskôr.

Ďalšou celkom bežnou situáciou je použitie kompilátora s vyššou verziou ako zamýšľaná platforma nasadenia. V takom prípade nepoužívate žiadne nedávno pridané rozhrania API, iba chcete využívať vylepšenia nástrojov. Prezrite si tento útržok kódu a skúste hádať, čo by malo robiť za behu programu:

public class ThreadSurprise {public static void main (String [] args) throws Exception {Thread [] threads = new Thread [0]; vlákna [-1]. spánok (1); // Malo by to hádzať? }} // Koniec triedy 

Mal by tento kód hodiť znak ArrayIndexOutOfBoundsException alebo nie? Ak zostavíte ThreadSurprise pri použití rôznych verzií Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit) nebude správanie konzistentné:

  • Verzia 1.1 a staršie kompilátory generujú kód, ktorý sa nehodí
  • Verzia 1.2 hodí
  • Verzia 1.3 nehádže
  • Verzia 1.4 hodí

Jemným bodom je tu Thread.sleep () je statická metóda a nepotrebuje Závit inštancia vôbec. Špecifikácia jazyka Java napriek tomu vyžaduje, aby kompilátor nielen odvodil cieľovú triedu z výrazu vľavo vlákna [-1]. spánok (1);, ale tiež vyhodnotiť samotný výraz (a zahodiť výsledok takého hodnotenia). Odkazuje na index -1 z nite súčasť takého hodnotenia? Znenie v špecifikácii jazyka Java je trochu neurčité. Zhrnutie zmien pre J2SE 1.4 naznačuje, že nejasnosť bola nakoniec vyriešená v prospech úplného vyhodnotenia ľavého výrazu. Skvelé! Pretože kompilátor J2SE 1.4 sa javí ako najlepšia voľba, chcem ho použiť na všetky svoje programovania v Jave, aj keď je moja cieľová runtime platforma staršia verzia, len aby som mohol využívať tieto opravy a vylepšenia. (Upozorňujeme, že v čase písania tohto článku nie sú všetky aplikačné servery certifikované na platforme J2SE 1.4.)

Aj keď posledný príklad kódu bol trochu umelý, slúžil na ilustráciu. Medzi ďalšie dôvody na použitie nedávnej verzie J2SDK patrí snaha využívať výhody javadoc a ďalších vylepšení nástrojov.

Napokon, krížová kompilácia je celkom dobrý spôsob života vo vývojovom prostredí Java a vývoji hier Java.

Ahoj triedna hádanka vysvetlená

The Ahoj príkladom, ktorý začal tento článok, je príklad nesprávnej krížovej kompilácie. J2SE 1.4 pridal novú metódu do StringBuffer API: pridať (StringBuffer). Kedy sa javac rozhodne, ako preložiť pozdrav.append (kto) do bajtového kódu vyhľadá StringBuffer definícia triedy v bootstrap classpath a namiesto tejto metódy vyberie túto novú metódu pridať (objekt). Aj keď je zdrojový kód plne kompatibilný s jazykom Java 1.0, výsledný bajtový kód vyžaduje runtime J2SE 1.4.

Všimnite si, aké ľahké je urobiť túto chybu. Neexistujú žiadne varovania o kompilácii a chybu je možné zistiť iba za behu programu. Správny spôsob použitia javacu z J2SE 1.4 na generovanie Java 1.1 kompatibilného Ahoj trieda je:

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

Správne zaklínadlo javac obsahuje dve nové možnosti. Poďme preskúmať, čo robia a prečo sú potrebné.

Každá trieda Java má pečiatku verzie

Možno si to neuvedomujete, ale každý .trieda Súbor, ktorý vygenerujete, obsahuje pečiatku verzie: dve nepodpísané krátke celé čísla začínajúce na posunutí bajtu 4, hneď za 0xCAFEBABE magické číslo. Jedná sa o hlavné / vedľajšie čísla verzie formátu triedy (pozri špecifikáciu formátu súboru triedy) a majú okrem iných rozšírení aj význam pre túto definíciu formátu. Každá verzia platformy Java špecifikuje celý rad podporovaných verzií. Tu je tabuľka podporovaných rozsahov v tejto dobe písania (moja verzia tejto tabuľky sa mierne líši od údajov v dokumentoch spoločnosti Sun - rozhodol som sa odstrániť niektoré hodnoty rozsahu, ktoré sú relevantné iba pre extrémne staré verzie kompilátora spoločnosti Sun staršie ako verzie 1.0.2) :

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 odmietne načítať triedu, ak je známka verzie triedy mimo rozsahu podpory JVM. Z predchádzajúcej tabuľky si všimnite, že neskoršie JVM vždy podporujú celý rozsah verzií od úrovne predchádzajúcej verzie a tiež ho rozširujú.

Čo to pre vás ako vývojára Java znamená? Vzhľadom na schopnosť ovládať túto pečiatku verzie počas kompilácie môžete vynútiť minimálnu runtime verziu Java požadovanú vašou aplikáciou. To je presne to, čo -cieľ možnosť kompilátora áno. Tu je zoznam pečiatok verzie vydaných kompilátormi javac z rôznych súborov JDK / J2SDK predvolene (všimnite si, že J2SDK 1.4 je prvý J2SDK, kde javac mení svoj predvolený cieľ 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 tu je účinok špecifikácie rôznych -cieľs:

-cieľ 1,1: 45,3 -cieľ 1,2: 46,0 -cieľ 1,3: 47,0 -cieľ 1,4: 48,0 

Ako príklad slúži nasledujúci príklad: URL.getPath () metóda pridaná do J2SE 1.3:

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

Pretože tento kód vyžaduje minimálne J2SE 1.3, mal by som použiť -cieľ 1.3 pri jeho stavbe. Prečo nútiť mojich používateľov, aby sa s tým vyrovnali java.lang.NoSuchMethodError prekvapenia, ktoré sa vyskytnú, len keď omylom načítali triedu do 1,2 JVM? Iste, mohol by som dokument že moja aplikácia vyžaduje J2SE 1.3, ale bola by čistejšia a robustnejšia presadiť to isté na binárnej úrovni.

Nemyslím si, že postup stanovenia cieľového JVM sa pri vývoji podnikového softvéru veľmi často nepoužíva. Napísal som jednoduchú triedu úžitkovosti DumpClassVersions (k dispozícii pri stiahnutí tohto článku), ktorý dokáže skenovať súbory, archívy a adresáre s triedami Java a hlásiť všetky narazené pečiatky verzie triedy. Niektoré rýchle prechádzanie populárnych projektov s otvoreným zdrojovým kódom alebo dokonca základných knižníc z rôznych súborov JDK / J2SDK neukážu pre verzie triedy žiadny konkrétny systém.

Vyhľadávacie cesty triedy bootstrap a triedy rozšírenia

Pri preklade zdrojového kódu Java musí kompilátor poznať definíciu typov, ktoré ešte nevidel. Patria sem vaše triedy aplikácií a základné triedy ako java.lang.StringBuffer. Ako ste si istí, druhá trieda sa často používa na preklad výrazov obsahujúcich String zreťazenie a podobne.

Proces povrchne podobný normálnemu načítaniu aplikácií vyhľadáva definíciu triedy: najskôr v bootstrap classpath, potom v prípone classpath a nakoniec v triede používateľa (-classpath). Ak necháte všetko na predvolené hodnoty, uplatnia sa definície z J2SDK „domáceho“ javaca - čo nemusí byť správne, ako ukazuje Ahoj príklad.

Na prepísanie vyhľadávacích ciest triedy bootstrap a triedy rozšírenia použijete -bootclasspath a - ďalšie možnosti javacu, resp. Táto schopnosť dopĺňa -cieľ voľba v tom zmysle, že zatiaľ čo druhá nastavuje minimálnu požadovanú verziu JVM, prvá vyberie API základnej triedy dostupné pre vygenerovaný kód.

Pamätajte, že samotný javac bol napísaný v jazyku Java. Dve možnosti, ktoré som práve spomenul, ovplyvňujú vyhľadávanie triedy pre generovanie bajtového kódu. Oni robia nie ovplyvňujú bootstrap a rozšírené triedy ciest, ktoré používa JVM na vykonávanie javacu ako programu Java (ten je možné vykonať pomocou -J možnosť, ale je to dosť nebezpečné a vedie to k nepodporenému správaniu). Inými slovami, javac v skutočnosti nenačíta žiadne triedy -bootclasspath a - ďalšie; odkazuje iba na ich definície.

Vďaka novo získanému porozumeniu pre podporu krížovej kompilácie javacu sa pozrime, ako to možno využiť v praktických situáciách.

Scenár 1: Zacielenie na jednu základnú platformu J2SE

Toto je veľmi častý prípad: vašu aplikáciu podporuje niekoľko verzií J2SE, a tak sa stane, že môžete všetko implementovať cez základné API určitého (budem to nazývať základňa) Verzia platformy J2SE. O zvyšok sa stará kompatibilita smerom nahor. Aj keď je J2SE 1.4 najnovšia a najlepšia verzia, nevidíte dôvod vylúčiť používateľov, ktorí J2SE 1.4 zatiaľ nemôžu spustiť.

Ideálny spôsob, ako zostaviť svoju aplikáciu, je:

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

Áno, znamená to, že vo svojom zostavovacom stroji budete musieť pravdepodobne použiť dve rôzne verzie J2SDK: tú, ktorú si vyberiete pre svoj javac, a tú, ktorá je vašou základňou podporovanou platformou J2SE. Zdá sa to ako dodatočné nastavenie, ale za robustné zostavenie je to v skutočnosti malá cena. Kľúčom je výslovné ovládanie známok verzie triedy aj triedy bootstrap triedy a nespoliehanie sa na predvolené hodnoty. Použi -verbose možnosť overiť, odkiaľ pochádzajú definície základných tried.

Ako vedľajší komentár spomeniem, že je bežné vidieť vývojárov zahrnutých rt.jar z ich J2SDK na -classpath riadok (to mohol byť zvyk z JDK 1,1 dňa, keď ste museli pridať triedy.zip do kompilačnej triedy). Ak ste sledovali diskusiu vyššie, teraz chápete, že je to úplne zbytočné a v najhoršom prípade by to mohlo narušiť správne poradie vecí.

Scenár 2: Prepnite kód na základe verzie Java zistenej za behu programu

Tu chcete byť sofistikovanejší ako v scenári 1: Máte verziu platformy Java podporovanú na základe základne, ale ak by váš kód bežal vo vyššej verzii Java, radšej by ste využili novšie API. Môžete si napríklad vystačiť s java.io. * API, ale nevadilo by im mať úžitok java.nio. * vylepšenia v novšom JVM, ak sa naskytne príležitosť.

V tomto scenári sa základný prístup kompilácie podobá prístupu scenára 1, s výnimkou, že bootstrap J2SDK by mal byť najvyššia verziu, ktorú musíte použiť:

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

To však nestačí; vo svojom kóde Java musíte tiež urobiť niečo chytré, aby v rôznych verziách J2SE postupovalo správne.

Jednou z alternatív je použitie Java preprocesora (minimálne s # ifdef / # else / # endif podpora) a v skutočnosti generovať rôzne zostavenia pre rôzne verzie platformy J2SE. Aj keď J2SDK nemá dostatočnú podporu predspracovania, na webe o takéto nástroje nie je núdza.

Správa niekoľkých distribúcií pre rôzne platformy J2SE je však vždy ďalšou záťažou. S trochou predvídavosti sa môžete dostať preč s distribúciou jednej zostavy vašej aplikácie. Tu je príklad toho, ako to urobiť (URLTest1 je jednoduchá trieda, ktorá extrahuje rôzne zaujímavé bity z adresy URL):

$config[zx-auto] not found$config[zx-overlay] not found