Java für alte Plattformen kompilieren

Heute soll es einmal um einige Compiler-Flags gehen, und zwar um -source, -target und das mit Java 9 eingeführte --release (JEP 247).
Während man mit -source das Quellcodelevel setzen kann (-source 1.6 würde es beispielsweise auf Java 6 setzen), lässt sich mit –target das Level im Ziel setzen.
Dies funktioniert natürlich nur so lange, wie man im Quellcode keine Sprachfeatures verwendet, die in der ausgewählten Version nicht verfügbar sind. Wenn man beispielsweise die mit Java 8 eingeführten Lambdas verwendet und versucht, das Programm auf Java 1.6 zurück zu portieren, ist dies entsprechend nicht möglich.

Aber selbst, wenn man bei installiertem JDK 9 den Standardbefehl
javac -target 1.7 -source 1.7 C:\Temp\Main.java
verwendet, gibt der Compiler aus:
warning: [options] bootstrap class path not set in conjunction with -source 1.7

Wenn man anschließend die Class-Datei mit
java -cp "C:\Temp\" Main
auf der Java 9 JVM ausführt, wird diese ganz normal ausgeführt. Zu einem Problem kann es kommen, wenn man nun die Datei auf einer Java 7 JVM ausführen möchte. Dies kann funktionieren, muss es aber nicht.

Das Problem ist, dass der Compiler bei der Verwendung von -target standardmäßig gegen die aktuell installierte Plattform-API kompiliert. Das heißt, wenn beispielsweise Methoden aus der Plattform-API verwendet werden, die sich von Java 7 zu Java 9 geändert haben, linkt der Compiler ganz selbstverständlich gegen die neue API. Bei der Ausführung auf einer Java 7 JVM kommt es dann zu einem Fehler, da die Methoden aus der Plattform-API plötzlich nicht mehr gefunden werden können oder eine andere Signatur haben.

Ein Beispiel aus einem Bugreport des JDK 8, bei der ein User versuchte, mit dem Java 8 Compiler ein Codesnippet auf Java 7 zurück zu portieren:

public static void main(String[] args) {
  ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
  Set<Integer> keys = map.keySet();
  System.out.println(keys);
} 

Dieses Codebeispiel lässt sich ohne Probleme mit den Flags -source 1.7 -target 1.7 kompilieren. Wenn man es startet kommt jedoch eine java.lang.NoSuchMethodError in Zeile 3.
Das rührt daher, dass sich die Plattform-APIs genau an dieser Stelle unterscheiden:
In Java 7 wurde hier einfach ein Set<K> zurückgegeben, in Java 8 dagegen eine KeySetView<K,V>.

In diesem Fall wurde gegen die damals aktuelle Plattform-API (Java 8) kompiliert. Daher wurde die keySet-Methode, die ein KeySetView<K,V>-Objekt zurückgeben soll in der Java 7 Plattform-API nicht gefunden. Ergo kommt es zu einem NoSuchMethodError.

Auf dieses Problem weist auch schon die Warnung, die während des Kompiliervorgangs angezeigt wurde, hin. Als Lösung muss nun der dort erwähnte Bootstrap Class Path angegeben werden:
javac -bootclasspath 'C:\Program Files\Java\jre1.7.0_0\lib\rt.jar' -Xlint -target 1.7 -source 1.7 C:\Temp\Main.java

Und siehe da, der Code lässt sich kompilieren und fehlerfrei auf einer Java 7 JVM ausführen.

Diesen Workaround zu finden ist nicht ganz trivial und das Verhalten der -target-Flag fällt definitiv unter “unexpected”.

Daher gibt es im Compiler zu Java 9 die neue Flag --release. Hier kann eine Javaversion ab Java 6 verwendet werden. Im Unterschied zu -source und -target wird hier standardmäßig gegen die korrekte Plattform-API kompiliert. Somit muss der Boot Class Path nicht mehr extra angegeben werden.
Mit
javac --release 7 C:\Temp\Main.java
lässt sich der Code also wunderbar kompilieren und danach auch auf der entsprechenden Java 7 JVM korrekt ausführen.

Short URL for this post: https://wp.me/p4nxik-32x
This entry was posted in Did you know?, Java and Quality, Java Runtimes - VM, Appserver & Cloud and tagged , , , , , . Bookmark the permalink.

Leave a Reply