zurück zum Artikel

Der neue Releasezyklus von Java

Nicolai Parlog
Der neue Releasezyklus von Java

(Bild: Christoph, Pixabay)

Alle sechs Monate erscheint mittlerweile eine neue Java-Version. Was bedeutet das für den Entwicklungsalltag? Wie können Projekte mit dem schnellen Releasezyklus umgehen?

Wenn man den bekannten Spielentwickler John Carmack fragte, wann das nächste Doom erscheint, antwortete er üblicherweise "When it's done". Früher lief es in Java ähnlich. Java 7 zum Beispiel erschien im Juli 2011 nach viereinhalb Jahren Entwicklung.

Im Jahr 2012, die JDK-Entwickler arbeiteten gerade fieberhaft am Release von Java 8, kamen Zweifel daran auf, ob ein Feature-getriebener Release-Zyklus sinnvoll sei. Mark Reinhold, Chief Architect der Java Platform Group bei Oracle, sprach sich damals für einen rein zeitbasierten Releasezyklus aus [1]. Alle zwei Jahre sollte eine neue Java-Version herauskommen. Mit allen Features, die dann fertig sein werden – die, die es nicht rechtzeitig geschafft hätten, müssten auf die nächste Version warten.

Das hat, vorsichtig ausgedrückt, nicht gut funktioniert. Java 8 verzögerte sich wieder und wieder und erschien im März 2014 nach beinahe drei Jahren Entwicklungszeit, Java 9 im September 2017 nach dreieinhalb. Das ist ärgerlich für alle Beteiligten: Die JDK-Entwickler geraten unter Druck, Features rechtzeitig fertigzustellen, und der Rest der Community ist enttäuscht, dass es länger und länger dauert, bis sie neue Werkzeuge bekommt. In der Zwischenzeit lungern fertiggestellte Features im Repository der unveröffentlichten Version herum und schaffen keinen Mehrwert.

Aber warum haben die Entwickler das Zweijahresziel so klar verpasst? Treibende Kraft war die wiederkehrende Fehleinschätzung, dass ein Feature fast fertig war und unbedingt noch in die neue Version musste (in Java 9 war es zum Beispiel das Modulsystem). Aber "fast fertig" ist in der Softwareentwicklung ein dehnbarer Begriff.

"The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time." (Tom Cargill, Bell Labs)

Das Problem ist weniger das Verschieben auf die nächste Version, als dass die zwei (oder mehr) Jahre bis dahin eine lange Zeit sind. Je kürzer der Abstand zwischen Releases, desto einfacher ist es für die Entwickler eines Features, ein Release zu verpassen und das nächste anzupeilen. Zwei Jahre ist dafür auf jeden Fall zu lang, ein Jahr scheint ziemlich genau auf der Kippe zu stehen, aber sechs Monate sind deutlich diesseits der Schwelle. Da sorgt es nicht einmal bei den ungeduldigsten Entwicklern für Missmut, wenn ein Feature in einer späteren Version landet.

Jetzt erscheint alle sechs Monate eine neue Java-Version. Dabei hat jedes Release die gewohnte Qualität – es gibt keine Beta-Versionen. Dass Oracle und andere Anbieter für manche Versionen Long-Term-Support anbieten [2], ist eine organisatorische Entscheidung, keine technische: LTS-Versionen unterscheiden sich hinsichtlich der eingebrachten Sorgfalt und der Qualitätsansprüche nicht ein Jota von den anderen Versionen.

Der schnellere Releasezyklus hat eine Reihe positiver Effekte:

Aber nichts ist umsonst und die Softwareentwicklung ist voller Trade-offs. Es gibt teilweise berechtigte Sorgen, dass der schnelle Zyklus ernsthaften Nachteil mitbringt:

Der Rest des Artikels beleuchtet die Sorgen genauer und prüft, an welchen etwas dran ist und was man getrost auf sich zukommen lassen kann. Spoiler: Ganz so dramatisch wird es wohl nicht.

Eine häufig geäußerte Befürchtung ist, dass sich Java in Zukunft zu schnell entwickeln könnte und Entwickler sich schwer damit tun, alle Features kennenzulernen und ausreichend sicher zu beherrschen. Darin steckt die implizite Annahme, dass die kommenden Versionen ähnlich viel Neues bringen wie zuletzt Java 8 und 9. Wie man an Java 10 und 11 erkennen kann, ist das nicht der Fall. Beide Versionen enthalten nur wenige Features, die für den Entwicklungsalltag relevant sind.

Bei näherem Hinsehen ist das nicht weiter überraschend. Die kritische Ressource bei der JDK-Entwicklung sind die Entwickler selber, und Oracle hat das Team nicht vergrößert. Was sich ändert, ist also nicht die Entwicklungsgeschwindigkeit (zumindest nicht wesentlich), sondern die Geschwindigkeit, in der das Team Änderungen ausliefert. Features kommen nicht schneller, nur regelmäßiger (Java 11 ist ein gutes Beispiel dafür). In Mark Reinholds Worten: "The rate of innovation doesn't change. The rate of innovation delivery increases."

Warum die Einschränkung, dass sich die Entwicklung nicht wesentlich beschleunigt? Die Einschätzung des Autors ist, dass Java tatsächlich etwas flotter wird. Zum einen geht das Projekt Amber [5] einige kleinere Sprachfeatures an, die wesentlich zügiger fertigzustellen sind als Mammutprojekte wie Lambdas, das Modulsystem oder Value Types. Zum anderen erwecken einige von Oracles Entscheidungen in der jüngeren Vergangenheit den Eindruck, dass sie Ressourcen von Wartung auf Weiterentwicklung umschichten.

Wer das Update auf Java 9+ schon hinter sich hat, weiß, dass das gerade bei großen Projekten kein Zuckerschlecken ist. Erwartet Entwickler das jetzt alle sechs Monate? Nein, zum Glück nicht.

Das Update auf Java 10 ist trivial, und auch Java 11 macht kaum mehr Probleme (falls man die Java-EE-Module bei der Migration auf 9 ersetzt hat). Oracle legt, genau wie in der Vergangenheit, weiterhin viel Wert auf Abwärtskompatibilität. Übrigens: Viele Änderungen, die bei der Java-9-Migration Probleme machen [6], sind penibel gesehen keine Inkompatibilitäten, weil sie nicht standardisierte Implementierungsdetails betreffen.

Kompatibilität ist aber nicht absolut und steht in einem Spannungsfeld mit der Evolution der Sprache, ihrer VM und APIs. Im Spannungsfeld wird Java sich tatsächlich ein bisschen aus der Kompatibilitätsecke heraus trauen:

Zukünftige Migrationen sind also bei weitem nicht so schwierig wie die zu Java 9, aber die derzeit gelegentlich verbreitete Einschätzung, dass der Schritt von Java 10 auf 11 so unkompliziert ist wie der von 8u20 auf 8u40, ist nicht ganz richtig. Es ist allerdings möglich, Migrationen durch verschiedene Maßnahmen zu vereinfachen.

Eine naheliegende Maßnahme ist, sich unbedingt nur auf standardisierte Features, unterstützte APIs und festgelegtes Verhalten zu verlassen. In den meisten Fällen merkt man, wenn man vom diesem Weg abweicht, aber solche Abhängigkeiten können subtil sein und versehentlich entstehen.

Klassisches Beispiel ist die Reihenfolge, in der die Reflection API die Methoden einer Klasse zurückgibt. Als das in einem kleinen Java-Update geändert wurde, scheiterten plötzlich überall Tests, Persistenzschichten und anderer Code, der versehentlich von der Aufrufreihenfolge annotierter Methoden abhängig war.

Um sich nur auf standardisiertes Verhalten zu verlassen, müssen Entwickler die Augen offen halten, wobei Entwicklungsprozesse wie Pair Programming und Code Reviews helfen können. Außerdem sollte man sich bei wichtigen Entscheidungen, die das Aussehen des Codes langfristig prägen, nicht nur auf beobachtetes Verhalten verlassen, sondern unbedingt in den dazugehörigen Vertrag schauen, also meist die Java Language Specification beziehungsweise Javadoc.

Eine andere Maßnahme ist, bei den eigenen Abhängigkeiten genau hinzuschauen, wie gut das jeweilige Projekt gewartet wird. Es lohnt sich, bei Codebasen, die lange leben sollen, Projekte zu bevorzugen, an denen Entwickler aktiv arbeiten und die viele Nutzer haben. Das macht es wahrscheinlicher, rechtzeitig Updates zu bekommen, die Migrationsprobleme lösen. Dass dieses Vorgehen die Experimentierfreudigkeit dämpft, ist allerdings ein unangenehmer Nebeneffekt.

Die dritte Maßnahme lautet ganz einfach: Updates. Abhängigkeiten, Entwicklertools, Infrastruktur – alles jederzeit auf der aktuellen Version zu halten ist aufwendig, aber bei Java-Updates macht sich das bezahlt. Wer zunächst alle Abhängigkeiten aktualisiert, bevor man Java auf die nächste Version hebt, kann manche Hürde überspringen, ohne sie überhaupt zu bemerken.

Wer Anwendungen zum Kunden ausliefert und sich dort mit verschiedenen Java-Versionen konfrontiert sieht, sollte jlink in Betracht ziehen. Mit dem in Java 9 hinzugefügten Werkzeug lässt sich ein komplett eigenständiges Image erstellen, das den eigenen Code, alle Abhängigkeiten sowie das JRE enthält. Dann spielt es keine Rolle mehr, ob und welche Java-Version auf der Zielmaschine installiert ist, und man kann sich ganz auf ein Release konzentrieren.

Im Gegenzug gibt man die Unabhängigkeit vom Betriebssystem auf (jedes Betriebssystem erfordert ein eigenes Image), und wenn auf dem Zielrechner das JRE aktualisiert wird, profitiert man nicht von dessen verbesserter Sicherheit oder Performanz. Nominell muss man Module erstellen, um jlink zu verwenden, aber Tools wie Moditect [8] erlauben, das zu umgehen.

Wer Software ausliefert und sicherstellen möchte, dass sie auf verschiedenen Java-Versionen funktioniert, kommt nicht umhin auf allen zu testen. Aber auch wer volle Kontrolle über seine Deployments hat, wie bei der Entwicklung einer klassischen Webanwendung oder der Verwendung von jlink, sollte in Betracht ziehen, auf mehreren Java-Version zu bauen, um frühzeitig herauszufinden, wenn Probleme auftreten. Der ansatzlose Sprung von Java 11 zu 17 kann schwierig sein, wenn man Migrationsprobleme bekommt, aber nicht weiß, welche Java-Version sie ausgelöst hat.

Um auf verschiedenen Versionen zu bauen und bei Bedarf dann auch in einer konkreten Version Fehler zu analysieren, muss man möglichst reibungslos zwischen ihnen wechseln können. Alle populären IDEs, Build-Tools und CI-Server machen das möglich, aber besonders benutzerfreundlich ist das nicht immer. Es lohnt sich für Entwickler, sich mit den Features intensiv auseinanderzusetzen und bei Bedarf Feature Requests zu erstellen, wenn es einfacher sein könnte.

Automatisierung spielt ebenfalls eine große Rolle. Entwickler machen sich das Leben deutlich einfacher, wenn man dafür sorgt, dass man mit ein paar Mausklicks oder ein paar Zeilen Konfiguration einen neuen CI-Build erstellen kann. Einen solchen Automatisierungsgrad zu erreichen hat viele Vorteile – ohne große Umstände auf verschiedenen Java-Versionen gleichzeitig bauen zu können, ist nur einer davon.

Eine Konsequenz von mehreren Builds ist allerdings gestiegener Ressourcenverbrauch. Jeden Build statt auf einer auf drei Java-Versionen auszuführen verdreifacht natürlich den Bedarf nach Rechenleistung. Wenn das zu aufwendig oder teuer ist, kann man in Betracht ziehen, für manche Java-Versionen nur einmal nächtlich zu bauen. Um nicht nur auf Sicht zu fahren, sollte man neben dem üblichen CI-Build aber zumindest einmal täglich einen Build auf der aktuellen Java-Version laufen lassen.

Wenn dafür außerdem noch Early Access Builds im Einsatz sind, derzeit zum Beispiel schon für Java 12 verfügbar [9], kann man sogar helfen, Bugs im JDK aufzudecken, und damit zur Gesundheit des Ökosystems beitragen.

Manche haben die Sorge, dass der Spalt zwischen Java 8 und 9 so tief ist wie der zwischen Python 2 und 3. Diejenigen möchte der Autor beruhigen: Das ist nicht der Fall. Alle weitverbreiteten IDEs und Build-Tools sowie alle großen Frameworks und Bibliotheken unterstützen Java 9, und wo das nicht der Fall ist, arbeitet das Team meist hart daran. Dem Autor ist kein Projekt bekannt, das entschieden hat, vorerst nur Java 8 zu unterstützen.

Was allerdings etwas auf sich warten lässt, ist die weitläufige Verwendung des Modulsystems. Bisher haben nur wenige Projekte angefangen, Module auszuliefern. Dass ist zu erwarten gewesen, denn bevor Java 9+ Mindestvoraussetzung wird, ergibt es für ein Projekt nicht viel Sinn, Module zu erstellen. Und da bisher nur wenige Anwendungen auf Java 9+ laufen, warten die großen Bibliotheken und Frameworks mit der Modularisierung noch etwas.

Jedes Projekt muss für sich, möglicherweise in Absprache mit den Nutzern oder Kunden, entscheiden, wie es mit dem schnelleren Releasezyklus und dem neuen Lizensierungsmodell umgeht und welche Java-Versionen es verwendet. Davon unabhängig ist es sinnvoll, immer gegen jede veröffentlichte oder zumindest die neueste Version zu bauen. Dabei sollte man Early Access Builds für unveröffentlichte Java-Versionen mit einbeziehen. Trifft man auf Probleme, sollte man unbedingt die passenden Mailinglisten [10] informieren.

Beim Programmieren ändert sich durch neue Java-Versionen nicht viel, außer dass es mit Javas leichter Verschiebung weg von Kompatibilität hin zu Evolution wichtiger geworden ist, sich nur auf standardisiertes Verhalten zu verlassen. Builds gegen jede Java-Version und das Tool jdeprscan helfen, Deprecations zu entdecken und frühzeitig darauf zu reagieren. Bei Tools und Abhängigkeiten hilft es, jederzeit eine möglichst aktuelle Version zu verwenden, sodass man bei Problemen mit neuen Java-Releases einfach ein Update auf eine kompatible Version machen kann.

Hält man sich daran, kann man es sich in der schönen neuen Welt mit regelmäßigen Java-Updates und einem konstanten Strom neuer Features bequem machen. John Carmack, übrigens, hat 2013 in einem Wired-Interview [11] gesagt, dass er der "When it's done"-Mentalität mittlerweile abgeschworen hat.

Nicolai Parlog
ist selbständiger Softwareentwicker, Autor und Trainer. Er lebt in Karlsruhe und bloggt auf codefx.org.
(bbo [12])


URL dieses Artikels:
https://www.heise.de/-4165009

Links in diesem Artikel:
[1] https://mreinhold.org/blog/late-for-the-train
[2] https://www.heise.de/hintergrund/Wird-Java-jetzt-kostenpflichtig-4144533.html
[3] http://openjdk.java.net/jeps/11
[4] http://openjdk.java.net/jeps/12
[5] http://openjdk.java.net/projects/amber
[6] https://blog.codefx.org/java/java-9-migration-guide
[7] https://stackoverflow.com/a/49398936/2525313
[8] https://github.com/moditect/moditect
[9] http://jdk.java.net/12/
[10] http://mail.openjdk.java.net/mailman/listinfo
[11] https://www.wired.com/2013/12/john-carmack-doom/
[12] mailto:bbo@ix.de