Quelle est la version de votre code Java?

23 mai 2003

Q:

UNE:

public class Hello {public static void main (String [] args) {StringBuffer salutation = new StringBuffer ("hello,"); StringBuffer who = new StringBuffer (args [0]). Append ("!"); salut.append (qui); System.out.println (salutation); }} // Fin du cours

Au début, la question semble assez triviale. Si peu de code est impliqué dans la Helloclasse, et quoi qu'il en soit, n'utilise que des fonctionnalités remontant à Java 1.0. La classe devrait donc fonctionner dans n'importe quelle machine virtuelle Java sans problème, non?

Ne sois pas si sûr. Compilez-le à l'aide de javac de Java 2 Platform, Standard Edition (J2SE) 1.4.1 et exécutez-le dans une version antérieure de Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Exception dans le thread "main" java.lang.NoSuchMethodError à Hello.main (Hello.java:20 ) 

Au lieu du «bonjour, monde!» Attendu, ce code génère une erreur d'exécution même si la source est 100% compatible avec Java 1.0! Et l'erreur n'est pas exactement ce à quoi vous pourriez vous attendre non plus: au lieu d'une incompatibilité de version de classe, il se plaint en quelque sorte d'une méthode manquante. Perplexe? Si tel est le cas, vous trouverez l'explication complète plus loin dans cet article. Tout d'abord, élargissons la discussion.

Pourquoi s'embêter avec différentes versions de Java?

Java est assez indépendant de la plate-forme et principalement compatible vers le haut, il est donc courant de compiler un morceau de code en utilisant une version J2SE donnée et de s'attendre à ce qu'il fonctionne dans les versions ultérieures de JVM. (Les changements de syntaxe Java se produisent généralement sans aucune modification substantielle du jeu d'instructions de code d'octet.) La question dans cette situation est: pouvez-vous établir une sorte de version Java de base prise en charge par votre application compilée, ou le comportement par défaut du compilateur est-il acceptable? J'expliquerai ma recommandation plus tard.

Une autre situation assez courante consiste à utiliser un compilateur à version plus élevée que la plate-forme de déploiement prévue. Dans ce cas, vous n'utilisez aucune API récemment ajoutée, mais vous souhaitez simplement bénéficier des améliorations de l'outil. Regardez cet extrait de code et essayez de deviner ce qu'il doit faire au moment de l'exécution:

classe publique ThreadSurprise {public static void main (String [] args) lève l'exception {Thread [] threads = new Thread [0]; threads [-1] .sleep (1); // Est-ce que ça devrait jeter? }} // Fin du cours

Ce code devrait-il lancer un ArrayIndexOutOfBoundsExceptionou non? Si vous compilez à l' ThreadSurpriseaide de différentes versions de Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit), le comportement ne sera pas cohérent:

  • La version 1.1 et les compilateurs antérieurs génèrent du code qui ne lance pas
  • La version 1.2 jette
  • La version 1.3 ne jette pas
  • La version 1.4 jette

Le point subtil ici est qu'il Thread.sleep()s'agit d'une méthode statique et n'a pas du tout besoin d'une Threadinstance. Néanmoins, la spécification du langage Java exige que le compilateur non seulement infère la classe cible à partir de l'expression de gauche threads [-1].sleep (1);, mais évalue également l'expression elle-même (et rejette le résultat d'une telle évaluation). Est-ce que l'index de référence -1 duthreadstableau fait partie d'une telle évaluation? Le libellé de la spécification du langage Java est quelque peu vague. Le résumé des changements pour J2SE 1.4 implique que l'ambiguïté a finalement été résolue en faveur d'une évaluation complète de l'expression de gauche. Génial! Puisque le compilateur J2SE 1.4 semble être le meilleur choix, je souhaite l'utiliser pour toute ma programmation Java même si ma plate-forme d'exécution cible est une version antérieure, juste pour bénéficier de ces corrections et améliorations. (Notez qu'au moment de la rédaction de cet article, tous les serveurs d'applications ne sont pas certifiés sur la plate-forme J2SE 1.4.)

Bien que le dernier exemple de code soit quelque peu artificiel, il a servi à illustrer un point. D'autres raisons d'utiliser une version récente de J2SDK incluent le fait de vouloir bénéficier de javadoc et d'autres améliorations d'outils.

Enfin, la compilation croisée est tout à fait un mode de vie dans le développement embarqué Java et le développement de jeux Java.

Bonjour puzzle de classe expliqué

L' Helloexemple qui a démarré cet article est un exemple de compilation croisée incorrecte. J2SE 1.4 a ajouté une nouvelle méthode pour l' StringBufferAPI: append(StringBuffer). Lorsque javac décide comment traduire greeting.append (who)en code octet, il recherche la StringBufferdéfinition de classe dans le chemin de classe d'amorçage et sélectionne cette nouvelle méthode au lieu de append(Object). Même si le code source est entièrement compatible avec Java 1.0, le code d'octet résultant nécessite un runtime J2SE 1.4.

Notez combien il est facile de faire cette erreur. Il n'y a pas d'avertissement de compilation et l'erreur n'est détectable qu'au moment de l'exécution. La bonne façon d'utiliser javac de J2SE 1.4 pour générer une Helloclasse compatible Java 1.1 est:

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

L'incantation javac correcte contient deux nouvelles options. Examinons ce qu'ils font et pourquoi ils sont nécessaires.

Chaque classe Java a un tampon de version

You may not be aware of it, but every .class file you generate contains a version stamp: two unsigned short integers starting at byte offset 4, right after the 0xCAFEBABE magic number. They are the major/minor version numbers of the class format (see the Class File Format Specification), and they have utility besides just being extension points for this format definition. Every version of the Java platform specifies a range of supported versions. Here is the table of supported ranges at this time of writing (my version of this table differs from data in Sun's docs slightly—I chose to remove some range values only relevant to extremely old (pre-1.0.2) versions of 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 

A compliant JVM will refuse to load a class if the class's version stamp is outside of the JVM's support range. Note from the previous table that later JVMs always support the entire version range from the previous version level and also extend it.

What does this mean to you as a Java developer? Given the ability to control this version stamp during compilation, you can enforce the minimum Java runtime version required by your application. This is precisely what the -target compiler option does. Here is a list of version stamps emitted by javac compilers from various JDKs/J2SDKs by default (observe that J2SDK 1.4 is the first J2SDK where javac changes its default target from 1.1 to 1.2):

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

And here is the effect of specifying various -targets:

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

As an example, the following uses the URL.getPath() method added in J2SE 1.3:

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

Since this code requires at least J2SE 1.3, I should use -target 1.3 when building it. Why force my users to deal with java.lang.NoSuchMethodError surprises that occur only when they have mistakenly loaded the class in a 1.2 JVM? Sure, I could document that my application requires J2SE 1.3, but it would be cleaner and more robust to enforce the same at binary level.

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.

Cependant, la gestion de plusieurs distributions pour différentes plates-formes J2SE est toujours une charge supplémentaire. Avec un peu de prévoyance supplémentaire, vous pouvez vous en sortir en distribuant une seule version de votre application. Voici un exemple de comment faire cela ( URLTest1est une classe simple qui extrait divers bits intéressants d'une URL):