Programmering

Hvilken versjon er Java-koden din?

23. mai 2003

Spørsmål:

EN:

offentlig klasse Hello {public static void main (String [] args) {StringBuffer greeting = new StringBuffer ("hallo,"); StringBuffer who = new StringBuffer (args [0]). Append ("!"); hilsen. vedlegg (hvem); System.out.println (hilsen); }} // Slutten på timen 

Først virker spørsmålet ganske trivielt. Så lite kode er involvert i Hallo klasse, og hva det enn er, bruker bare funksjonalitet som går tilbake til Java 1.0. Så klassen skal kjøre i omtrent hvilken som helst JVM uten problemer, ikke sant?

Ikke vær så sikker. Kompilere den ved hjelp av javac fra Java 2 Platform, Standard Edition (J2SE) 1.4.1 og kjør den i en tidligere versjon av Java Runtime Environment (JRE):

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

I stedet for det forventede "hei, verden!", Kaster denne koden en kjøretidsfeil selv om kilden er 100 prosent Java 1.0-kompatibel! Og feilen er ikke akkurat det du kanskje forventer heller: i stedet for en klasseversjon som ikke samsvarer, klager den på en eller annen måte om en manglende metode. Forvirret? I så fall finner du den fulle forklaringen senere i denne artikkelen. La oss først utvide diskusjonen.

Hvorfor bry seg med forskjellige Java-versjoner?

Java er ganske plattformuavhengig og stort sett oppoverkompatibelt, så det er vanlig å kompilere et stykke kode ved hjelp av en gitt J2SE-versjon og forvente at den skal fungere i senere JVM-versjoner. (Java-syntaksendringer oppstår vanligvis uten vesentlige endringer i settet med bytekodeinstruksjoner.) Spørsmålet i denne situasjonen er: Kan du etablere en slags base Java-versjon som støttes av det kompilerte programmet ditt, eller er standard kompilatoradferd akseptabel? Jeg vil forklare min anbefaling senere.

En annen ganske vanlig situasjon er å bruke en kompilator med høyere versjon enn den tiltenkte distribusjonsplattformen. I dette tilfellet bruker du ikke APIer som nylig er lagt til, men vil bare dra nytte av verktøyforbedringer. Se på denne kodebiten og prøv å gjette hva den skal gjøre under kjøretiden:

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

Skulle denne koden kaste en ArrayIndexOutOfBoundsException eller ikke? Hvis du kompilerer Trådoverraskelse ved å bruke forskjellige Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit) versjoner, vil oppførselen ikke være konsekvent:

  • Versjon 1.1 og tidligere kompilatorer genererer kode som ikke kaster
  • Versjon 1.2 kaster
  • Versjon 1.3 kaster ikke
  • Versjon 1.4 kaster

Det subtile punktet her er at Thread.sleep () er en statisk metode og trenger ikke en Tråd i det hele tatt. Likevel krever Java språkspesifikasjon at kompilatoren ikke bare utleder målklassen fra venstre uttrykk for tråder [-1] .sove (1);, men vurder også selve uttrykket (og kast resultatet av en slik evaluering). Henviser til indeks -1 av tråder rekke deler av en slik evaluering? Ordlyden i Java Language Specification er noe vag. Sammendraget av endringene for J2SE 1.4 innebærer at uklarheten til slutt ble løst til fordel for å evaluere venstreekspresjonen fullt ut. Flott! Siden J2SE 1.4-kompilatoren virker som det beste valget, vil jeg bruke den til alle Java-programmeringene mine, selv om målruntidsplattformen min er en tidligere versjon, bare for å dra nytte av slike reparasjoner og forbedringer. (Merk at i skrivende stund ikke alle applikasjonsservere er sertifiserte på J2SE 1.4-plattformen.)

Selv om det siste kodeeksemplet var noe kunstig, tjente det til å illustrere et poeng. Andre grunner til å bruke en nylig J2SDK-versjon inkluderer å ønske å dra nytte av javadoc og andre verktøyforbedringer.

Til slutt er kryss-kompilering ganske en livsstil i innebygd Java-utvikling og Java-spillutvikling.

Hei klassepuslespill forklart

De Hallo eksempel som startet denne artikkelen er et eksempel på feil krysssamling. J2SE 1.4 la til en ny metode for StringBuffer API: legge til (StringBuffer). Når javac bestemmer hvordan man skal oversette hilsen. vedlegg (hvem) i byte-kode, ser den opp StringBuffer klassedefinisjon i bootstrap classpath og velger denne nye metoden i stedet for legge til (Objekt). Selv om kildekoden er fullt Java 1.0-kompatibel, krever den resulterende bytekoden en J2SE 1.4-kjøretid.

Legg merke til hvor enkelt det er å gjøre denne feilen. Det er ingen advarsler om kompilering, og feilen kan bare oppdages ved kjøretid. Den riktige måten å bruke javac fra J2SE 1.4 til å generere en Java 1.1-kompatibel Hallo klassen er:

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

Den riktige javac-besvergelsen inneholder to nye alternativer. La oss undersøke hva de gjør og hvorfor de er nødvendige.

Hver Java-klasse har et versjonsstempel

Du er kanskje ikke klar over det, men alle .klasse filen du genererer inneholder et versjonsstempel: to usignerte korte heltall som starter ved byteforskyvning 4, rett etter 0xCAFEBABE magisk nummer. De er hoved- / mindreversjonsnumrene til klasseformatet (se Klassefilformatsspesifikasjonen), og de har verktøy i tillegg til bare å være utvidelsespunkter for denne formatdefinisjonen. Hver versjon av Java-plattformen spesifiserer en rekke støttede versjoner. Her er tabellen over støttede områder på dette tidspunktet for skriving (min versjon av denne tabellen skiller seg litt fra dataene i Suns dokumenter - jeg valgte å fjerne noen områdeverdier som bare er relevante for ekstremt gamle (pre-1.0.2) versjoner av Suns kompilator) :

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

En kompatibel JVM vil nekte å laste inn en klasse hvis klassens versjonsstempel er utenfor JVMs støtteområde. Legg merke til fra forrige tabell at senere JVM-er alltid støtter hele versjonsområdet fra forrige versjonsnivå og utvider det også.

Hva betyr dette for deg som Java-utvikler? Gitt muligheten til å kontrollere dette versjonsstempelet under kompilering, kan du håndheve den minste Java-kjøretidsversjonen som kreves av applikasjonen din. Dette er nettopp hva -mål kompilatoralternativet gjør. Her er en liste over versjonsstempler utgitt av javac-kompilatorer fra forskjellige JDKer / J2SDKer som standard (observer at J2SDK 1.4 er den første J2SDK der javac endrer standardmålet 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 av å spesifisere forskjellige -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 bruker følgende URL.getPath () metoden lagt til i J2SE 1.3:

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

Siden denne koden krever minst J2SE 1.3, bør jeg bruke den -mål 1.3 når du bygger den. Hvorfor tvinge brukerne mine til å håndtere java.lang.NoSuchMethodError overraskelser som bare oppstår når de feilaktig har lastet klassen i en 1.2 JVM? Jada, det kunne jeg dokument at søknaden min krever J2SE 1.3, men den vil være renere og mer robust å håndheve det samme på binært nivå.

Jeg tror ikke at bruken av å sette målet JVM er mye brukt i programvareutvikling. Jeg skrev en enkel nytteklasse DumpClassVersions (tilgjengelig med nedlasting av denne artikkelen) som kan skanne filer, arkiver og kataloger med Java-klasser og rapportere alle stempelversjonstempler. Noen raske bla gjennom populære open source-prosjekter eller til og med kjernebiblioteker fra forskjellige JDK / J2SDK vil ikke vise noe spesielt system for klasseversjoner.

Oppstartsstier for bootstrap og utvidelsesklasse

Når du oversetter Java-kildekode, må kompilatoren vite definisjonen av typer den ennå ikke har sett. Dette inkluderer applikasjonsklasser og kjerneklasser som java.lang.StringBuffer. Som jeg er sikker på at du er klar over, brukes sistnevnte klasse ofte til å oversette uttrykk som inneholder String sammenkobling og lignende.

En prosess som er overfladisk lik normalbelastning av applikasjoner, ser opp en klassedefinisjon: først i bootstrap-klassestien, deretter utvidelsesklassen og til slutt i brukerens klassesti (-klassesti). Hvis du overlater alt til standardinnstillingene, vil definisjonene fra "hjemmet" Javacs J2SDK tre i kraft - som kanskje ikke stemmer, som vist av Hallo eksempel.

For å overstyre oppstartsstiene for bootstrap og utvidelsesklasse, bruker du -bootclasspath og -extdirs javac-alternativer, henholdsvis. Denne evnen utfyller -mål alternativ i den forstand at mens sistnevnte setter den minste nødvendige JVM-versjonen, velger førstnevnte kjerneklasse-API-er som er tilgjengelige for den genererte koden.

Husk at javac i seg selv ble skrevet på Java. De to alternativene jeg nettopp nevnte, påvirker klassesøket for byte-kodegenerering. De gjør ikke påvirke bootstrap- og utvidelsesklassestiene som brukes av JVM for å utføre javac som et Java-program (sistnevnte kan gjøres via -J alternativet, men å gjøre det er ganske farlig og resulterer i atferd som ikke støttes). For å si det på en annen måte, laster javac faktisk ikke noen klasser fra -bootclasspath og -extdirs; det refererer bare til definisjonene deres.

Med den nyervervede forståelsen for javacs støtte for krysssamling, la oss se hvordan dette kan brukes i praktiske situasjoner.

Scenario 1: Målrett mot en enkelt base J2SE-plattform

Dette er en veldig vanlig sak: flere J2SE-versjoner støtter applikasjonen din, og det skjer slik at du kan implementere alt via kjerne-API-er for en bestemt (jeg vil kalle det utgangspunkt) J2SE plattformversjon. Oppoverkompatibilitet tar seg av resten. Selv om J2SE 1.4 er den nyeste og beste versjonen, ser du ingen grunn til å ekskludere brukere som ikke kan kjøre J2SE 1.4 ennå.

Den ideelle måten å kompilere søknaden din på er:

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

Ja, dette innebærer at du kanskje må bruke to forskjellige J2SDK-versjoner på byggemaskinen din: den du velger for javacen og den som er din basestøttede J2SE-plattform. Dette virker som ekstra installasjonsinnsats, men det er faktisk en liten pris å betale for en robust bygging. Nøkkelen her er eksplisitt å kontrollere både klasseversjonsstemplene og bootstrap classpath og ikke stole på standardinnstillinger. Bruke -verbose alternativ for å verifisere hvor kjerneklassedefinisjoner kommer fra.

Som en sidekommentar vil jeg nevne at det er vanlig å se utviklere inkludere rt.jar fra J2SDK-ene deres på -klassesti linje (dette kan være en vane fra JDK 1.1 dager da du måtte legge til classes.zip til samlekursstien). Hvis du fulgte diskusjonen ovenfor, forstår du nå at dette er fullstendig overflødig, og i verste fall kan forstyrre riktig rekkefølge.

Scenario 2: Bytt kode basert på Java-versjonen oppdaget ved kjøretid

Her vil du være mer sofistikert enn i Scenario 1: Du har en basestøttet Java-plattformversjon, men skulle koden din kjøre i en høyere Java-versjon, foretrekker du å utnytte nyere API-er. For eksempel kan du klare deg med java.io. * APIer, men ville ikke ha noe imot å dra nytte av java.nio. * forbedringer i en nyere JVM hvis muligheten byr seg.

I dette scenariet ligner den grunnleggende kompileringsmetoden Scenario 1s tilnærming, bortsett fra at bootstrap J2SDK skal være høyest versjon du trenger å bruke:

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

Dette er imidlertid ikke nok; du må også gjøre noe smart i Java-koden din, slik at den gjør det rette i forskjellige J2SE-versjoner.

Et alternativ er å bruke en Java-prosessor (med minst # ifdef / # else / # endif support) og genererer faktisk forskjellige bygg for forskjellige J2SE-plattformversjoner. Selv om J2SDK mangler riktig forhåndsbehandlingsstøtte, er det ingen mangel på slike verktøy på nettet.

Å administrere flere distribusjoner for forskjellige J2SE-plattformer er imidlertid alltid en ekstra byrde. Med litt ekstra framsyn kan du komme unna med å distribuere en enkelt versjon av applikasjonen din. Her er et eksempel på hvordan du gjør det (URLTest1 er en enkel klasse som trekker ut forskjellige interessante biter fra en URL):

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