Aus Linux-Magazin 05/2024

Mit Java 22 beginnt der nächste Entwicklungszyklus

© Roman Samborskyi / 123RF.com

Seit 19. März liegt die neueste Java-Version 22 vor. Sie dient als erstes Feature Release auf dem Weg zur nächsten LTS-Version Java 25 im September 2025. Wir werfen einen Blick auf die Neuerungen.

Wie geplant liegt sechs Monate nach der Freigabe der Vorversion jetzt Java 22 zum Download bereit [1]. Es handelt sich dabei um ein Feature Release, an dem Anwender die neuesten Änderungen testen können. Ab dem Erscheinen von Java 23 im September 2024 wird diese Version keinen Support mehr bekommen, weshalb sich für die produktive Anwendung der Einsatz eines langzeitunterstützten Releases (LTS) anbietet. Das ist aktuell Java 21, das bis September 2026 gepflegt werden soll, wie im Artikel [2] zu dessen Release erhofft. Sowohl Adoptium [3] als auch Oracle [4] und selbst Microsoft bietet Java 21 als LTS zum Download an.

Die jetzt vorliegende Version 22 bringt reichlich Verbesserungen, die Release Notes [5] zählen nicht weniger als zwölf abgearbeitete JDK Enhancement Proposals (JEPs) und gut 30 kleinere Änderungen auf. Die betreffen sowohl die Syntax der Sprache als auch die mitgelieferte Standardbibliothek sowie Änderungen an den Entwickler-Tools und der Laufzeitumgebung.

Anonym

Eine kleinere Sprachänderung ist die Verwendung des Unterstrichs (»_«) anstelle von Variablennamen. Das in JEP 456 “Unnamed Variables” beschriebene Feature kann man immer dann nutzen, wenn die Syntax die Verwendung eines Variablennamens erfordert, der Wert aber im Folgenden nie zur Anwendung kommt. Daneben erleichtert das Feature die Verwendung von statischen Codeanalysewerkzeugen, die sonst nicht zugewiesene Rückgabewerte bemäkeln. Diese Änderung wurde schon in Java 21 als Preview vorgestellt und steht in Java 22 ohne Änderungen für die allgemeine Verwendung bereit.

Listing 1 zeigt alle wesentlichen Anwendungen der Unnamed Variables, der komplett lauffähige Code [6] steht auf Github bereit. In Zeile 3 nutzt eine Exception die Unnamed Variable, in den Zeilen 9 bis 11 dient sie in einem Switch-Statement zum Pattern Matching. Zeile 13 wendet das Pattern Matching für Records an, hier wird die Länge nicht benötigt. Zeile 19 demonstriert, wie man die Warnung über nicht zugewiesene Rückgabewerte aus der statischen Codeanalyse loswird.

Listing 1

Unnamed Variables (JEP 456)

try {
    throw new IllegalArgumentException("demo");
  } catch (Exception _) {
    System.err.println("es trat ein Fehler auf");
}
Form form = new Rechteck(1, 2);
switch (form) {
  case Rechteck _ -> System.out.println("ja");
  case Kreis _ -> System.out.println("nein");
  case Form _ -> System.out.println("vielleicht");
}
if (form instanceof Rechteck(_, int b)) {
  System.out.println("breite " + b);
}
List<String> werte = [...]
var iterator = werte.iterator();
var _ = iterator.next();
while (iterator.hasNext()) {
  var wirdBenötigt = iterator.next();
}

Vorschlagswesen

Die Wiederverwendung von Konstruktoren vermeidet doppelten Code. Allerdings musste der »this()«- oder »super()«-Call dafür immer als Erstes erfolgen. Ist das nicht möglich, dienen wie in Zeile 8 von Listing 2 oft statische Methoden als Lösung, was jedoch das Codeverständnis erschwert. Das JEP 447 “Statements before Super” nimmt sich dieses Problems an und liegt in Java 22 als Preview vor. Damit kann das Parsen wie in den Zeilen 12 bis 14 vor dem Aufruf des anderen Konstruktors geschehen. In diesem Codeabschnitt gibt es jedoch noch kein Objekt, Zugriffe auf Variablen und Methoden der Klasse sind hier nicht möglich.

Listing 2

Statements before super (JEP 447)

public class Punkt {
  public Punkt( int x, int y) {
    this.x = x;
    this.y = y;
  }
  // vor Java 22
  public Punkt(String sval) {
    this( parse(sval,0), parse(sval,1));
  }
  // mit Java 22
  public Punkt(String sval) {
    String[] split = sval.split(" ");
    int x = Integer.parseInt(split[0]);
    int y = Integer.parseInt(split[1]);
    this( x, y);
  }
}

Sammelleidenschaft

Die mit Java 8 eingeführte Stream-API ermöglicht das Mappen und Filtern. Dank der eingebauten Funktionen in Kombination mit der Lambda-Syntax ergibt das leserlichen Code. Bisher ließen sich jedoch keine eigenen Stream-Funktionen erstellen. Stattdessen musste man die Werte aus dem Stream in Lists oder Sets sammeln, dort weiter bearbeiten und gegebenenfalls für die nächsten Schritte in Streams umwandeln.

Mit den Stream Gatherers (JEP 461) lassen sich nun eigene Funktionen erstellen, die jenseits von »map()« oder »filter()« auf Streams arbeiten. Dazu wurde das neue »Gatherer«-Interface definiert, das Funktionen zum Initialisieren und Verarbeiten eines Stream-Elements sowie zur Rekombination und zum Aufräumen von Streams enthält. Je nach Aufgabe kann man alle vier Funktionen implementieren, in der Regel kommt man aber mit einer oder zwei aus.

Listing 3 zeigt, wie sich die Gatherer anwenden lassen. Als Ursprungsdaten dient der Stream einer leicht verrauschten Cosinus-Funktion, in Abbildung 1 grau dargestellt. Dieser Stream lässt sich jetzt mit selbst geschriebenen Gatherern ändern. Ein einfaches Beispiel finden Sie gleich zu Anfang des Listings. Die dort definierte Funktion wird auf jedes Element des Streams angewendet (Integrator). Die Parametrisierung des Integrators enthält den Typ des Statusobjekts (hier ohne, also »Void«) sowie den Ein- und Ausgabewerte (»Double«).

Abbildung 1: Die Ausgaben des Codes aus <a href="#artRef-l3">Listing&nbsp;3</a> demonstrieren die Funktion der neuen Stream Gatherers (JEP&nbsp;461).

Abbildung 1: Die Ausgaben des Codes aus Listing 3 demonstrieren die Funktion der neuen Stream Gatherers (JEP 461).

Zeile 3 quadriert den Eingabewert und übergibt das Ergebnis in Zeile 4 an den nächsten Stream. Sobald der Eingabewert des Integrators unter »-0,95« fällt, springt der Rückgabewert des Integrators auf »false« und unterbricht damit den Stream (Zeile 5). Die Zeile 7 erzeugt dann den Gatherer mit der Integrator-Funktion, Zeile 8 wendet ihn auf dem Stream an. Das Ergebnis ist die rote Linie in Abbildung 1; wegen der Abbruchbedingung in Zeile 5 hört sie vorzeitig auf.

Viele Stream-Funktionen benötigen mehr Informationen als nur das aktuelle Element. Ab Zeile 10 dient ein Gatherer zur Berechnung des gleitenden Mittelwerts. Mit je einer Initializer- und Integrator-Funktion erstellt, unterstützt er wegen des »ofSequential()« keine parallelen Streams. Die Berechnung eines gleitenden Mittels benötigt ein Statusobjekt mit den letzten Elementen. Der Initializer in Zeile 12 erzeugt dafür eine einfache Liste. Der Integrator bekommt bei jedem Aufruf sowohl diese Liste als auch das nächste Element übergeben. Er fügt den neuesten Wert der Liste hinzu, reduziert sie in Zeile 16 auf maximal zehn Elemente und berechnet den Mittelwert (Zeile 23), den er dem Downstream übergibt (Zeile 24). Das gleitende Mittel ist die blaue Linie in Abbildung 1.

Mit dem neuen Gatherer-Interface lassen sich also beliebige Funktionen für die Stream-Bearbeitung definieren. Die neue »Gatherer«-Klasse bringt dazu noch fünf vorgefertigte Methoden mit. Die in Zeile 32 verwendete Methode »windowFixed()« trennt den Stream in Listen auf. Im Beispiel sind die Abschnitte jeweils 25 Elemente lang, auf den Sublisten berechnet der Code dann wieder den Mittelwert. Anders als beim gleitenden Mittel liegt er hier entsprechend nur alle 25 Elemente vor – das sind die grünen Punkte in Abbildung 1. Die Stream Gatherers befinden sich noch im Preview-Status, weswegen Sie zum Kompilieren und Ausführen die Option »–enable-preview« angeben müssen.

Listing 3

Stream Gatherer (JEP 461)

Gatherer.Integrator<Void, Double, Double> elementFunktion =
  (status, eingabe, downstream) -> {
     var neu = Math.pow(eingabe, 2);
     downstream.push(neu);
     return eingabe > -0.95;
  };
Gatherer<Double, Void, Double> gatherer = Gatherer.of(elementFunktion);
var quadriert = ströme().gather(gatherer).toList();
Gatherer<Double, List<Double>, Double> gmg= Gatherer.ofSequential(
  // Initializer
  () -> new ArrayList<Double>(),
  // Integrator
  Gatherer.Integrator.of((liste, element, downstream) -> {
    liste.add(element);
    if (liste.size() > 10) {
      liste.removeFirst();
    }
    double mw = 0.0;
    for (double d : liste) {
      mw += d;
    }
    mw /= liste.size();
    downstream.push(mw);
    return true;
  })
);
var gleitendesMittel = ströme().gather(gmg).toList();
var mittelwerte =
  ströme().gather(Gatherers.windowFixed(25)).
     map( bereich -> bereich.stream().mapToDouble(d -> d).average().getAsDouble()).
     toList();

Nabelschau

Zu Laufzeit nutzt die Java Virtual Machine (JVM) Bytecode, der aus dem Java-Compiler (Class-Dateien), dem Groovy-Interpreter oder einer sonstigen Quelle stammt. Zum Lesen, Erstellen und Ändern von Bytecode gibt es diverse Bibliotheken wie ASM [7], BCEL [8] und Javassist [9].

Die wenigsten Entwickler arbeiten direkt mit Bytecode, doch spätestens beim Dekompilieren einer Klasse zur Fehlersuche nutzt man diese Funktionalität. In den Werkzeugen Jar und Jlink kommt eine Kopie von ASM zum Einsatz. Das entpuppt sich zunehmend als Problem, da sich mit jeder Java-Version auch das Bytecode-Format ändert. Da ASM erst nach Freigabe der neuesten Java-Version nachgezogen wird, kann man nicht innerhalb einer Version das Bytecode-Format ändern und das gleich nutzen.

Um dies in Zukunft zu vermeiden, gibt es nun mit der Class-File API (JEP 457) eine eigene Bibliothek. Sie wird im selben Tempo wie das Bytecode-Format selbst weiterentwickelt und beseitigt so das Problem des Zeitverzugs. Eine direkte Übernahme von ASM wurde verworfen: Nach über 20 Jahren ist diese Bibliothek zwar sehr ausgereift, aber vom API-Stil her nicht mehr auf der Höhe der Zeit. Brian Goetz hat seine Ideen hinter der Class-File API auf dem JVM Language Summary vorgestellt [11].

Fremdherrschaft

Während Bytecode-Manipulation für normale Entwickler eher ein Exotenthema darstellt, kommt der Aufruf von C-Bibliotheken öfter vor. Die Foreign Function & Memory API (FFM, JEP 454) liegt nach wenigen Änderungen mit Java 22 in der finalen Version vor. Damit können Entwickler C-Funktionen von Java aus aufrufen, Speicher zwischen den Programmiersprachen austauschen und sogar Callbacks für C-Funktionen in Java entwickeln. Im Vergleich zum Java Native Interface (JNI) ist das wesentlich sicherer, und man kommt in der Regel ohne Griff zum C-Compiler aus.

In Listing 4 sehen Sie ein rudimentäres Beispiel zur Verwendung der FFM-API. Die ersten beiden Zeilen schaffen per »Linker« und »SymbolLookup« Zugriff auf die verfügbaren Bibliotheken, Zeile 3 sucht nach der Funktion »strlen()«. Damit lässt sich dann ein »MethodHandle« erzeugen, dem man neben der Funktion noch die Signatur (Zeile 6: »long« raus, Speicheradresse rein) übergibt.

Damit klappt der Zugriff; als Nächstes gilt es, die Eingabewerte in das Off-Heap-Memory zu übertragen und die Funktion aufzurufen. Für den kontrollierten Zugriff auf das Off-Heap-Memory gibt es die Klasse »Arena« (Zeile 10), die sich auch automatisch um die Freigabe des angeforderten Speichers kümmert. Per »allocateFrom()« (Zeile 11) überträgt man den Java-String, die Speicheradresse wird dann als Eingabeparameter beim Aufrufen des »strlen«-Handles in Zeile 12 genutzt.

Nun ist die Ermittlung der richtigen Namen und Signaturen für die Funktionen etwas lästig, letztlich steht das alles ja auch in der Header-Datei (Zeile 4). Das Werkzeug Jextract [10] erspart diese Arbeit und erzeugt auf Basis der Header-Dateien statische Java-Klassen. Das macht die ersten sechs Zeilen von Listing 4 überflüssig, Zeile 15 zeigt den Aufruf mit dem per Jextract generierten Code. Man muss sich dann nur noch um die Memory-Behandlung kümmern.

Listing 4

Foreign Function & Memory API (JEP 454)

Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment strlen = stdlib.find("strlen").orElseThrow();
// size_t strlen(const char* str);
MethodHandle handle = linker.downcallHandle(strlen,
  FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
String string ="LinuxMagazin";
try (Arena offHeap = Arena.ofConfined()) {
  MemorySegment addr = offHeap.allocateFrom(string);
  long length =(long) handle.invoke(addr);
  System.out.println("länge " + length);
  // mit Jextract
  long l2 = string_h.strlen(addr);
  } catch (Throwable exp) {
    [...]
}

Im Review

Neben diesen neuen oder abgeschlossenen JEPs liegen in Java 22 vier weitere mit wenigen Änderungen zum Review vor. Bei den String Templates (JEP 459), der Structured Concurrency (JEP 462) und den Scoped Values (JEP 464) gab es keine signifikanten Änderungen, die Vorstellung aus unserem Artikel zu Java 21 [2] gilt unverändert auch für Java 22. Bei den Implicitly Declared Classes and Instance Main Methods (JEP 463) wurden die Regeln für das Finden der Main-Methode vereinfacht. Alle vier JEPs sollen mit Java 23 oder spätestens 24 fertiggestellt sein.

Direktzugriff

Seit Java 11 kann man über das Kommando »java« einzelne Java-Dateien direkt ohne Kompilieren ausführen. Das klappte bislang jedoch nur für eine einzelne Datei, alles andere musste kompiliert im Classpath vorliegen. Dank Launch Multi-File Source-Code Programs (JEP 458) fällt diese Einschränkung nun weg.

In Listing 5 und Listing 6 sehen Sie den Inhalt von zwei Quelldateien, die im selben Verzeichnis liegen. Wie Abbildung 2 zeigt, kann man sie ohne Kompilieren direkt starten. Dank JEP 463 (Implicitly Declared Classes …) erfordert das noch nicht mal eine Klasse oder eine richtige Main-Methode. Die Klasse »ScriptPartner« aus Listing 6 wird in Listing 5 über die normalen Namensregeln gefunden (identischer Datei- und Klassenname, pro Java-Package ein Verzeichnis).

Abbildung 2: Der Aufruf des Codes aus <a href="#artRef-l5">Listing&nbsp;5</a> und <a href="#artRef-l6">Listing&nbsp;6</a> samt der resultierenden Ausgaben.

Abbildung 2: Der Aufruf des Codes aus Listing 5 und Listing 6 samt der resultierenden Ausgaben.

In Listing 6 enthält die Zeile 5 noch ein kleines Gimmick ohne JEP, das neue »ListFormat«. Es fasst Listen unter Berücksichtigung der Systemsprache zu einem String zusammen. In Abbildung 2 wird nach dem ersten Durchlauf die Sprache von Deutsch auf »C« umgestellt, dementsprechend erscheinen in der Ausgabe statt des deutschen “und” nun ein weiteres abschließendes Aufzählungskomma und das Wort “and”.

Listing 5

ImplicitDeclaredClass.java (JEP 463)

void main() {
  System.out.println(gruesse());
  new ScriptPartner().methode();
}
public String gruesse() {
  return "Hallo " + System.getProperty("user.name");
}

Listing 6

ScriptPartner.java (JEP 458)

public class ScriptPartner {
  public void methode() {
    System.out.println("ScriptPartner#methode");
    var daltons = List.of("Joe", "William", "Jack", "Averall");
    System.out.println(ListFormat.getInstance().format(daltons));
  }
}

Fazit …

Das aktuelle Java-Release 22 bringt eine ganze Menge sinnvoller Features mit, die sowohl Programmieranfänger als auch Profis erfreuen. Gerade zum Lernen und Testen kommt man dank JEP 463 (Launch Multi-File …) ohne eine aufwendige IDE aus – die Kommandozeile, Java und ein einfacher Texteditor genügen.

Auf der anderen Seite des Spektrums liegen die Class-File- und FFM-APIs, die sich eher an Poweruser richten. Ein Wermutstropfen bei FMM ist die fehlende C++-Unterstützung, hier wird man weiter kleine C++-nach-C-Brücken implementieren müssen.

Normale Java-Anwender können mit den JEPs 447 und 461 (Statements before Super, Stream Gatherers) einfacheren und besser lesbaren Code schreiben, Unnamed Variables (JEP 456) stellen die statische Code-Analyse ruhig.

… und Ausblick

Das Umfeld war auch nicht untätig, besonders im Bereich polyglotter Programmierung. Mit dem WebAssembly-Port von Java, CheerpJ [12], lassen sich lange nach dem Tod des Java-Applets wieder Java-Anwendungen im Webbrowser ausführen. Aktuell unterstützt CheerpJ nur den Bytecode von Java 8, Abbildung 3 zeigt die alte Swing-Demo von 2014 im aktuellen Firefox. Für einfache Anwendungen genügt die Performance, Minecraft spielt sich jedoch etwas zäh.

Abbildung 3: Return of the Applet: Java&nbsp;8 im Webbrowser mit CheerpJ.

Abbildung 3: Return of the Applet: Java 8 im Webbrowser mit CheerpJ.

Andersherum lassen sich Wasm-Bibliotheken von Java aus per Wasmer-java nutzen [13]. Mit der TornadoVM [14] kann man Berechnungen von Java auf die Grafikkarte und FPGAs auslagern, die GraalVM [15] verspricht, Spring Boot dank nativer Kompilierung binnen Sekunden zu starten. Es finden sich reichlich weitere Neuerungen, die alle den ein oder anderen Blick wert sind, bevor im September 2024 dann das nächste Feature-Release Java 23 erscheint. (jlu)

Der Autor

Carsten Zerbst erstellt mit seinem Team Software für Ingenieure, vom simplen Konverter bis hin zur unternehmenskritischen Integrationslösung. Das Team sucht für 2024 in Hamburg noch Verstärkung und bietet interessante Abschlussarbeiten für Informatik- und Schiffbaustudierende an.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben