Quale versione è il tuo codice Java?

23 maggio 2003

Q:

UN:

public class Hello {public static void main (String [] args) {StringBuffer greeting = new StringBuffer ("hello,"); StringBuffer who = new StringBuffer (args [0]). Append ("!"); saluto.append (chi); System.out.println (saluto); }} // Fine della lezione

In un primo momento la domanda sembra piuttosto banale. Così poco codice è coinvolto nella Helloclasse, e qualunque cosa ci sia usa solo funzionalità che risalgono a Java 1.0. Quindi la classe dovrebbe essere eseguita praticamente in qualsiasi JVM senza problemi, giusto?

Non essere così sicuro. Compilarlo utilizzando javac da Java 2 Platform, Standard Edition (J2SE) 1.4.1 ed eseguirlo in una versione precedente di Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Eccezione nel thread "main" java.lang.NoSuchMethodError su Hello.main (Hello.java:20 ) 

Invece del previsto "ciao, mondo!", Questo codice genera un errore di runtime anche se l'origine è compatibile al 100% con Java 1.0! E l'errore non è esattamente quello che ci si potrebbe aspettare: invece di una mancata corrispondenza della versione della classe, in qualche modo si lamenta di un metodo mancante. Perplesso? In tal caso, troverai la spiegazione completa più avanti in questo articolo. Per prima cosa, allarghiamo la discussione.

Perché perdere tempo con diverse versioni di Java?

Java è abbastanza indipendente dalla piattaforma e per lo più compatibile con le versioni successive, quindi è comune compilare un pezzo di codice utilizzando una data versione di J2SE e aspettarsi che funzioni nelle versioni successive di JVM. (Le modifiche alla sintassi Java di solito avvengono senza modifiche sostanziali al set di istruzioni del codice byte.) La domanda in questa situazione è: puoi stabilire un qualche tipo di versione Java di base supportata dall'applicazione compilata o il comportamento predefinito del compilatore è accettabile? Spiegherò la mia raccomandazione più tardi.

Un'altra situazione abbastanza comune è quella di utilizzare un compilatore con versione superiore rispetto alla piattaforma di distribuzione prevista. In questo caso, non utilizzi alcuna API aggiunta di recente, ma desideri semplicemente beneficiare dei miglioramenti dello strumento. Guarda questo frammento di codice e prova a indovinare cosa dovrebbe fare in fase di esecuzione:

public class ThreadSurprise {public static void main (String [] args) genera Eccezione {Thread [] threads = new Thread [0]; thread [-1] .sleep (1); // Dovrebbe lanciare? }} // Fine della lezione

Questo codice dovrebbe lanciare un ArrayIndexOutOfBoundsExceptiono no? Se si compila ThreadSurpriseutilizzando versioni differenti di Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit), il comportamento non sarà coerente:

  • I compilatori della versione 1.1 e precedenti generano codice che non genera
  • La versione 1.2 genera
  • La versione 1.3 non genera file
  • La versione 1.4 genera

Il punto sottile qui è che Thread.sleep()è un metodo statico e non necessita affatto di Threadun'istanza. Tuttavia, la specifica del linguaggio Java richiede che il compilatore non solo deduca la classe di destinazione dall'espressione a sinistra di threads [-1].sleep (1);, ma valuti anche l'espressione stessa (e scarti il ​​risultato di tale valutazione). Fa riferimento all'indice -1 dithreadsparte dell'array di tale valutazione? La formulazione nelle specifiche del linguaggio Java è piuttosto vaga. Il riepilogo delle modifiche per J2SE 1.4 implica che l'ambiguità è stata finalmente risolta a favore della valutazione completa dell'espressione della mano sinistra. Grande! Poiché il compilatore J2SE 1.4 sembra la scelta migliore, voglio usarlo per tutta la mia programmazione Java anche se la mia piattaforma runtime di destinazione è una versione precedente, solo per beneficiare di tali correzioni e miglioramenti. (Si noti che al momento della scrittura non tutti i server delle applicazioni sono certificati sulla piattaforma J2SE 1.4.)

Sebbene l'ultimo esempio di codice fosse in qualche modo artificiale, è servito per illustrare un punto. Altri motivi per utilizzare una versione recente di J2SDK includono il voler trarre vantaggio da javadoc e altri miglioramenti dello strumento.

Infine, la compilazione incrociata è piuttosto un modo di vivere nello sviluppo Java incorporato e nello sviluppo di giochi Java.

Ciao puzzle di classe spiegato

L' Helloesempio che ha avviato questo articolo è un esempio di compilazione incrociata errata. J2SE 1.4 aggiunto un nuovo metodo per StringBufferl'API: append(StringBuffer). Quando javac decide come tradurre greeting.append (who)in byte code, cerca la StringBufferdefinizione della classe nel classpath di bootstrap e seleziona questo nuovo metodo invece di append(Object). Anche se il codice sorgente è completamente compatibile con Java 1.0, il codice byte risultante richiede un runtime J2SE 1.4.

Nota quanto è facile commettere questo errore. Non sono presenti avvisi di compilazione e l'errore è rilevabile solo in fase di esecuzione. Il modo corretto per utilizzare javac da J2SE 1.4 per generare una Helloclasse compatibile con Java 1.1 è:

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

L'incantesimo javac corretto contiene due nuove opzioni. Esaminiamo cosa fanno e perché sono necessari.

Ogni classe Java ha un timbro della versione

Potresti non esserne consapevole, ma ogni .classfile che generi contiene un timbro di versione: due interi brevi senza segno che iniziano con l'offset di byte 4, subito dopo il 0xCAFEBABEnumero magico. Sono i numeri di versione maggiore / minore del formato della classe (vedere la specifica del formato del file di classe) e hanno utilità oltre a essere solo punti di estensione per questa definizione di formato. Ogni versione della piattaforma Java specifica una gamma di versioni supportate. Ecco la tabella degli intervalli supportati in questo momento (la mia versione di questa tabella differisce leggermente dai dati nei documenti di Sun: ho scelto di rimuovere alcuni valori di intervallo rilevanti solo per le versioni estremamente vecchie (precedenti alla 1.0.2) del compilatore di Sun) :

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

Una JVM conforme rifiuterà di caricare una classe se il timbro della versione della classe è al di fuori dell'intervallo di supporto della JVM. Nota dalla tabella precedente che le JVM successive supportano sempre l'intera gamma di versioni dal livello di versione precedente e la estendono.

Cosa significa questo per te come sviluppatore Java? Data la possibilità di controllare questo timbro della versione durante la compilazione, è possibile applicare la versione minima del runtime Java richiesta dall'applicazione. Questo è esattamente ciò che fa l' -targetopzione del compilatore. Di seguito è riportato un elenco di timbri di versione emessi dai compilatori javac da vari JDK / J2SDK per impostazione predefinita (osservare che J2SDK 1.4 è il primo J2SDK in cui javac cambia il suo target predefinito da 1.1 a 1.2):

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

Ed ecco l'effetto di specificare vari -targets:

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

Ad esempio, quanto segue utilizza il URL.getPath()metodo aggiunto in J2SE 1.3:

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

Poiché questo codice richiede almeno J2SE 1.3, dovrei usarlo -target 1.3durante la compilazione. Perché costringere i miei utenti ad affrontare le java.lang.NoSuchMethodErrorsorprese che si verificano solo quando hanno erroneamente caricato la classe in una JVM 1.2? Certo, potrei documentare che la mia applicazione richiede J2SE 1.3, ma sarebbe più pulito e più robusto applicare lo stesso a livello binario.

I don't think the practice of setting the target JVM is widely used in enterprise software development. I wrote a simple utility class DumpClassVersions (available with this article's download) that can scan files, archives, and directories with Java classes and report all encountered class version stamps. Some quick browsing of popular open source projects or even core libraries from various JDKs/J2SDKs will show no particular system for class versions.

Bootstrap and extension class lookup paths

When translating Java source code, the compiler needs to know the definition of types it has not yet seen. This includes your application classes and core classes like java.lang.StringBuffer. As I am sure you are aware, the latter class is frequently used to translate expressions containing String concatenation and the like.

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.

Tuttavia, la gestione di più distribuzioni per diverse piattaforme J2SE è sempre un onere aggiuntivo. Con qualche accortezza in più puoi farla franca distribuendo una singola build della tua applicazione. Ecco un esempio di come farlo ( URLTest1è una semplice classe che estrae vari bit interessanti da un URL):