¿Qué versión es su código Java?

23 de mayo de 2003

Q:

UN:

public class Hola {public static void main (String [] args) {StringBuffer greeting = new StringBuffer ("hola,"); StringBuffer quién = nuevo StringBuffer (args [0]). Append ("!"); saludo.append (quién); System.out.println (saludo); } } // Fin de clases

Al principio, la pregunta parece bastante trivial. Hay tan poco código involucrado en la Helloclase, y todo lo que hay usa solo funcionalidad que se remonta a Java 1.0. Entonces, la clase debería ejecutarse en casi cualquier JVM sin problemas, ¿verdad?

No estés tan seguro. Compílelo usando javac de Java 2 Platform, Standard Edition (J2SE) 1.4.1 y ejecútelo en una versión anterior de Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Excepción en el hilo "main" java.lang.NoSuchMethodError en Hello.main (Hello.java:20 ) 

En lugar del esperado "¡hola, mundo!", Este código arroja un error de tiempo de ejecución a pesar de que la fuente es 100% compatible con Java 1.0. Y el error tampoco es exactamente lo que podría esperar: en lugar de una falta de coincidencia de la versión de la clase, de alguna manera se queja de un método faltante. ¿Perplejo? Si es así, encontrará la explicación completa más adelante en este artículo. Primero, ampliemos la discusión.

¿Por qué molestarse con diferentes versiones de Java?

Java es bastante independiente de la plataforma y en su mayoría compatible con versiones posteriores, por lo que es común compilar un fragmento de código utilizando una versión de J2SE determinada y esperar que funcione en versiones posteriores de JVM. (Los cambios de sintaxis de Java generalmente ocurren sin cambios sustanciales en el conjunto de instrucciones del código de bytes). La pregunta en esta situación es: ¿Puede establecer algún tipo de versión base de Java compatible con su aplicación compilada, o es aceptable el comportamiento predeterminado del compilador? Explicaré mi recomendación más adelante.

Otra situación bastante común es utilizar un compilador con una versión superior a la plataforma de implementación prevista. En este caso, no utiliza ninguna API agregada recientemente, sino que simplemente desea beneficiarse de las mejoras de la herramienta. Mire este fragmento de código e intente adivinar qué debería hacer en tiempo de ejecución:

public class ThreadSurprise {public static void main (String [] args) arroja Exception {Thread [] threads = new Thread [0]; hilos [-1] .sleep (1); // ¿Debería tirar esto? } } // Fin de clases

¿Debería este código lanzar un ArrayIndexOutOfBoundsExceptiono no? Si compila ThreadSurpriseutilizando diferentes versiones de Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit), el comportamiento no será coherente:

  • Los compiladores de la versión 1.1 y anteriores generan código que no arroja
  • Lanzamientos de la versión 1.2
  • La versión 1.3 no arroja
  • Lanzamientos de la versión 1.4

El punto sutil aquí es que Thread.sleep()es un método estático y no necesita una Threadinstancia en absoluto. Aún así, la especificación del lenguaje Java requiere que el compilador no solo infiera la clase de destino a partir de la expresión de la izquierda threads [-1].sleep (1);, sino que también evalúe la expresión en sí (y descarte el resultado de dicha evaluación). ¿Está haciendo referencia al índice -1 delthreadsmatriz parte de dicha evaluación? La redacción de la Especificación del lenguaje Java es algo vaga. El resumen de cambios para J2SE 1.4 implica que la ambigüedad finalmente se resolvió a favor de evaluar completamente la expresión de la izquierda. ¡Excelente! Dado que el compilador J2SE 1.4 parece ser la mejor opción, quiero usarlo para toda mi programación Java, incluso si mi plataforma de tiempo de ejecución de destino es una versión anterior, solo para beneficiarme de tales correcciones y mejoras. (Tenga en cuenta que en el momento de redactar este documento no todos los servidores de aplicaciones están certificados en la plataforma J2SE 1.4).

Aunque el último ejemplo de código fue algo artificial, sirvió para ilustrar un punto. Otras razones para utilizar una versión reciente de J2SDK incluyen querer beneficiarse de javadoc y otras mejoras de herramientas.

Finalmente, la compilación cruzada es toda una forma de vida en el desarrollo de Java integrado y el desarrollo de juegos Java.

Hola clase rompecabezas explicado

El Helloejemplo que inició este artículo es un ejemplo de compilación cruzada incorrecta. J2SE 1.4 añade un nuevo método para la StringBufferAPI: append(StringBuffer). Cuando javac decide cómo traducir greeting.append (who)a código de bytes, busca la StringBufferdefinición de clase en la ruta de clase de arranque y selecciona este nuevo método en lugar de append(Object). Aunque el código fuente es totalmente compatible con Java 1.0, el código de bytes resultante requiere un tiempo de ejecución J2SE 1.4.

Tenga en cuenta lo fácil que es cometer este error. No hay advertencias de compilación y el error solo se puede detectar en tiempo de ejecución. La forma correcta de usar javac de J2SE 1.4 para generar una Helloclase compatible con Java 1.1 es:

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

El encantamiento javac correcto contiene dos nuevas opciones. Examinemos qué hacen y por qué son necesarios.

Cada clase de Java tiene un sello de versión

Es posible que no se dé cuenta, pero cada .classarchivo que genera contiene un sello de versión: dos enteros cortos sin firmar que comienzan en el byte offset 4, justo después del 0xCAFEBABEnúmero mágico. Son los números de versión mayor / menor del formato de clase (consulte la Especificación de formato de archivo de clase), y tienen utilidad además de ser puntos de extensión para esta definición de formato. Cada versión de la plataforma Java especifica una gama de versiones compatibles. Aquí está la tabla de rangos admitidos en el momento de escribir este artículo (mi versión de esta tabla difiere ligeramente de los datos en los documentos de Sun; elegí eliminar algunos valores de rango que solo son relevantes para versiones extremadamente antiguas (anteriores a la 1.0.2) del compilador de Sun) :

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

Una JVM compatible se negará a cargar una clase si el sello de versión de la clase está fuera del rango de soporte de la JVM. Tenga en cuenta de la tabla anterior que las JVM posteriores siempre admiten todo el rango de versiones del nivel de versión anterior y también lo amplían.

¿Qué significa esto para usted como desarrollador de Java? Dada la capacidad de controlar este sello de versión durante la compilación, puede aplicar la versión mínima de tiempo de ejecución de Java requerida por su aplicación. Esto es precisamente lo que hace la -targetopción del compilador. Aquí hay una lista de sellos de versión emitidos por compiladores javac desde varios JDK / J2SDK de forma predeterminada (observe que J2SDK 1.4 es el primer J2SDK donde javac cambia su destino predeterminado de 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 

Y aquí está el efecto de especificar varios -targets:

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

Como ejemplo, lo siguiente utiliza el URL.getPath()método agregado en J2SE 1.3:

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

Dado que este código requiere al menos J2SE 1.3, debería usarlo -target 1.3al compilarlo. ¿Por qué obligar a mis usuarios a lidiar con java.lang.NoSuchMethodErrorsorpresas que ocurren solo cuando han cargado por error la clase en una JVM 1.2? Claro, podría documentar que mi aplicación requiere J2SE 1.3, pero sería más limpio y más sólido hacer cumplir lo mismo a nivel 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.

Sin embargo, administrar varias distribuciones para diferentes plataformas J2SE es siempre una carga adicional. Con un poco de previsión adicional, puede distribuir una única versión de su aplicación. Aquí hay un ejemplo de cómo hacer eso ( URLTest1es una clase simple que extrae varios bits interesantes de una URL):