Programmering

Hvilken version er din Java-kode?

23. maj 2003

Spørgsmål:

EN:

offentlig klasse Hej {offentlig statisk tomrum hoved (String [] args) {StringBuffer hilsen = ny StringBuffer ("hej,"); StringBuffer who = new StringBuffer (args [0]). Append ("!"); hilsen. tilføj (hvem); System.out.println (hilsen); }} // Afslutning af undervisningen 

Først virker spørgsmålet ret trivielt. Så lidt kode er involveret i Hej klasse, og hvad der end er, bruger kun funktionalitet, der går tilbage til Java 1.0. Så klassen skulle køre i næsten enhver JVM uden problemer, ikke?

Vær ikke så sikker. Kompilér det ved hjælp af javac fra Java 2 Platform, Standard Edition (J2SE) 1.4.1, og kør det i en tidligere version af Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Undtagelse i tråden "main" java.lang.NoSuchMethodError at Hello.main (Hello.java:20 ) 

I stedet for det forventede "hej, verden!" Kaster denne kode en runtime-fejl, selvom kilden er 100 procent Java 1.0-kompatibel! Og fejlen er heller ikke nøjagtigt, hvad du kunne forvente: i stedet for en klasseversion-uoverensstemmelse klager den på en eller anden måde over en manglende metode. Forvirret? I så fald finder du den fulde forklaring senere i denne artikel. Lad os først udvide diskussionen.

Hvorfor gider med forskellige Java-versioner?

Java er ret platformuafhængigt og for det meste opad kompatibelt, så det er almindeligt at kompilere et stykke kode ved hjælp af en given J2SE-version og forvente, at den fungerer i senere JVM-versioner. (Java-syntaksændringer forekommer normalt uden væsentlige ændringer i byte-kodeinstruktionssættet.) Spørgsmålet i denne situation er: Kan du oprette en slags base Java-version, der understøttes af din kompilerede applikation, eller er standard compiler-opførsel acceptabel? Jeg vil forklare min anbefaling senere.

En anden forholdsvis almindelig situation er at bruge en kompiler med højere version end den tiltænkte implementeringsplatform. I dette tilfælde bruger du ikke nogen nyligt tilføjede API'er, men vil bare drage fordel af værktøjsforbedringer. Se på dette kodestykke, og prøv at gætte, hvad det skal gøre ved kørsel:

public class ThreadSurprise {public static void main (String [] args) throw Exception {Thread [] threads = new Thread [0]; tråde [-1] .sove (1); // Skal dette kaste? }} // Afslutning af undervisningen 

Skulle denne kode kaste et ArrayIndexOutOfBoundsException eller ikke? Hvis du kompilerer Trådoverraskelse ved hjælp af forskellige Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit) versioner, vil adfærd ikke være konsistent:

  • Version 1.1 og tidligere kompilatorer genererer kode, der ikke kaster
  • Version 1.2 kaster
  • Version 1.3 kaster ikke
  • Version 1.4 kaster

Det subtile punkt her er det Thread.sleep () er en statisk metode og behøver ikke en Tråd eksempel overhovedet. Stadig kræver Java Language Specification, at compileren ikke kun udleder målklassen fra venstre udtryk for tråde [-1] .sove (1);, men evaluer også selve udtrykket (og kassér resultatet af en sådan evaluering). Henviser til indeks -1 af tråde række af en sådan evaluering? Formuleringen i Java Language Specification er noget vag. Resuméet af ændringer for J2SE 1.4 indebærer, at tvetydigheden endelig blev løst til fordel for at evaluere venstrehåndsudtrykket fuldt ud. Store! Da J2SE 1.4-kompilatoren virker som det bedste valg, vil jeg bruge den til al min Java-programmering, selvom min målkørselsplatform er en tidligere version, bare for at drage fordel af sådanne rettelser og forbedringer. (Bemærk at i skrivende stund ikke alle applikationsservere er certificeret på J2SE 1.4-platformen.)

Selvom det sidste kodeeksempel var noget kunstigt, tjente det til at illustrere et punkt. Andre grunde til at bruge en nylig J2SDK-version inkluderer ønske om at drage fordel af javadoc og andre værktøjsforbedringer.

Endelig er krydskompilering en ganske livsstil i integreret Java-udvikling og Java-spiludvikling.

Hej klassepuslespil forklaret

Det Hej eksempel, der startede denne artikel, er et eksempel på forkert krydskompilering. J2SE 1.4 tilføjede en ny metode til StringBuffer API: tilføj (StringBuffer). Når javac beslutter, hvordan man skal oversætte hilsen. tilføj (hvem) i byte kode, ser det op på StringBuffer klassedefinition i bootstrap classpath og vælger denne nye metode i stedet for tilføje (Objekt). Selvom kildekoden er fuldt Java 1.0-kompatibel, kræver den resulterende bytekode en J2SE 1.4-kørselstid.

Bemærk, hvor let det er at begå denne fejl. Der er ingen kompileringsadvarsler, og fejlen kan kun registreres ved kørsel. Den korrekte måde at bruge javac fra J2SE 1.4 til at generere en Java 1.1-kompatibel Hej klasse er:

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

Den korrekte javac-besværgelse indeholder to nye muligheder. Lad os undersøge, hvad de gør, og hvorfor de er nødvendige.

Hver Java-klasse har et versionstempel

Du er måske ikke opmærksom på det, men alle .klasse fil, du genererer, indeholder et versionstempel: to usignerede korte heltal, der starter ved byteoffset 4, lige efter 0xCAFEBABE magisk nummer. De er de største / mindre versionsnumre i klasseformatet (se Specifikation for klassefilformat), og de har nytte ud over at være udvidelsespunkter til denne formatdefinition. Hver version af Java-platformen specificerer en række understøttede versioner. Her er tabellen med understøttede områder på dette tidspunkt for skrivning (min version af denne tabel adskiller sig lidt fra data i Suns dokumenter - jeg valgte at fjerne nogle rækkeværdier, der kun er relevante for ekstremt gamle (før 1.0.2) versioner af Sun's compiler) :

Java 1.1-platform: 45.3-45.65535 Java 1.2-platform: 45.3-46.0 Java 1.3-platform: 45.3-47.0 Java 1.4-platform: 45.3-48.0 

En kompatibel JVM vil nægte at indlæse en klasse, hvis klassens versionstempel er uden for JVM's supportområde. Bemærk fra den foregående tabel, at senere JVM'er altid understøtter hele versionen fra det forrige versionniveau og også udvider det.

Hvad betyder dette for dig som Java-udvikler? I betragtning af muligheden for at kontrollere dette versionstempel under kompilering kan du håndhæve den minimum Java-runtime-version, der kræves af din applikation. Dette er netop hvad -mål kompilator mulighed gør. Her er en liste over versionstempler udsendt af javac-kompilatorer fra forskellige JDK'er / J2SDK'er som standard (bemærk, at J2SDK 1.4 er den første J2SDK, hvor javac ændrer sit standardmål fra 1.1 til 1.2):

JDK 1.1: 45.3 J2SDK 1.2: 45.3 J2SDK 1.3: 45.3 J2SDK 1.4: 46.0 

Og her er effekten af ​​at specificere forskellige -måls:

-mål 1.1: 45.3-mål 1.2: 46.0-mål 1.3: 47.0-mål 1.4: 48.0 

Som et eksempel bruger følgende URL.getPath () metode tilføjet i J2SE 1.3:

 URL url = ny URL ("//www.javaworld.com/column/jw-qna-index.shtml"); System.out.println ("URL-sti:" + url.getPath ()); 

Da denne kode kræver mindst J2SE 1.3, skal jeg bruge -mål 1.3 når du bygger det. Hvorfor tvinge mine brugere til at håndtere java.lang.NoSuchMethodError overraskelser, der kun opstår, når de fejlagtigt har indlæst klassen i en 1.2 JVM? Sikker på, det kunne jeg dokument at min ansøgning kræver J2SE 1.3, men det ville være renere og mere robust at håndhæve det samme på binært niveau.

Jeg tror ikke, at fremgangsmåden med at indstille målet JVM er meget udbredt i softwareudvikling af virksomheder. Jeg skrev en simpel hjælpeklasse DumpClassVersions (tilgængelig med denne artikels download), der kan scanne filer, arkiver og mapper med Java-klasser og rapportere alle stødte klasseversionstempler. Nogle hurtige browsing af populære open source-projekter eller endda kernebiblioteker fra forskellige JDK'er / J2SDK'er viser intet særligt system til klasseversioner.

Bootstrap- og udvidelsesklasseopslagsstier

Ved oversættelse af Java-kildekode skal compileren kende definitionen af ​​typer, den endnu ikke har set. Dette inkluderer dine applikationsklasser og kerneklasser som java.lang.StringBuffer. Som jeg er sikker på, at du er opmærksom på, bruges sidstnævnte klasse ofte til at oversætte udtryk, der indeholder Snor sammenkædning og lignende.

En proces, der overfladisk svarer til normal applikationsklassebelastning, ser en klassedefinition op: først i bootstrap-klassestien, derefter udvidelsesklassestien og endelig i brugerens klassesti (-klassesti). Hvis du overlader alt til standardindstillingerne, vil definitionerne fra "hjem" javacs J2SDK træde i kraft - hvilket muligvis ikke er korrekt, som vist af Hej eksempel.

For at tilsidesætte opslagsstier til bootstrap og udvidelsesklasse bruger du -stienklassesti og -extdirs javac-indstillinger, henholdsvis. Denne evne supplerer -mål mulighed i den forstand, at mens sidstnævnte indstiller den minimum krævede JVM-version, vælger førstnævnte de centrale klasse API'er, der er tilgængelige for den genererede kode.

Husk, at javac selv blev skrevet på Java. De to muligheder, jeg lige har nævnt, påvirker klassesøgningen til byte-kodegenerering. De gør ikke påvirke bootstrap- og udvidelsesklassestier, der bruges af JVM til at udføre javac som et Java-program (sidstnævnte kan gøres via -J mulighed, men det er ret farligt at gøre det og resulterer i adfærd, der ikke understøttes). For at sige det på en anden måde indlæser javac faktisk ikke nogen klasser fra -stienklassesti og -extdirs; det refererer kun til deres definitioner.

Med den nyligt erhvervede forståelse for javacs understøttelse af kryds-kompilering, lad os se, hvordan dette kan bruges i praktiske situationer.

Scenarie 1: Målret mod en enkelt base J2SE-platform

Dette er en meget almindelig sag: flere J2SE-versioner understøtter din applikation, og det sker så, at du kan implementere alt via kernen API'er af en bestemt (jeg vil kalde det grundlag) J2SE-platformversion. Opadgående kompatibilitet tager sig af resten. Selvom J2SE 1.4 er den nyeste og bedste version, ser du ingen grund til at udelukke brugere, der endnu ikke kan køre J2SE 1.4.

Den ideelle måde at kompilere din ansøgning på er:

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

Ja, dette betyder, at du muligvis skal bruge to forskellige J2SDK-versioner på din build-maskine: den, du vælger til dens javac, og den, der er din basestøttede J2SE-platform. Dette virker som en ekstra installationsindsats, men det er faktisk en lille pris at betale for en robust build. Nøglen her er eksplicit at kontrollere både klasseversionens stempler og bootstrap-klassestien og ikke stole på standardindstillinger. Brug -ordrig mulighed for at kontrollere, hvor kerneklassedefinitioner kommer fra.

Som en sidekommentar vil jeg nævne, at det er almindeligt at se udviklere inkludere rt.jar fra deres J2SDK'er på -klassesti linje (dette kunne være en vane fra JDK 1.1 dage, hvor du skulle tilføje classes.zip til kompilations klassestien). Hvis du fulgte diskussionen ovenfor, forstår du nu, at dette er helt overflødigt og i værste fald kan forstyrre den rette rækkefølge.

Scenarie 2: Skift kode baseret på den Java-version, der blev fundet ved kørsel

Her vil du være mere sofistikeret end i Scenario 1: Du har en base-understøttet Java-platformversion, men hvis din kode kører i en højere Java-version, foretrækker du at udnytte nyere API'er. For eksempel kan du klare dig med java.io. * API'er, men har ikke noget imod at drage fordel af java.nio. * forbedringer i en nyere JVM, hvis muligheden byder sig.

I dette scenarie ligner den basale kompileringsmetode Scenario 1's tilgang, bortset fra at din bootstrap J2SDK skal være højest version, du skal bruge:

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

Dette er dog ikke nok; du skal også gøre noget klogt i din Java-kode, så det gør det rigtige i forskellige J2SE-versioner.

Et alternativ er at bruge en Java preprocessor (med mindst # ifdef / # else / # endif support) og genererer faktisk forskellige builds til forskellige J2SE-platformversioner. Selvom J2SDK mangler korrekt understøttelse af forbehandling, er der ingen mangel på sådanne værktøjer på nettet.

At administrere flere distributioner til forskellige J2SE-platforme er dog altid en ekstra byrde. Med lidt ekstra fremsyn kan du slippe af med at distribuere en enkelt version af din applikation. Her er et eksempel på, hvordan man gør det (URLTest1 er en simpel klasse, der udtrækker forskellige interessante bits fra en URL):