Mit Java 16 liegt jetzt eine neue Ausgabe der beliebten Programmiersprache vor. Als letztes Feature Release vor der nächsten LTS-Version mit Langzeit-Support liefert sie interessante Ausblicke auf die weitere Entwicklung.
Im halbjährlichen Release-Zyklus steht seit Mitte März Java 16 bereit. Wie immer bringt die neue Version Verbesserungen an der Sprache selbst, den Bibliotheken sowie der Laufzeitumgebung und den Werkzeugen. Sie laden Java 16 zum Beispiel vom AdoptOpenJDK-Projekt [1] herunter, dessen Beitritt zur Eclipse Foundation sein dauerhaftes Leben als Open-Source-Projekt sichern sollte.
Unsichtbar
Insgesamt siebzehn Änderungen haben den Weg in dieses Feature-Release geschafft. Die Features JEP-357 und JEP-369 sieht man der Software gar nicht an, dennoch kommen sie einer kleinen Revolution gleich. Java nutzte in den letzten Jahrzehnten das bei Sun Microsystems entwickelte Mercurial [2] als Source-Code-Management-System – an sich kein schlechtes SCM, doch die Fußabstimmung unter den Entwicklern hat eindeutig Git gewonnen, Linus Torvalds zweite Großtat. Der Umzug von Java auf Git und Github ist ein weiterer Schritt, die Java-Quellen externen Programmierern leichter und schneller bereitzustellen. Dabei geht es nicht nur um das reine Speichern der Quellen, auch die Zuarbeit stellen die Entwickler auf die inzwischen übliche Vorgehensweise mit Gits Pull-Mechanismus um.
Ein Blick auf die neue Github-Seite [3] zeigt, dass knapp 20 Prozent der Quellen in C und C++ geschrieben sind. Ähnlich wie bei Python (34 Prozent) oder dem Javascript-Interpreter V8 (70 Prozent) gilt C/C++ nach wie vor als Standardsprache zur Implementation von (Bytecode-)Interpretern. Java 16 macht nun den Schritt auf den C++14-Standard, was durch den Wegfall einiger unschöner Makros saubereren Code ermöglicht. Der C/C++-Code muss vor allem angefasst werden, um Java auf weitere Plattformen zu portieren. Dazu zählen momentan Windows auf ARM-Prozessoren (JEP-388) und Linux-System mit Musl-libc (JEP-386).
Die Musl-libc-Implementation [4] fällt wesentlich kleiner aus als die sonst genutzte Glibc und eignet sich daher bestens für Systeme mit beschränkten Ressourcen. Ein komplettes Linux kommt dann mit 8 MByte RAM und weniger als 200 MByte Speicherplatz aus. Das erlaubt nicht nur den Einsatz auf schwachbrüstiger Router-Hardware [5]: Auch Docker-Images auf Basis von Alpine Linux [6], der schlanken Container-Basis für Java-Anwendungen, lassen sich in Windeseile starten.
Eine ganz andere Verpackung bietet das wiederbelebte Werkzeug Jpackage an (JEP-392). Es erstellt DEB- und RPM-Pakete für Java-Anwendungen (MSI und EXE für Windows, PKG und DMG für MacOS), wodurch sie sich mit den Standardwerkzeugen des Betriebssystems verwalten lassen. Ursprünglich mit JavaFX verbandelt, flog Jpackage mit Java 11 aus der Standardinstallation und steht nun wieder allgemein zur Verfügung. Allerdings funktioniert es nach wie vor nicht plattformübergreifend; wer unter Linux ein EXE für Windows erstellen muss, sollte einen Blick auf Launch4j [7] werfen.
Es gibt wohl kein Java-Release ohne Verbesserungen an der Garbage-Collection. Die ehemals gefürchteten Bedenksekunden beim Aufräumen sind allerdings schon lange kein Thema mehr. Der ZGC sichert nun eine maximale Pausenzeit unter einer Millisekunde zu, bei gleichzeitig verringertem Speicherverbrauch (JEP 376). Dem gleichen Zweck dient JEP 387: Beim Elastic Metaspace geht es um die Freigabe von Klasseninformationen aus dem Hotspot-Compiler.
Unix-Power
Die Unix Domain Sockets (UDS) ermöglichen seit vier Jahrzehnten den Datenaustausch zwischen zwei Prozessen auf einem Rechner. Inzwischen auch unter Windows und MacOS verfügbar, stellen sie eine oft übersehene Alternative zur allgegenwärtigen Kommunikation über Netzwerk-Sockets dar. Die UDS benötigen jedoch weder ein aufwendiges Netzwerkprotokoll, noch kann man die Kommunikation von außen an- oder abgreifen. Mit der in JEP-380 beschriebenen Java-Implementation lassen sich nun sowohl UDS-Clients als auch UDS-Server schreiben.
Listing 1 zeigt ein einfaches Beispiel. Gemäß dem Unix-Motto, dass alles eine Datei ist, adressiert man die UDS nicht über Host und Port, sondern über eine Pseudodatei. Der Zugriff darauf unterliegt der üblichen Rechteverwaltung des Dateisystems, was es ermöglicht, die Kommunikation extrem sicher zu machen. Dementsprechend erzeugen die ersten Zeilen die »UnixDomainSocketAddress« auf Basis einer Datei und nicht über Host und Port.
Der Rest des Codes unterscheidet sich nicht wesentlich von der Kommunikation über Netzwerksockets. Beim Öffnen des Server-Sockets in Zeile 3 wird die »StandardProtocolFamily.UNIX« statt »StandardProtocolFamily.INET« angegeben und an die (Datei-)Adresse gebunden. Ab Zeile 7 folgt dann ein minimaler Listener-Thread, der alle Bytes entgegennimmt und auf die Kommandozeile ausgibt. Auch der Client ab Zeile 30 unterscheidet sich nur hinsichtlich der Protokollfamilie vom üblichen Netzwerktransfer.
Listing 1
Kommunikation über Unix Domain Sockets
Path socketDatei = Paths.get("/tmp/jep380.socket");
UnixDomainSocketAddress address = UnixDomainSocketAddress.of(socketDatei);
ServerSocketChannel serverKanal = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
serverKanal.bind(address);
// Server Seite
Thread serverThread = new Thread(() -> {
try {
SocketChannel kanal = serverKanal.accept();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
int bytesRead = kanal.read(buffer);
if (bytesRead > 0) {
byte[] bytes = new byte[bytesRead];
buffer.flip();
buffer.get(bytes);
String message = new String(bytes);
System.out.println("`" + message + "`");
}
buffer.clear();
Thread.sleep(100);
}
} catch (IOException | InterruptedException exp) {
System.err.println("Server Fehler " + exp);
}
});
serverThread.start();
// Client Seite
SocketChannel clientKanal = SocketChannel.open(StandardProtocolFamily.UNIX);
clientKanal.connect(address);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put(LocalDateTime.now().toString().getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
clientKanal.write(buffer);
}
Die Vorteile der Unix Domain Sockets lassen sich nicht nur nutzen, wenn man Server und Client selbst entwickelt. Auch Datenbanken wie Postgres bieten diese Kommunikation an. Gunnar Morling zeigt, wie sich das unter Java nutzen lässt [8]. Neben der erhöhten Sicherheit lockt eine um 10 bis 20 Prozent niedrigere Latenz, was sich bei vielen kleinen Anfragen sehr positiv auswirkt.
Noch eine Ebene tiefer können zwei Programme über ein geteiltes Stück Hauptspeicher miteinander kommunizieren. Er kann zum Beispiel den Inhalt des Videospeichers einer Kamera enthalten, den ein Treiber ins RAM kopiert. Solche Zugriffe ermöglicht die Foreign-Memory Access API (JEP 393), die mittlerweile in der dritten Iteration vorliegt, aber immer noch als experimentell gilt.
Primärer Nutznießer des Speicherzugriffs ist die ebenso experimentelle Foreign Linker API (JEP 389), die den direkten Zugriff auf native Bibliotheken ermöglicht. Gegenüber dem abgekündigten Java Native Interface (JNI) oder nutzerfreundlichen Verpackungen wie Swig [9] oder JNA [10] soll die neue API einfacher, schneller und sicherer operieren. Als Basis für die Nutzung großer C++-Bibliotheken mit enger Hardwarebindung (OpenGL, Tensorflow, etc.) könnte sie den aktuell genutzten, liebevoll handgeschrieben Zwischencode überflüssig machen.
Glücklicherweise agiert Java in der Regel ähnlich zügig wie C- oder C++-Code. Anders als bei Python muss man also nicht zwangsläufig auf native Bibliotheken zurückgreifen, um den Code zu beschleunigen. Deshalb dürften wohl nur wenige Programmierer diese beiden JEPs direkt nutzen.
Sprachverbesserungen
Die meisten Java-Anwender interessieren sich eher für Änderungen, die ihre tägliche Arbeit im Java-Code vereinfachen. Dazu gehört das mit Java 16 finalisierte Pattern-Matching für »instanceof« (JEP 394). Gibt man diesem Java-Urgestein eine Zielvariable mit, erledigt es nicht nur die Typprüfung, sondern weist den untersuchten Wert nach der Typumwandlung zu (Listing 2, Zeile 4). Anders als noch bei Java 15 ist die Variable »liste« nicht mehr final. Außerdem überprüft der Compiler, ob die Zuweisung sinnvoll wäre. Der im Listing 2 auskommentierte Code in Zeile 3 würde beim Kompilieren zu einem Fehler führen.
Listing 2
JEP 394 und JEP 397
// JEP 394
Object o = List.of("A", "B", "C");
// List o = List.of( "A", "B", "C");
if (o instanceof List liste && ! liste.isEmpty()) {
System.out.println("länge " + liste.size());
} else {
System.out.println("listenlos");
}
// JEP 397
sealed class Teil permits Platte, Profil {};
final class Platte extends Teil {};
final class Profil extends Teil {};
// final class Schraube extends Teil {};
Auch die Records (JEP 395) haben nun den Preview-Status verlassen. Sie stellen eine Alternative zu normalen Java-Klassen dar; ihre Definition fällt extrem kompakt aus. Den Konstruktor, wie die Methoden »toString« und »equals«, bekommt man quasi geschenkt; die Werte im Record lassen sich später nicht mehr ändern. Anders als bei Java 15 [11] darf man die Records nun auch in Lambdas verwenden, sie sind komplett durch die Reflection-API abgedeckt.
Bei den Sealed Classes (JEP 397) handelt es sich dagegen immer noch um ein Preview-Feature. Sie lassen sich nur nutzen, wenn man beim Kompilieren und Starten die Option »–enable-preview« angibt. Hier geht es darum, die Klassenvererbung auf bestimmte Klassen einzuschränken. Wie der entsprechende Abschnitt in Listing 2 zeigt, gibt man bei der Basisklasse »Teil« an, wer davon ableiten darf. Fehlt das »final« oder wird wie in Zeile 13 eine weitere Klasse hinzugefügt, schlägt das Kompilieren fehl. Hier gab es in Java 16 nur noch kosmetische Änderungen in der Spezifikation sowie beim Verhalten bei anonymen Klassen. Mit Java 17 werden die Sealed Classes den Preview-Status verlassen.
Als vollkommen experimentell gilt dagegen die neue Vector-API (JEP 338). Die CPUs von AMD und Intel enthalten spezielle Befehle (Advanced Vector Extensions, AVX) für das gleichzeitige Berechnen von bis zu 16 Floats. Sie arbeiten wesentlich schneller als die generischen x86-Instruktionen, ließen sich bisher jedoch vom Standard-Java aus nicht nutzen. Das ändert die Vector-API; bei CPUs ohne AVX greift sie auf die normalen Instruktionen zurück.
Apropos Zugriff auf CPU-Sonderfunktionen: In Javas Untiefen locken nach wie vor Klassen wie »sun.misc.Unsafe« Entwickler vom Pfad der Tugend. Sie sollten ursprünglich mit Java 8 oder spätestens 9 entfallen, doch die Entwickler gaben dem empörten Aufschrei vieler Programmierer nach und beließen sie in der JVM. Als pädagogische Maßnahme muss man ihre Verwendung in Zukunft jedoch explizit mit »–illegal-access=permit« erlauben (JEP 396).
Fazit
Mit Java 16 liegt ein Feature-Release vor, das einige länger laufenden Entwicklungen abschließt. Endlich stehen Records und das verbesserte »instanceof« ohne »–enable-preview« zur Verfügung. Entwicklungsumgebungen bieten hierfür ebenso Unterstützung wie die parallel gepflegte UI-Bibliothek JavaFX [12]. Auch wer sich nicht auf Javas Feature-Releases verlassen darf oder möchte, sollte seinen Code einmal mit dieser Version testen, bevor im Herbst der Schritt von der LTS-Version Java 11 auf den Langzeit-Nachfolger Java 17 ansteht.
Insgesamt ist die Java-Welt nach wie vor gesund – die Sprache liegt in der Programmierergunst auf Platz 3 – und bunt. Auf der Server-Seite hat die Java Enterprise Edition inzwischen unter Führung der Eclipse Foundation den Schritt zur Jakarta EE geschafft. Parallel laufende Projekte wie Spring, Apache TomEE und andere beflügeln sich im Kampf um die schlankestmögliche Java-Server-Basis gegenseitig. Die läuft oft nicht mehr auf einem vollwertigen Betriebssystem: Dank Googles Maven-Plugin Jib [13] lassen sich Java-Anwendungen direkt in ein Docker-Image verpacken.
Selbst die zwischenzeitlich schon abgeschriebene Programmierung von Clients findet zunehmend wieder Beachtung. Mittlerweile dämmert es vielen Entwicklern, dass Webbrowser wohl nicht für jede Anwendung die beste Plattform darstellen. Mit JavaFX steht eine moderne UI-Bibliothek zur Verfügung, die sich auf dem Desktop vom Embedded-Bereich (Raspberry Pi mit Touchscreen) bis zur Bearbeitung hochauflösender medizinische Bilder bewährt. Dabei kommt der Spaß nicht zu kurz: Almas Baimagambetov zeigt mit seiner einsteigerfreundlichen FXGL-Bibliothek [14], dass sich Java auch nach Minecraft noch gut für die Spieleentwicklung (Abbildung 1) eignet. (jlu)
Der Autor
Carsten Zerbst erstellt mit seinem Team Software für Ingenieure, vom einfachen Konverter bis hin zur unternehmenskritischen Integrationslösung. Er sucht derzeit zwei weitere Mitarbeitende für abwechslungsreiche Tätigkeiten mitten in Hamburg.
Infos
- AdoptOpenJDK: https://adoptopenjdk.net
- Mercurial: https://www.mercurial-scm.org
- Github: https://github.com/openjdk
- Musl: https://musl.libc.org
- OpenWrt: https://openwrt.org
- Alpine Linux : https://alpinelinux.org
- Launch4j: http://launch4j.sourceforge.net
- UDS und Postgres: https://www.morling.dev/blog/talking-to-postgres-through-java-16-unix-domain-socket-channels
- Swig: https://github.com/swig/swig
- JNA: https://github.com/java-native-access/jna
- Java 15: Carsten Zerbst, “Blockweise”, LM 11/2020, S. 90, https://www.lm-online.de/45334
- JavaFX: https://openjfx.io
- Jib: https://github.com/GoogleContainerTools/jib
- FXGL: https://github.com/AlmasB/FXGL







