Die erste Java-Version mit Long-Term-Support seit Java 8

Java, die Elfte

Mit Java 11 ist im September 2018 das erste Release, nach Java 8, mit Long-Term-Support erschienen. Neben den neuen Features und Änderungen geht es in diesem Artikel somit auch um das neue Support-Modell von Java. Und auch ein Blick in die nähere Zukunft von Java soll nicht fehlen.

Am 25. September 2018 war es soweit und das finale Release von Java 11 wurde veröffentlicht. Es ist das zweite Release in dem auf sechs Monate verkürzten Releasezyklus. Neben einigen neuen Features und vielen kleinen Änderungen sorgte vor allem die Diskussion um Support, Lizenzierung und die Verteilung von Builds des JDKs im Vorhinein für Unruhe.

Dieser Artikel stellt die insgesamt 17 neu umgesetzten JDK Enhancement Proposals, einige Änderungen und Erweiterungen an bestehenden Klassen und Interfaces sowie die Diskussion um das neue Support-Modell vor.

Tabelle 1: In Java 11 umgesetzte JDK Enhancement Proposals
JEP Name
181 Nest-Based Access Control
309 Dynamic Class-File Constants
315 Improve Aarch64 Intrinsics
318 Epsilon: A No-Op Garbage Collector – Experimental
320 Remove the Java EE and CORBA Modules
321 HTTP Client – Standard
323 Local-Variable Syntax for Lambda Parameters
324 Key Agreement with Curve25519 and Curve448
327 Unicode 10
328 Flight Recorder
329 ChaCha20 and Poly1305 Cryptographic Algorithms
330 Launch Single-File Source-Code Programs
331 Low-Overhead Heap Profiling
332 Transport Layer Security 1.3
333 ZGC: A Scalable Low-Latency Garbage Collector – Experimental
335 Deprecate the Nashorn JavaScript Engine
336 Deprecate the Pack200 Tools and API

Erweiterung von var für Lambda Parameter

Das für Entwickler wohl größte Feature in JDK 10 war die Syntax für lokale Variablen und damit das teilweise Verzichten einer Typangabe auf der linken Seite. Mit JEP 323 wurde die Nutzung nun auf eine weitere Stelle, nämlich die Parameter von Lambda-Funktionen, erweitert.

Zwar konnte bei Lambda-Parametern bereits seit JDK 8 auf die Angabe eines Typs verzichtet werden, dieser konnte dann jedoch nicht mehr mit einer Annotation versehen werden. Annotationen für Parameter müssen nämlich immer am Typ des Parameters stehen. Mit JDK 11 lässt sich ein solcher Fall nun mit dem Wort var (s. Listing 1) lösen.

(@Nonnull var function, @Nullable var param) -> function.apply(param)
Listing 1: Lokale Variablen-Syntax für Lambda-Parameter

Finale Version des neuen HTTP-APIs

Bereits in JDK 9 wurde im Rahmen von JEP 110 (HTTP/2 Client – Incubator) ein neues API für HTTP-Clients hinzugefügt. Die beiden primären Ziele waren dabei der Support von HTTP/2 und WebSockets sowie von blockierenden und nicht blockierenden Programmiermodellen.

Da die JDK Maintainer jedoch noch mehr Feedback für dieses API erhalten wollten, wurde es in JDK 9 lediglich als Inkubator-Modul aufgenommen und anschließend bereits im JDK 10 verbessert.

Mit JDK 11 sind die Maintainer nun der Meinung, dass die Programmierschnittstelle fertig ist, und sie integrieren dieses API mit dem JEP 321 als neues Modul. An der generellen Programmierschnittstelle hat sich dabei wenig geändert.

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://www.google.de"))
    .build();

client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println)
    .join();
Listing 2: Neues HTTP-API

Listing 2 zeigt, wie das neue API verwendet wird, um ein HTTP GET-Anfrage auf die URI http://www.google.de abzusenden. Der Body der Antwort wird anschließend in einen String umgewandelt und auf der Konsole ausgegeben. Zudem kommt, am Methodennamen sendAsync zu erkennen, die asynchrone Variante der Programmierschnittstelle zum Einsatz.

Ausführen von Einzeldatei-Java-Programmen

Ein weiteres neues Feature wurde mit JEP 330 umgesetzt. Es erlaubt das Ausführen einer einzelnen Java-Quelldatei, ohne weitere Abhängigkeiten, ohne dass diese vorher explizit kompiliert werden muss. Somit lässt sich eine Klasse wie in Listing 3 direkt mit dem Befehl java Greet.java ausführen.

public class Greet {
    public static void main(String[] args) {
        System.out.println("Hello, world!"); }
    }
}
Listing 3: Greet.java

In einer zweiten Variante wurde zudem noch Support für direkt ausführbare Dateien eingebaut. Für diese wird innerhalb der Datei als erste Zeile eine Shebang-Direktive eingefügt. Listing 4 erweitert das Beispiel aus Listing 3 um diese Direktive.

#!/usr/bin/java --source 11
public class Greet {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}
Listing 4: greet

Diese Datei darf nun nicht mehr auf .java enden, da sie streng genommen keine reine Java-Quelldatei mehr ist. Wird nun dafür gesorgt, dass diese Datei ausführbar ist, zum Beispiel mit chmod +x greet unter Linux/MacOS, dann kann diese direkt mit dem Befehl ./greet ausgeführt werden und die Begrüßung erscheint.

Dieses Feature erleichtert es somit, kleinere Skripte in Java zu schreiben. Die Limitierung auf eine einzelne Klasse und kleinere Fremdbibliotheken schränkt das Feature jedoch wieder ein.

JVM/Bytecode-Erweiterungen

Im Rahmen von Java 11 wurden drei JEPs umgesetzt, die sich primär um Erweiterungen der JVM selbst beziehungsweise des Bytecode-Formats gekümmert haben.

In JEP 181 ging es darum, direkten Support für Zugriffsregeln bei der Nutzung von inneren Klassen zu implementieren. Angenommen, es existiert eine Klasse Host und innerhalb dieser ist wiederum eine Klasse Member definiert. Die JVM erlaubt bei diesem Konstrukt, dass die innere Klasse Member auch auf private Instanzvariablen von Host zugreifen darf (s. Listing 5).

public class Host {

    private final int i;

    public Host(int i) {
        this.i = i;
    }

    public Member createMember() {
        return new Member();
    }

    public class Member {

        public void print() {
            System.out.println(i);
        }
    }
}
Listing 5: Zugriff von einer inneren Klasse auf eine private Instanzvariable der äußeren Klasse

Wird diese Datei kompiliert, existieren anschließend zwei Class-Dateien, eine pro Klasse. Wird nun der vom Compiler erzeugte Bytecode betrachtet, lässt sich erkennen, dass der Compiler ein paar Dinge hinzufügt, damit diese Verschachtelung funktioniert (s. Listing 6).

Compiled from "Host.java"
public class Host {
    ...
    public Host$Member createMember();
        Code:
            0: new #3 // class Host$Member
            3: dup
            4: aload_0
            5: invokespecial #4 // Method Host$Member."<init>":(LHost;)V
            8: areturn

    static int access$000(Host);
        Code:
            0: aload_0
            1: getfield #1 // Field i:I
            4: ireturn
}

Compiled from "Host.java"
public class Host$Member {
    final Host this$0;

    public Host$Member(Host);
        Code:
            0: aload_0
            1: aload_1
            2: putfield
            5: aload_0
            6: invokespecial
            9: return

    public void print();
        Code:
            0: getstatic
            3: aload_0
            4: getfield
            7: invokestatic
            10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
            13: return
}
Listing 6: Bytecode von Java 10 oder früher für verschachtelte (nested) Klassen

Zum einen erzeugt der Compiler für die Klasse Host eine zusätzliche statische Methode access$000 und zum zweiten erhält die Klasse Member eine Instanzvariable von Host, welche über den Konstruktor gesetzt wird. Durch die Kombination dieser beiden generierten Konstrukte ist nun der Zugriff auf die Variable i von Host aus der Methode print in Member möglich.

Wenn die Kompilierung allerdings mit Java 11 durchgeführt wurde, ergibt sich anderer Bytecode (s. Listing 7). Es lässt sich erkennen, dass nun keine statische Methode access$000 mehr vom Compiler erzeugt wird.

Compiled from "Host.java"
public class Host {
    ...
}

Compiled from "Host.java"
public class Host$Member {
    ...
    public void print();
        Code:
            0: getstatic
            3: aload_0
            4: getfield
            7: getfield
            10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
            13: return
}
Listing 7: Bytecode von Java 11 für verschachtelte Klassen

Diese sogenannte Bridge-Methode war bis Java 11 notwendig, um die Zugriffsregeln von Java nicht zu verletzen. Immerhin versucht hier eine Klasse, direkt auf eine private Instanzvariable einer anderen Klasse zuzugreifen. Dank JEP 181 ist dieser Trick des Compilers nun nicht mehr notwendig. Im Bytecode werden dazu die Beziehungen von verschachtelten Klassen festgehalten. Die Zugriffsregelungen werden anschließend mit diesen Informationen geprüft. Diese Änderung hat auch Auswirkungen auf reflexive Zugriffe (s. Listing 8).

public class Host {

    private final int i;
    ...
    public class Member {

        public void print() throws Exception {
            Field field = Host.class.getDeclaredField("i");
            Object result = field.get(Host.this);
            System.out.println(result);
        }
    }
}
Listing 8: Reflexiver Zugriff auf private Instanzvariable der äußeren Klasse

Mit Java 10 oder früher wird hier eine java.lang.IllegalAccessException geworfen, obwohl das Feld laut Zugriffsregelungen für die Klasse Member sichtbar ist. Es musste somit noch field.setAccessible(true) aufgerufen werden. Mit Java 11 funktioniert es nun wie erwartet.

Weiterhin wurde mit JEP 309 ein eine neue Bytecode-Instruktion CONSTANT_Dynamic eingeführt, und JEP 315 optimiert intrinsische Funktionen für Strings, Arrays und einige Funktionen aus java.lang.Math auf ARM CPUs.

Neue experimentelle Garbage-Collectoren

Die Aufgabe eines Garbage-Collectors (GC) besteht darin, Objekte auf dem Heap zu allokieren und nicht mehr benötigte Objekte vom Heap zu entfernen, um wieder Platz für weitere Objekte zu schaffen. Java 11 bringt hierzu insgesamt zwei neue, experimentelle GCs mit.

Der Epsilon GC (JEP 318) kann lediglich neue Objekte allokieren. Ein Aufräumen des Heaps ist nicht vorgesehen. Demnach wird eine Anwendung, die mit ihm gestartet wurde, mit einer OutOfMemoryException enden, sobald der für die JVM zur Verfügung stehende Speicher einmal komplett aufgebraucht wurde. Es wird kein Versuch unternommen, nicht mehr erreichbare Objekte freizugeben und damit wieder Speicher zur Verfügung zu stellen.

Auf den ersten Blick erscheint ein GC, der keinen Speicher freiräumt, als eher nutzlos. Auf den zweiten gibt es dann doch ein paar Anwendungsfälle, in denen ein solcher GC hilft:

  • Er kann dazu verwendet werden, eine Base-Line zu erzeugen und diese mit anderen GCs zu vergleichen. Somit lässt sich ermitteln, wie viel Overhead ein „richtiger“ GC benötigt.
  • Das Testen von Code unter Speicherdruck wird einfacher.
  • Sehr kurze Jobs, die nie in Gefahr geraten, eine GC zu benötigen, werden beschleunigt.
  • Er kann als letztes Mittel zum Optimieren von Applikationen verwendet werden, bei denen genau bekannt ist, wie viel Speicher benötigt wird, oder bei denen ein Neustart performanter als eine GC ist.

Um den Epsilon GC verwenden zu können, muss beim Start der JVM der Schalter -XX:+UseEpsilonGC angegeben werden. Da es sich um ein experimentelles Feature handelt, muss zudem -XX:+UnlockExperimentalVMOptions angegeben werden.

Als zweiter GC wurde in JEP 333 noch der ZGC umgesetzt. Das primäre Ziel dieses GCs ist es, dass keine der durch den GC verursachten Pausen länger als zehn Millisekunden dauert. Nebenbei soll der Durchsatz im Verhältnis zum aktuellen Standard GC G1 um nicht mehr als 15 Prozent sinken.

Wie auch beim Epsilon GC muss der ZGC beim Starten der JVM mit -XX:UseZGC explizit angeschaltet werden. Zudem ist dieser aktuell nur auf Linux 64bit vorhanden.

Sicherheit

Auch Themen rund um den Bereich Sicherheit haben es ins JDK 11 geschafft. MitJEP 332 wurde der TLS-Standard in Version 1.3 implementiert.

Zudem wurden mit den JEPs 324 und 329 die zwei neuen elliptischen Kurven Curve25519 und Curve448 sowie die beiden Algorithmen ChaCha20 und Poly1305 hinzugefügt.

Aufräumarbeiten

Lange Zeit war es so, dass im JDK immer nur Dinge hinzugefügt, aber nicht entfernt wurden. Dies ändert sich nun langsam, aber sicher. Im JDK 11 entfällt ein komplettes API, zwei weitere wurden als Deprecated markiert und werden voraussichtlich in Zukunft vollständig entfernt.

JEP 320 entfernt das Java EE und CORBA API aus dem JDK. Auf den ersten Blick erscheint dies für viele Anwendungen kein Problem zu sein, setzen doch viele neue Anwendungen auf HTTP oder Messaging.

Ein Teil des Java EE APIs ist jedoch auch JAXB. Setzt eine Anwendung also auf diesen Standard zur Verarbeitung von XML-Dokumenten, muss bei der Migration auf JDK 11 etwas Arbeit eingeplant werden. Glücklicherweise ist es hierbei in der Regel damit getan, eine JAXB-Implementierung als externe Abhängigkeit hinzuzufügen. Dies hat zwar den Nachteil einer weiteren externen Abhängigkeit, ermöglicht aber auch einen einfacheren Einsatz von neueren Versionen. Wer dies einmal benötigte, Stichwort lib/endorsed, weiß den neuen Weg zu schätzen.

Weiterhin wurden mit JDK 11 Nashorn (JEP 335) und Pack200 (JEP 336) als Deprecated markiert. Zudem sind diese auch für eine Entfernung in einem späteren JDK vorgesehen. Wird also Nashorn zur Ausführung von JavaScript oder Pack200 zur Komprimierung verwendet, entsteht in Zukunft vermutlich Migrationsaufwand.

Sollten sich jedoch Maintainer für diese Features finden, kann es auch passieren, dass diese weiter im JDK vorhanden sein werden. Alternativ werden diese beiden Features als eigenständige Open-Source-Projekte weitergeführt. Idealerweise reicht anschließend eine Einbindung als externe Abhängigkeit, wie auch mit JAXB.

Einsichten

Die JVM bietet schon lange diverse Möglichkeiten, um während der Laufzeit an relevante Metriken und Informationen von ihr zu gelangen. JDK 11 erweitert diese durch die beiden JEPS 328 und 331.

JEP 328 erweitert das bereits mit JEP 167 (Event-Based JVM Tracing) bestehende Framework, um während der Laufzeit einer JVM Trace-Events zu produzieren.

Die Funktionalität existierte dabei schon länger innerhalb des OracleJDKs, war jedoch nur zahlenden Kunden vorbehalten. Im Zuge der Angleichung von Open- und OracleJDK hat Oracle dieses Feature Open Source gestellt und dem OpenJDK übergeben.

Das JDK wird somit um die folgenden Trace-Event-Features erweitert:

  • Ein Java-API, um aus Java heraus Events erzeugen und lesen zu können.
  • Einen Puffer-Mechanismus und ein binäres Datenformat.
  • Die Möglichkeit, zur Laufzeit nur bestimmte Events zu erzeugen beziehungsweise diese generell zu filtern.

Zudem werden nun an diversen Stellen innerhalb des JDKs neue Events erzeugt, die eine detailliertere Überwachung erlauben.

Gleichzeitig ermöglicht JEP 331 ein verbessertes Profiling des Heaps der JVM. Somit ist es nun für Tools wie VisualVM, Java Flight Recorder oder YourKit einfacher und performanter, mitzubekommen, wann Heap für ein Objekte allokiert oder wann dieser wieder freigegeben wird.

Unicode-Upgrade

Teil von fast jedem JDK-Update ist ein Update des Unicode-Standards. Basierte JDK 10 noch auf Unicode 8.0, wird mit dem JDK nun Unicode 10.0 unterstützt. Somit können nun, dank JEP 327, über 16.000 weitere Unicode-Zeichen verwendet werden.

Erweiterungen von existierenden Klassen/Interfaces

Neben den größeren Features wurden auch im JDK 11 wieder bereits bestehende Klassen und Interfaces erweitert.

Im java.io-Paket haben die beiden Klassen FileReader und FileWriter nun einen weiteren Konstruktor, über den sich das Encoding der Datei angeben lässt. Zudem gibt es nun statische Methoden, um Input- und Outputstreams sowie Reader und Writer zu erzeugen, die nichts lesen oder sämtlichen Output verwerfen.

java.lang.String wurde direkt um sechs neue Methoden ergänzt. Die drei Methoden strip(), stripLeading() und stripTrailing() können genutzt werden, um Whitespace am Anfang, Ende oder an beiden Stellen zu entfernen. Der Unterschied von strip() zu trim() besteht darin, dass strip() alle Zeichen entfernt für die Character::isWhitespace() true ergibt. trim() entfernt hingegen alle Zeichen, deren Zahlenwert kleiner oder gleich 20 ist.

Zusätzlich wurde String noch um die Methode isBlank() ergänzt, die den Einsatz des Musters trim().isEmpty() ablöst. Vorher muss allerdings bei Bedarf weiterhin geprüft werden, ob der zu testende String ungleich null ist.

Die beiden Methoden repeat(int), um einen gegebenen String x-mal zu wiederholen, und lines(), um einen String in einen Stream mit allen Zeilen zu konvertieren, vervollständigen die Erweiterungen von String.

In java.util.Optional wurde nach langer Diskussion die Methode isEmpty() ergänzt, und java.util.function.Predicate wurde um eine statische Methode not erweitert, welche das als Parameter übergebene Predicate negiert. Weiterhin gibt es in java.util.regex.Pattern mit der Methode asMatchPredicate() eine direkte Möglichkeit, ein Pattern als Filter für Streams zu verwenden.

Zwei besondere Änderungen finden sich in der Klasse java.lang.Thread. Hier wurden die Methoden stop(Throwable) und destroy() entfernt. Dies ist insofern besonders, da historisch im JDK nur äußerst selten Methoden wirklich entfernt wurden. Da beide Methoden jedoch bereits seit einiger Zeit lediglich eine Exception geworfen haben, sollte diese Änderung wenig alten Code brechen.

Ein vollständiger Report über alle Änderungen lässt sich mit dem JDK-API Diff Report Generator erzeugen. Einen veröffentlichten Report für JDK 11 gibt es bereits fertig unter https://gunnarmorling.github.io/jdk-api-diff/jdk10-jdk11-api-diff.html.

Support-Modell

Vor dem Release von JDK 11 kamen diverse Diskussionen auf, wie die Zukunft von Support und Builds wohl aussehen würde. Jetzt, nach dem Release, gibt es ein klares Bild.

Oracle stellt insgesamt zwei Builds des OpenJDKs zur Verfügung:

  • Das Oracle JDK steht dabei unter einer Oracle spezifischen Lizenz und darf in Produktion nicht ohne bezahlten Support eingesetzt werden.
  • Der zweite Build ist der offizielle OpenJDK Build. Dieser steht unter der freien Lizenz GPL2 mit Classpath-Erweiterung zur Verfügung.

Diese Builds werden allerdings nur sechs Monate zur Verfügung gestellt und auch der Support endet mit Erscheinen des nächsten JDK-Releases.

Soll ein von Oracle gebautes und unterstütztes JDK eingesetzt werden, müssen demnach Lizenzkosten gezahlt werden oder es muss alle sechs Monate ein Upgrade des JDKs auf die aktuellste Version durchgeführt werden.

Als Alternative hierzu gibt es die AdoptOpenJDK Community. Diese stellt, durch Sponsoren finanziert, eigene Builds des OpenJDKs zur Verfügung. Dabei ist geplant, dass auch nach dem Erscheinen von JDK 12, bis mindestens September 2022, Builds von JDK 11 zur Verfügung zu stellen. Somit kann dieses JDK auch ohne Lizenzkosten für einen langen Zeitraum eingesetzt werden.

Natürlich gibt es auch weiterhin kommerzielle JDK Builds von Firmen, wie Azul Systems, Red Hat, SAP oder anderen. Auch diese können, wie das Oracle JDK, in der Regel nur mit Support, und somit bezahlt, in Produktion eingesetzt werden.

Fazit

Im Vorfeld von JDK 11 hat vor allem die Diskussion um den Support dominiert. Mit der AdoptOpenJDK Community hat sich hier eine neue Partei ins Spiel gebracht, die auch weiterhin freie und somit kostenlose Builds zur Verfügung stellt. Alternativ kann bei diversen Herstellern auch kommerzieller Support erworben werden. Abseits von dieser Thematik bringt JDK 11 17 neue JEPs und viele weitere kleine API-Änderungen mit.

Da es sich beim JDK 11 um ein LTS-Release handelt, bietet sich ein direktes Upgrade auf dieses an. Wer noch auf JDK 8 geblieben ist, sollte sich überlegen, direkt auf 11 zu gehen, da der Aufwand quasi identisch zu einem Upgrade von 8 auf 9 ist. Ein guter Guide für eine Migration von 8 auf 11 bietet der englische Blog Post „Migrate Maven Projects to Java 11“ von Benjamin Winterberg.

Der Leitsatz „Nach dem JDK ist vor dem JDK“ gilt vor allem durch den verkürzten Releasezyklus auf sechs Monate. So ist bereits für den 19.03.2019 JDK 12 geplant. Aus Entwicklersicht sind dabei bereits jetzt die beiden Preview Features Switch Expressions (JEP 325) und Raw String Literals (JEP 326) interessant und bereits vorab einen Blick Wert. Listing 9 zeigt die aktuell geplante Syntax für beide Features.

// Switch Expressions
static String intToString(int i) {
    return switch (i) {
        case 1 -> "one";
        case 2 -> "two";
        case 3 -> "three";
        default -> "" + i;
    }
}

// Raw String Literal
String html = `
  <html>
    <body>
      <p>Hello World.</p>
    </body>
</html>`;
Listing 9: JDK 12 Switch Expressions und Raw String Literals

Auch in Zukunft bleibt die Entwicklung von Java und dem JDK spannend. Die Reduzierung der Zeit zwischen Releases auf sechs Monate ermöglicht einen kontinuierlichen Zufluss von kleinen und mittleren Features. Somit besteht die Möglichkeit, schrittweise Neuerungen einzuführen, von denen die gesamte Community profitiert. Weiterhin besteht für große Codebasen die Möglichkeit, von LTS- zu LTS-Release zu migrieren und somit nicht alle sechs, sondern nur alle drei Jahre einen Versionssprung vollziehen zu müssen.

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more