Write Once, Run Anywhere – wie abwärtskompatibel ist Java eigentlich wirklich?

Mit aktuellen Programmierparadigmen und Features stellt sich die Frage, ob Java die stets beworbene Abwärtskompatibilität noch gewährleisten kann.

In Pocket speichern vorlesen Druckansicht 29 Kommentare lesen

(Bild: metamorworks/Shutterstock.com)

Lesezeit: 10 Min.
Von
  • Hendrik Ebbers
Inhaltsverzeichnis

Mit dem Slogan "Write Once, Run Anywhere" (WORA) hat Sun Microsystems ab 1995 für die Java- Plattform geworben. Dieser Slogan vereinte zwei unterschiedliche Vorteile von Java: Durch die Nutzung der JVM (Java Virtual Machine) können kompilierte Programme auf allen Plattformen ausgeführt werden, auf denen eine JVM verfügbar ist. So kann eine auf Windows kompilierte Java-Anwendung beispielsweise problemlos auf Linux in einer JVM ausgeführt werden.

Neuigkeiten von der Insel - Hendrik Ebbers

Hendrik Ebbers (@hendrikEbbers) ist Java Champion, JCP Expert Group Member und wurde mehrfach als Rockstar-Speaker der JavaOne ausgezeichnet. Mit seinem eigenen Unternehmen Open Elements hilft Hendrik aktuell den Hedera Hashgraph zu gestalten und dessen Services der Öffentlichkeit zugänglich zu machen. Hendrik ist Mitgründer der JUG Dortmund sowie der Cyberland und gibt auf der ganzen Welt Vorträge und Workshop zum Thema Java. Sein Buch "Mastering JavaFX 8 Controls" ist 2014 bei Oracle Press erschienen. Hendrik arbeitet aktiv an Open Source Projekten wie beispielsweise JakartaEE oder Eclipse Adoptium mit. Hendrik ist Mitglied des AdoptOpenJDK TSC und der Eclipse Adoptium WG.

Der zweite Aspekt des Slogans ist die Abwärtskompatibilität von Java. Software, die mit einer Java-Version kompiliert wurde, soll auch problemlos auf zukünftigen Java-Versionen ausgeführt werden. In den vergangenen Jahren hat sich aber bezüglich dieses Versprechens einiges geändert.

Abwärtskompatibilität sollte in Java schon immer durch die Trennung der Public und Private APIs in der Java Class Library (JCL) ermöglicht werden. Die JCL beinhaltet alle Klassen der Java-API, mit denen wir täglich arbeiten, wie etwa java.lang.String oder java.util.List. Aber auch exotischere Klassen wie sun.misc.Unsafe sind Bestandteil der JCL. Die JCL definiert zusammen mit der Java Virtual Machine (JVM) und verschiedenen Tools wie dem Java-Compiler (javac) das JDK von JavaSE, mit dem Entwickler täglich arbeiten.

Die Private API des JCL liegt zwar auf dem Classpath, sollte von Anwendungen aber nie direkt genutzt werden. Interne Änderungen im OpenJDK werden oft in diesem Bereich implementiert, wodurch mögliche Änderungen in den Schnittstellen der Private API entstehen konnten. Generell kann man sagen, dass alle Klassen der JCL, deren Packages nicht mit java.* oder javax.* beginnen, zur Private API gehören, da manche Java-Distributionen JavaFX enthalten, kann man für diese noch javafx.* hinzufügen.

Software, die zur Compile-Zeit oder Laufzeit Klassen aus der Private API nutzte, war somit bei jedem (Major-)Release von Java gefährdet, nicht mehr lauffähig zu sein. Während man die Nutzung zur Compile-Zeit direkt feststellen konnte, kam es teils bei der Nutzung zur Laufzeit beispielsweise durch Reflexion oder transitive Abhängigkeiten sogar zu unerwarteten Problemen in Produktion.

Mit Java 9 und der Einführung des Modulsystems hat sich das Ganze aber geändert. Das Modulsystem erlaubt es, APIs vor der Außenwelt zu verbergen und sie somit nur noch innerhalb des eigenen Moduls nutzbar zu machen. Hierdurch konnten die Private APIs von Java komplett verborgen werden.

Da diese privaten APIs von vielen Programmen und Libraries genutzt wurden, hätte dieser Einschnitt mit Java 9 sicherlich zu immensen Umbauten geführt. Daher hat man sich im OpenJDK dafür entschieden, dass die privaten APIs von Java 9 bis Java 15 weiterhin genutzt werden können. Hier wird lediglich eine Warnung ausgegeben, wenn die Software auf Private APIs zugreift. Dafür wurde der Parameter illegal-access eingeführt, der bei Java 9 bis 15 per Default auf warn gesetzt ist. Geändert werden konnte der Parameter einfach zum Start der JVM als Kommandozeilenparameter.

So konnte man auch in diesen Versionen durch Hinzufügen von --illegal-access=deny bereits dafür sorgen, dass ein Javaprogramm die Private APIs des JCL nicht mehr nutzen kann. Dies war dann auch das Standardverhalten von JDK 16. Hier muss man das Flag aktiv auf warn setzen, wenn man der eigenen Anwendung noch die Nutzung von Private APIs ermöglichen möchte. Mit dem LTS-Release von Java 17 wurde diese Option dann allerdings komplett entfernt. Die Werte permit, warn und debug wurden für das illegal-access Flag entfernt, wodurch es nicht mehr möglich ist, den generellen Zugriff auf Private APIs zu erlauben. Ist man mit Java 17 noch immer gezwungen Private APIs zu nutzen, so kann man dieses noch immer über das --add-opens-Flag oder dem Add-Opens-Attribut im Manifest für spezifische Module ermöglichen.

Auch Änderungen in den Tools oder der JVM können sich auf die Abwärtskompatibilität von Java auswirken. Mit Java 10 wurde beispielsweise durch das JEP 286 die Java Language um die Nutzung von var erweitert. Hierdurch muss der Typ einer Variable in Java nicht mehr händisch angegeben werden, wenn er vom Compiler bestimmt werden kann. Hier einmal ein Beispiel:

var list = new ArrayList<String>();  // infers ArrayList<String>
var stream = list.stream();          // infers Stream<String> 

Die Einführung von var in die Java Language hat hierbei einige Auswirkungen nach sich gezogen. Zwar wurde var nicht als Keyword zur Java-Syntax hinzugefügt, wodurch es noch immer möglich ist, var als Variablennamen zu nutzen. Der Status von var in der Java Language ist als "Reserved Type Name" (siehe JEP 286) definiert. Hierdurch ist es allerdings nicht mehr möglich, Klassen oder Interfaces var zu nennen. Auch wenn dies nur in sehr wenigen Javaprogrammen je vorgekommen sein mag, ist es doch ein Bruch in der Abwärtskompatibilität zu Java.

Die erste Version von Java wurde 1996 veröffentlicht. Da sich nicht nur Java als Programmiersprache, sondern auch Programmierparadigmen seit dieser Zeit immer weiter entwickelt haben, wurden viele APIs in Java umgestellt. Patterns, die 1996 noch typisch waren, sind heutzutage teils als veraltet. Dazu kommt, dass auch die Entwickler des OpenJDK gelegentlich Fehler machen und dadurch APIs entstehen, die man nach heutiger Kenntnis besser nicht mehr nutzen sollte.

Um Java allerdings abwärtskompatibel zu halten, wurden solche APIs nicht entfernt, wenn sie Bestandteil der Public APIs der JCL sind. Hier wurde initial in der JavaDoc vor der Nutzung gewarnt und oft wurden auch direkt alternative APIs vorgeschlagen. Durch die Einführung von Annotation mit Java 1.5 konnte dies durch die Nutzung der @Deprecated-Annotation noch einmal verbessert werden. Diese Annotation zeigt nicht nur dem Nutzer, dass eine API nicht mehr genutzt werden sollte, sondern lässt auch den Java Compiler eine Warnung (oder je nach Konfiguration sogar einen Fehler) erzeugen. Auch in IDEs wird das Ganze heute deutlich hervorgehoben, sodass man schnell sieht, ob Programmcode auf APIs zugreift, die als deprecated (veraltet) markiert sind.

Obschon das Verfahren lange funktioniert hat, entstand über die Jahre doch immer mehr Code im OpenJDK, der mit @Deprecated annotiert war und somit bei jeder Version und Änderung mit gepflegt werden musste. Auch wurde die Java-API somit immer aufgeblähter. Mit dem Einzug des Java-Modulsystems und der Aufteilung der JCL in einzelne Module tauchten noch ganz andere Probleme auf: Durch die vielen veralteten Codestellen, die nie entfernt wurden, gab es völlig wilde Abhängigkeiten im OpenJDK, die nicht einfach aufgelöst werden konnten.

Daher wurde mit Java 9 die @Deprecated-Annotation um das Attribut forRemoval erweitert. Dieses Attribut gibt an, dass eine API, die mit @Deprecated(forRemoval=true) annotiert ist, in einer zukünftigen Version von Java entfernt werden kann. Durch den neuen Release Train von Java und neuen Versionen im Sechsmonatstakt kann dies mitunter äußerst schnell gehen. Und die letzten Versionen von Java zeigen auch, dass hiervon Gebrauch gemacht wird. So wurden unter anderem die CORBA-API, verschiedene Interfaces unter java.security.acl.* oder Methoden aus dem java.lang.SecurityManager entfernt. Der java.lang.SecurityManager soll sogar komplett aus dem JCL entfernt werden.

Um die Unterschiedene und Änderungen zwischen zwei Java-Versionen überprüfen und beurteilen zu können, gibt es seit einiger Zeit die Webseite javaalmanac.io. Hier können alle Unterschiede in der Java Class Library zwischen zwei Java-Versionen angezeigt werden. Da hier nicht nur Versionen mit Long-term Support (LTS) gelistet sind, sondern alle Major Releases seit Java 1.0, kann man einfach auch schon vor einer Umstellung oder sogar vor dem Erscheinen einer neuen LTS-Version von Java mit der Umstellung der eigenen Software beginnen. Neben Änderungen zeigt das Tool auch alle Klassen, Funktionen und weitere Elemente an, die mit @Deprecated annotiert wurden.

Java hat gezeigt, dass sich Programmiersprachen irgendwann zwischen einer innovativen und agilen Weiterentwicklung und einer ständigen Abwärtskompatibilität entscheiden müssen. Je älter eine Sprache wird, desto mehr Altlasten bringt sie mit sich. Viele Teile der API sind nicht mehr zeitgemäß und lassen sich nur schwer auf moderne Paradigmen adaptieren. Daher ist es sinnvoll, dass eine Sprache auch mal Altlasten über Bord wirft. Natürlich darf man dabei die User nicht vergessen und muss sensibel mit diesen Themen umgehen.

Aus meiner Sicht haben die Verantwortlichen für Java diesen Spagat gut bewältigt, indem sie die neuen Konzepte wie das Entfernen von deprecated APIs über einen langen Zeitraum angekündigt haben. Auch sind sie auf Kritik und Feedback der Community eingegangen. Hier gilt sicherlich der Umgang mit der Klasse sun.misc.Unsafe als gutes Beispiel. Deren Entfernen aus dem OpenJDK wurde sehr lange diskutiert. Für Entwickler von Frameworks und Libraries wurden auch weitere Funktionen zum OpenJDK hinzugefügt, um Abwärtskompatibilität gewährleisten zu können. Mit Multi-Release JAR-Files (JEP 238) können Jars somit spezifische Klassen für verschiedene Java-Versionen enthalten und so die Kompatibilität deutlich erhöhen.

Trotzdem kommt durch solche Änderungen mehr Arbeit auf Entwicklerinnen und Entwickler zu, die lange mit dem Wechsel zwischen Java-Versionen warten. Wenn man aber immer auf die jeweils aktuelle LTS-Version von Java umstellt und die hier beschriebenen Konzepte und Tools kennt, bleiben die Arbeiten generell überschaubar.

(rme)