Welche Version ist Ihr Java-Code?

23. Mai 2003

F:

EIN:

öffentliche Klasse Hallo {public static void main (String [] args) {StringBuffer greeting = neuer StringBuffer ("hallo"); StringBuffer who = neuer StringBuffer (args [0]). Append ("!"); greeting.append (who); System.out.println (Begrüßung); }} // Ende des Unterrichts

Die Frage erscheint zunächst eher trivial. Die HelloKlasse enthält also nur wenig Code , und was auch immer vorhanden ist, verwendet nur Funktionen, die auf Java 1.0 zurückgehen. Die Klasse sollte also in nahezu jeder JVM ohne Probleme laufen, oder?

Sei dir nicht so sicher. Kompilieren Sie es mit javac von Java 2 Platform, Standard Edition (J2SE) 1.4.1 und führen Sie es in einer früheren Version der Java Runtime Environment (JRE) aus:

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hallo Welt Ausnahme im Thread "main" java.lang.NoSuchMethodError bei Hello.main (Hello.java:20 ) 

Anstelle des erwarteten "Hallo Welt!" Gibt dieser Code einen Laufzeitfehler aus, obwohl die Quelle zu 100 Prozent mit Java 1.0 kompatibel ist! Und der Fehler ist auch nicht genau das, was Sie vielleicht erwarten: Anstelle einer Nichtübereinstimmung der Klassenversion beschwert er sich irgendwie über eine fehlende Methode. Verwirrt? In diesem Fall finden Sie die vollständige Erklärung später in diesem Artikel. Lassen Sie uns zunächst die Diskussion erweitern.

Warum sich mit verschiedenen Java-Versionen beschäftigen?

Java ist ziemlich plattformunabhängig und größtenteils aufwärtskompatibel. Daher ist es üblich, einen Code mit einer bestimmten J2SE-Version zu kompilieren und zu erwarten, dass er in späteren JVM-Versionen funktioniert. (Änderungen der Java-Syntax treten normalerweise ohne wesentliche Änderungen am Bytecode-Befehlssatz auf.) Die Frage in dieser Situation lautet: Können Sie eine Java-Basisversion einrichten, die von Ihrer kompilierten Anwendung unterstützt wird, oder ist das Standardverhalten des Compilers akzeptabel? Ich werde meine Empfehlung später erläutern.

Eine andere ziemlich häufige Situation ist die Verwendung eines Compilers mit höherer Version als die beabsichtigte Bereitstellungsplattform. In diesem Fall verwenden Sie keine kürzlich hinzugefügten APIs, sondern möchten lediglich von Toolverbesserungen profitieren. Schauen Sie sich dieses Code-Snippet an und versuchen Sie zu erraten, was es zur Laufzeit tun soll:

öffentliche Klasse ThreadSurprise {public static void main (String [] args) löst eine Ausnahme aus {Thread [] threads = new Thread [0]; Threads [-1] .sleep (1); // Sollte das werfen? }} // Ende des Unterrichts

Sollte dieser Code einen werfen ArrayIndexOutOfBoundsExceptionoder nicht? Wenn Sie ThreadSurprisemit verschiedenen Sun Microsystems JDK / J2SDK-Versionen (Java 2 Platform, Standard Development Kit) kompilieren , ist das Verhalten nicht konsistent:

  • Compiler der Version 1.1 und früher generieren Code, der nicht ausgelöst wird
  • Version 1.2 wirft
  • Version 1.3 wirft nicht
  • Version 1.4 wirft

Der subtile Punkt hier ist, dass Thread.sleep()es sich um eine statische Methode handelt und überhaupt keine ThreadInstanz benötigt . Die Java-Sprachspezifikation erfordert jedoch, dass der Compiler die Zielklasse nicht nur aus dem linken Ausdruck von ableitet threads [-1].sleep (1);, sondern auch den Ausdruck selbst auswertet (und das Ergebnis einer solchen Auswertung verwirft). Verweist auf Index -1 derthreadsArray Teil einer solchen Bewertung? Der Wortlaut in der Java-Sprachspezifikation ist etwas vage. Die Zusammenfassung der Änderungen für J2SE 1.4 impliziert, dass die Mehrdeutigkeit endgültig zugunsten einer vollständigen Bewertung des Ausdrucks für die linke Hand behoben wurde. Großartig! Da der J2SE 1.4-Compiler die beste Wahl zu sein scheint, möchte ich ihn für alle meine Java-Programmierungen verwenden, auch wenn meine Ziel-Laufzeitplattform eine frühere Version ist, um von solchen Korrekturen und Verbesserungen zu profitieren. (Beachten Sie, dass zum Zeitpunkt des Schreibens nicht alle Anwendungsserver für die J2SE 1.4-Plattform zertifiziert sind.)

Obwohl das letzte Codebeispiel etwas künstlich war, diente es zur Veranschaulichung eines Punktes. Weitere Gründe für die Verwendung einer neueren J2SDK-Version sind der Wunsch, von Javadoc und anderen Toolverbesserungen zu profitieren.

Schließlich ist Cross-Compilation in der Embedded-Java-Entwicklung und der Java-Spieleentwicklung eine ziemliche Lebensweise.

Hallo Klassenrätsel erklärt

Das HelloBeispiel, mit dem dieser Artikel gestartet wurde, ist ein Beispiel für eine falsche Kreuzkompilierung. J2SE 1.4 hat der StringBufferAPI eine neue Methode hinzugefügt : append(StringBuffer). Wenn javac entscheidet, wie greeting.append (who)in Bytecode übersetzt werden soll, wird die StringBufferKlassendefinition im Bootstrap-Klassenpfad nachgeschlagen und stattdessen diese neue Methode ausgewählt append(Object). Obwohl der Quellcode vollständig Java 1.0-kompatibel ist, erfordert der resultierende Bytecode eine J2SE 1.4-Laufzeit.

Beachten Sie, wie einfach es ist, diesen Fehler zu machen. Es gibt keine Kompilierungswarnungen und der Fehler ist nur zur Laufzeit erkennbar. Der richtige Weg, um mit Javac aus J2SE 1.4 eine Java 1.1-kompatible HelloKlasse zu generieren, ist:

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

Die richtige Javac-Beschwörung enthält zwei neue Optionen. Lassen Sie uns untersuchen, was sie tun und warum sie notwendig sind.

Jede Java-Klasse hat einen Versionsstempel

Sie wissen es vielleicht nicht, aber jede .classDatei, die Sie generieren, enthält einen Versionsstempel: zwei vorzeichenlose kurze Ganzzahlen, beginnend mit dem Byte-Offset 4, direkt nach der 0xCAFEBABEmagischen Zahl. Sie sind die Haupt- / Nebenversionsnummern des Klassenformats (siehe Spezifikation des Klassendateiformats) und sie sind nicht nur Erweiterungspunkte für diese Formatdefinition. Jede Version der Java-Plattform gibt eine Reihe unterstützter Versionen an. Hier ist die Tabelle der unterstützten Bereiche zum Zeitpunkt des Schreibens (meine Version dieser Tabelle unterscheidet sich geringfügig von den Daten in Suns Dokumenten - ich habe einige Bereichswerte entfernt, die nur für extrem alte (vor 1.0.2) Versionen von Suns Compiler relevant sind). ::

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 

Eine kompatible JVM lehnt das Laden einer Klasse ab, wenn der Versionsstempel der Klasse außerhalb des Unterstützungsbereichs der JVM liegt. Beachten Sie aus der vorherigen Tabelle, dass spätere JVMs immer den gesamten Versionsbereich der vorherigen Versionsstufe unterstützen und ihn auch erweitern.

Was bedeutet das für Sie als Java-Entwickler? Da Sie diesen Versionsstempel während der Kompilierung steuern können, können Sie die für Ihre Anwendung erforderliche Mindest-Java-Laufzeitversion erzwingen. Genau das macht die -targetCompiler-Option. Hier ist eine Liste der Versionsstempel, die standardmäßig von Javac-Compilern aus verschiedenen JDKs / J2SDKs ausgegeben werden (beachten Sie, dass J2SDK 1.4 das erste J2SDK ist, bei dem Javac sein Standardziel von 1.1 auf 1.2 ändert):

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

Und hier ist der Effekt der Angabe verschiedener -targets:

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

Im Folgenden wird als Beispiel die URL.getPath()in J2SE 1.3 hinzugefügte Methode verwendet :

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

Da dieser Code mindestens J2SE 1.3 erfordert, sollte ich ihn -target 1.3beim Erstellen verwenden . Warum meine Benutzer zwingen, mit java.lang.NoSuchMethodErrorÜberraschungen umzugehen , die nur auftreten, wenn sie die Klasse versehentlich in eine 1.2-JVM geladen haben? Sicher, ich könnte dokumentieren, dass meine Anwendung J2SE 1.3 erfordert, aber es wäre sauberer und robuster, dasselbe auf Binärebene durchzusetzen .

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.

Die Verwaltung mehrerer Distributionen für verschiedene J2SE-Plattformen ist jedoch immer eine zusätzliche Belastung. Mit etwas mehr Weitsicht können Sie einen einzelnen Build Ihrer Anwendung verteilen. Hier ist ein Beispiel dafür ( URLTest1ist eine einfache Klasse, die verschiedene interessante Bits aus einer URL extrahiert):