Koja je inačica vašeg Java koda?

23. svibnja 2003

P:

O:

javna klasa Pozdrav {public static void main (String [] args) {StringBuffer pozdrav = novi StringBuffer ("zdravo,"); StringBuffer koji = novi StringBuffer (args [0]). Append ("!"); pozdrav.append (who); System.out.println (pozdrav); }} // Kraj nastave

Isprva se pitanje čini prilično trivijalnim. Tako je malo koda uključeno u Helloklasu, a sve što postoji koristi samo funkcije koje potječu iz Jave 1.0. Dakle, nastava bi se trebala izvoditi na gotovo bilo kojem JVM-u bez problema, zar ne?

Ne budi tako siguran. Sastavite ga koristeći javac iz Java 2 Platform, Standard Edition (J2SE) 1.4.1 i pokrenite ga u starijoj verziji Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Iznimka u niti "main" java.lang.NoSuchMethodError at Hello.main (Hello.java:20 ) 

Umjesto očekivanog "bok, svijete!", Ovaj kod baca pogrešku u izvođenju iako je izvor 100-postotni Java 1.0 kompatibilan! A pogreška nije točno ono što biste mogli očekivati: umjesto neusklađenosti verzije klase, nekako se žali na metodu koja nedostaje. Zbunjen? Ako je tako, cjelovito objašnjenje pronaći ćete kasnije u ovom članku. Prvo, proširimo raspravu.

Zašto se mučiti s različitim inačicama Jave?

Java je prilično neovisna o platformi i uglavnom je kompatibilna prema gore, pa je uobičajeno kompajlirati komad koda pomoću zadane verzije J2SE i očekivati ​​da će raditi u kasnijim JVM verzijama. (Izmjene sintakse Java obično se događaju bez značajnih promjena u skupu uputa bajt koda.) Pitanje je u ovoj situaciji: Možete li uspostaviti neku osnovnu inačicu Jave koju podržava vaša kompajlirana aplikacija ili je zadano ponašanje kompajlera prihvatljivo? Objasnit ću svoju preporuku kasnije.

Sljedeća prilično česta situacija je uporaba kompatibilnijeg verzija s verzijom od predviđene platforme za implementaciju. U ovom slučaju ne upotrebljavate nedavno dodane API-je, već samo želite imati koristi od poboljšanja alata. Pogledajte ovaj isječak koda i pokušajte pogoditi što bi trebao raditi u vrijeme izvođenja:

javna klasa ThreadSurprise {public static void main (String [] args) baca iznimku {Thread [] thread = new Thread [0]; niti [-1] .sleep (1); // Treba li ovo baciti? }} // Kraj nastave

Treba li ovaj kod baciti ArrayIndexOutOfBoundsExceptionili ne? Ako kompajlirate ThreadSurprisepomoću različitih verzija Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit), ponašanje neće biti dosljedno:

  • Prevoditelji verzije 1.1 i stariji generiraju kod koji se ne baca
  • Verzija 1.2 baca
  • Verzija 1.3 ne baca
  • Verzija 1.4 baca

Suptilna poanta ovdje je da Thread.sleep()je to statična metoda i da joj uopće nije potrebna Threadinstanca. Ipak, Specifikacija jezika Java zahtijeva od prevoditelja da ne samo zaključi ciljnu klasu iz lijevog izraza threads [-1].sleep (1);, već i procijeni sam izraz (i odbaci takav rezultat procjene). Referencira indeks -1 datotekethreadsdio takve procjene? Tekst u Specifikaciji jezika Java donekle je nejasan. Sažetak promjena za J2SE 1.4 implicira da je nejasnoća konačno razriješena u korist cjelovite procjene lijevog izraza. Sjajno! Budući da se kompajler J2SE 1.4 čini najboljim izborom, želim ga koristiti za sve svoje Java programiranje, čak i ako je moja ciljana runtime platforma ranija verzija, samo da bih imao koristi od takvih popravaka i poboljšanja. (Imajte na umu da u vrijeme pisanja ovog članka nisu svi aplikacijski poslužitelji certificirani na platformi J2SE 1.4.)

Iako je posljednji primjer koda bio pomalo umjetan, poslužio je kao ilustracija. Ostali razlozi za upotrebu nedavne verzije J2SDK uključuju želju da se iskoriste javadoc i druga poboljšanja alata.

Konačno, unakrsna kompilacija prilično je način života u ugrađenom Java razvoju i razvoju Java igara.

Objašnjena slagalica klase

HelloPrimjer koji je započeo ovaj članak je primjer pogrešnog cross-kompilaciju. J2SE 1.4 dodao novu metodu za StringBufferAPI: append(StringBuffer). Kada javac odluči kako prevesti greeting.append (who)u bajt kôd, traži StringBufferdefiniciju klase u bootstrap stazi i odabire ovu novu metodu umjesto append(Object). Iako je izvorni kod u potpunosti kompatibilan s Java 1.0, rezultirajući bajt kôd zahtijeva vrijeme izvođenja J2SE 1.4.

Primijetite kako je lako napraviti ovu pogrešku. Nema upozorenja o kompilaciji, a pogreška se može otkriti samo tijekom izvođenja. Ispravan način upotrebe javaca iz J2SE 1.4 za generiranje Helloklase kompatibilne s Java 1.1 je:

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

Ispravna inkavcija javac sadrži dvije nove mogućnosti. Ispitajmo što rade i zašto su potrebni.

Svaka klasa Java ima pečat verzije

Možda toga niste svjesni, ali svaka .classdatoteka koju generirate sadrži pečat verzije: dvije nepotpisane kratke cjelobrojne vrijednosti koje počinju s pomakom bajta 4, odmah nakon 0xCAFEBABEčarobnog broja. Oni su glavni / manji brojevi verzija formata klase (pogledajte Specifikaciju formata datoteke klase), a osim što su točke produženja za ovu definiciju formata, imaju i korisnost. Svaka verzija Java platforme navodi niz podržanih verzija. Evo tablice podržanih raspona u ovom trenutku pisanja (moja verzija ove tablice malo se razlikuje od podataka u Sunčevim dokumentima - odlučio sam ukloniti neke vrijednosti raspona relevantne samo za izuzetno stare (pre-1.0.2) verzije Sun-ovog kompajlera) :

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 

Sukladni JVM odbit će učitavanje klase ako je pečat verzije klase izvan dosega podrške JVM-a. Primijetite iz prethodne tablice da kasniji JVM-ovi uvijek podržavaju cijeli raspon verzija s prethodne razine verzije i također ga proširuju.

Što ovo znači za vas kao Java programera? S obzirom na mogućnost upravljanja ovim pečatom verzije tijekom kompilacije, možete primijeniti minimalnu verziju Java runtimea koju zahtijeva vaša aplikacija. Upravo to čini -targetopcija kompajlera. Evo popisa oznaka verzije koje su javački kompajleri emitirali iz različitih JDK-ova / J2SDK-a prema zadanim postavkama (imajte na umu da je J2SDK 1.4 prvi J2SDK u kojem javac mijenja zadani cilj s 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 

I ovdje je učinak specificiranja raznih -targets:

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

Kao primjer, sljedeći koristi URL.getPath()metodu dodanu u J2SE 1.3:

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

Budući da ovaj kod zahtijeva najmanje J2SE 1.3, trebao bih ga koristiti -target 1.3prilikom izrade. Zašto prisiljavati moje korisnike da se bave java.lang.NoSuchMethodErroriznenađenjima koja se događaju samo ako su greškom učitali klasu u 1.2 JVM? Svakako, mogao bih dokumentirati da moja aplikacija zahtijeva J2SE 1.3, ali bilo bi čistije i robusnije da se ista primijeni na binarnoj razini.

Mislim da se praksa postavljanja ciljnog JVM-a ne koristi široko u razvoju softvera za poduzeća. Napisao sam jednostavnu uslužnu klasu DumpClassVersions(dostupnu uz preuzimanje ovog članka) koja može skenirati datoteke, arhive i direktorije s Java klasama i izvještavati o svim naiđenim pečatima verzije klase. Neko brzo pregledavanje popularnih projekata otvorenog koda ili čak temeljnih knjižnica iz različitih JDK-ova / J2SDK-ova neće pokazati poseban sustav za verzije klasa.

Putovi pretraživanja klase pokretanja i proširenja

Prilikom prevođenja Java izvornog koda, sastavljač mora znati definiciju tipova koje još nije vidio. To uključuje vaše klase aplikacija i osnovne klase poput java.lang.StringBuffer. Kao što sam siguran da ste svjesni, potonja klasa često se koristi za prevođenje izraza koji sadrže Stringspajanje i slično.

A process superficially similar to normal application classloading looks up a class definition: first in the bootstrap classpath, then the extension classpath, and finally in the user classpath (-classpath). If you leave everything to the defaults, the definitions from the "home" javac's J2SDK will take effect—which may not be correct, as shown by the Hello example.

To override the bootstrap and extension class lookup paths, you use -bootclasspath and -extdirs javac options, respectively. This ability complements the -target option in the sense that while the latter sets the minimum required JVM version, the former selects the core class APIs available to the generated code.

Remember that javac itself was written in Java. The two options I just mentioned affect the class lookup for byte-code generation. They do not affect the bootstrap and extension classpaths used by the JVM to execute javac as a Java program (the latter could be done via the -J option, but doing that is quite dangerous and results in unsupported behavior). To put it another way, javac does not actually load any classes from -bootclasspath and -extdirs; it merely references their definitions.

With the newly acquired understanding for javac's cross-compilation support, let's see how this can be used in practical situations.

Scenario 1: Target a single base J2SE platform

This is a very common case: several J2SE versions support your application, and it so happens you can implement everything via core APIs of a certain (I will call it base) J2SE platform version. Upward compatibility takes care of the rest. Although J2SE 1.4 is the latest and greatest version, you see no reason to exclude users who can't run J2SE 1.4 yet.

The ideal way to compile your application is:

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

Yes, this implies you might have to use two different J2SDK versions on your build machine: the one you pick for its javac and the one that is your base supported J2SE platform. This seems like extra setup effort, but it is actually a small price to pay for a robust build. The key here is explicitly controlling both the class version stamps and the bootstrap classpath and not relying on defaults. Use the -verbose option to verify where core class definitions are coming from.

As a side comment, I'll mention that it is common to see developers include rt.jar from their J2SDKs on the -classpath line (this could be a habit from the JDK 1.1 days when you had to add classes.zip to the compilation classpath). If you followed the discussion above, you now understand that this is completely redundant, and in the worst case, might interfere with the proper order of things.

Scenario 2: Switch code based on the Java version detected at runtime

Here you want to be more sophisticated than in Scenario 1: You have a base-supported Java platform version, but should your code run in a higher Java version, you prefer to leverage newer APIs. For example, you can get by with java.io.* APIs but wouldn't mind benefiting from java.nio.* enhancements in a more recent JVM if the opportunity presents itself.

In this scenario, the basic compilation approach resembles Scenario 1's approach, except your bootstrap J2SDK should be the highest version you need to use:

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

This is not enough, however; you also need to do something clever in your Java code so it does the right thing in different J2SE versions.

One alternative is to use a Java preprocessor (with at least #ifdef/#else/#endif support) and actually generate different builds for different J2SE platform versions. Although J2SDK lacks proper preprocessing support, there is no shortage of such tools on the Web.

Međutim, upravljanje nekoliko distribucija za različite J2SE platforme uvijek je dodatni teret. Uz dodatnu predviđanje možete se izvući distribucijom jedne verzije aplikacije. Evo primjera kako to učiniti ( URLTest1jednostavna je klasa koja iz URL-a izdvaja razne zanimljive bitove):