Module entwickeln und einbinden

Das JPMS (Java Platform Module System), auch unter dem Projektnamen Jigsaw bekannt, ist eine der größten Neuerungen in Java 9. Im Mittelpunkt steht die starke Kapselung: Implementierungsdetails kann ein Modul geheim gehalten. Selbst Hilfscode innerhalb des Moduls, auch wenn er öffentlich ist, darf nicht nach außen dringen. Zweitens kommt eine Abstraktion von Verhalten über Schnittstellen hinzu, die interne Klassen aus dem Modul implementieren können, wobei dem Nutzer die konkreten Klassen nicht bekannt sind. Als dritten Punkt machen explizite Abhängigkeiten die Interaktion mit anderen Modulen klar. Eine grafische Darstellung hilft auch bei großen Architekturen, die Übersicht über Nutzungsbeziehungen zu behalten.

Wer sieht wen

Klassen, Pakete und Module lassen sich als Container mit unterschiedlichen Sichtbarkeiten sehen:

· Ein Typ, sei es Klasse oder Schnittstelle, enthält Attribute und Methoden

· Ein Paket enthält Typen.

· Ein Modul enthält Pakete.

· Private Eigenschaften in einem Typ sind nicht in anderen Typen sichtbar.

· Nicht öffentliche Typen sind in anderen Paketen nicht sichtbar.

· Nicht exportierte Pakete sind außerhalb eines Moduls nicht sichtbar.

Ein Modul ist definiert

1. durch einem Namen,

2. durch die Angabe, was es exportiert möchte und

3. welches Modul es zur Arbeit selbst benötigt.

Interessant ist der zweite Aspekt, also dass ein Modul etwas exportiert. Wenn nichts exportiert wird, ist auch nichts sichtbar nach außen. Alles, was Außenstehende sehen sollen, muss in der Modulbeschreibung aufgeführt sein – nicht alle öffentlichen Typen des Moduls sind standardmäßig öffentlich, dann wäre das kein Fortschritt zu JAR-Dateien. Mit dem neuen Modulsystem haben wir also eine ganz andere Sichtbarkeit. Aus der Viererbande public, private, paketsichtbar, protected bekommt public eine viel feinere Abstufung. Denn was public ist bestimmt das Modul, und das sind

– Typen, die das Modul für alle exportiert,

– Typen für explizit aufgezählte Module,

– alle Typen im gleichen Modul.

Der Compiler und die JVM achten auf die Einhaltung der Sichtbarkeit, und auch Tricks mit Reflection sind nicht mehr möglich, wenn ein Modul keine Freigabe erteilt hat.

Modultypen

Wir wollen uns in dem Abschnitt intensiver mit drei Modultypen beschäftigen. Wenn wir neue Module schreiben, dann sind das benannte Module. Daneben gibt es aus Kompatibilitätsgründen automatische Module und unbenannte Module, mit denen wir vorhandene JAR-Dateien einbringen können. Die Bibliothek der Java SE ist selbst in Module unterteilt, wir nennen sie Plattform-Module.

Die Laufzeitumgebung zeigt mit einem Schalter –list-modules alle Plattform-Module an.

Beispiel: Liste die ca. 70 Module auf:

$ java –list-modules

java.activation@9

java.base@9

java.compiler@9

oracle.desktop@9

oracle.net@9

Im Ordner C:\Program Files\Java\jdk-9\jmods liegen JMOD-Dateien.

Plattform-Module und JMOD-Beispiel

Das Kommandozeilenwerkzeug jmod zeigt an, was ein Modul exportiert und benötigt. Nehmen wir die JDBC-API für Datenbankverbindungen als Beispiel; die Typen sind in einem eigenen Modul mit den Namen java.sql.

C:\Program Files\Java\jdk-9\bin>jmod describe ..\jmods\java.sql.jmod

java.sql@9

exports java.sql

exports javax.sql

exports javax.transaction.xa

requires java.base mandated

requires java.logging transitive

requires java.xml transitive

uses java.sql.Driver

platform windows-amd64

Wir können ablesen:

· den Namen

· die Pakete, die das Modul exportiert: java.sql, javax.sql und javax.transation.xa

· die Module, die java.sql benötigt: java.base ist hier immer drin, dazu kommen java.logging und java.xml

Die Meldung mit „uses“ steht im Zusammenhang mit dem Service-Locator – wir können das vorerst ignorieren. Die Kennung über die Plattform (windows-amd64) schreibt jmod mit hinein, es ist die Belegung der System-Property os.arch auf dem Build-Server.

Verbotene Plattformeigenschaften nutzen, –add-exports

Als Sun von vielen Jahren mit der Entwicklung der Java-Bibliotheken begann, kamen viele interne Hilfsklassen mit in die Bibliothek. Viele beginnen mit den Paketpräfixen com.sun und sun. Die Typen wurden immer als interne Typen kommuniziert, doch bei einigen Entwicklern war die Neugierde und das Interesse so groß, dass die Warnungen von Sun/Oracle ignoriert wurden. In Java 9 kommt der große Knall, da public nicht mehr automatisch public für alle Klassen außerhalb des Moduls ist; die internen Klassen werden nicht mehr exportiert, sind also nicht mehr benutzbar. Es kommt zu einem Compilerfehler, wie in folgendem Beispiel:

public class ShowRuntimeArguments {

public static void main( String[] args ) throws Exception {

System.out.println( java.util.Arrays.toString( jdk.internal.misc.VM.getRuntimeArguments() ) );

}

}

Unser Programm greift auf die VM-Klasse zurück, um die eigentliche Belegung der Kommandozeile zu erfragen. In der main(String[] args)-Methode sind in args keine VM-Argumente enthalten.

Übersetzen wir das Programm, gibt es einen Compilerfehler (nicht bei einem Java 8-Compiler):

$ javac ShowRuntimeArguments.java

ShowRuntimeArguments.java:3: error: package jdk.internal.misc is not visible

System.out.println( java.util.Arrays.toString( jdk.internal.misc.VM.getRuntimeArguments() ) );

^

(package jdk.internal.misc is declared in module java.base, which does not export it to the unnamed module)

1 error

The module java.base does not export the package jdk.internal.misc., so the type jdk.internal.misc.Unsafe is not accessible – as a consequence compilation fails.

Das Problem dokumentiert der Compiler. Es ist dadurch zu lösen, indem mit dem Schalter –add-exports aus dem Modul java.base das Paket jdk.internal.misc unserer Klasse bereitgestellt wird. Die Angabe ist für den Compiler und für die Laufzeitumgebung zu setzen:

$ javac –add-exports java.base/jdk.internal.misc=ALL-UNNAMED ShowRuntimeArguments.java

$ java ShowRuntimeArguments

Exception in thread "main" java.lang.IllegalAccessError: class ShowRuntimeArguments (in unnamed module @0x77afea7d) cannot access class jdk.internal.misc.VM (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @0x77afea7d

at ShowRuntimeArguments.main(ShowRuntimeArguments.java:3)

$ java –add-exports java.base/jdk.internal.misc=ALL-UNNAMED ShowRuntimeArguments

[–add-exports=java.base/jdk.internal.misc=ALL-UNNAMED]

Wir sehen die Ausgabe, das Programm funktioniert.

Eine Angabe wie java.base/jdk.internal.misc, bei der vorne das Modul steht und hinter dem / der Paketname, ist oft ab Java 9 anzutreffen. Hinter dem Gleichheitszeichen steht entweder unser Paket, welches die Typen in jdk.internal.misc sehen kann, oder – wie in unserem Fall – ALL-UNNAMED.

jdeps

Hätten wir das Programm schon erfolgreich unter Java 8 übersetzt, würde es zur Laufzeit ebenfalls knallen. Da es nun sehr viel Programmcode gibt, haben die Java-Entwickler bei Oracle das Kommandozeilenprogramm jdeps entwickelt. Es meldet, wenn interne Typen im Programm vorkommen:

$ jdeps ShowRuntimeArguments.class

ShowRuntimeArguments.class -> java.base

<unnamed> -> java.io java.base

<unnamed> -> java.lang java.base

<unnamed> -> java.util java.base

<unnamed> -> jdk.internal.misc JDK internal API (java.base)

Die Meldung „JDK internal API“ bereitet uns darauf vor, dass es gleich Ärger geben wird.

So kann relativ leicht eine große Codebasis untersucht werden und Entwicker können proaktiv den Stellen auf den Grund gehen, die problematische Abhängigkeiten haben.

Plattformmodule einbinden, –add-modules und –add-opens

Jedes Java SE-Projekt basiert auf dem Modul java.se, was diverse Modulabhängigkeiten nach sich zieht.

<pic: java.se-graph.png, „Modulabhängigkeiten von java.se“>

Diverse Module sind nicht Teil vom Modul Java SE, unter anderem sind das das Java Activation Framework, CORBA, Transaction-API, JAXB, Web-Services und interne Module, die mit jdk beginnen.

Starten wir ein Programm mit Bezug zu einem dieser Bibliotheken gibt es einen Fehler. Zunächst zum Programm, das ein Objekt automatisch in XML konvertieren soll:

public class Person {

public String name = "Chris";

public static void main( String[] args ) {

javax.xml.bind.JAXB.marshal( new Person(), System.out );

}

}

Ausgeführt auf der Kommandozeile folgt ein Fehler:

$ java Person

Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/JAXB

at Person.main(Person.java:6)

Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXB

at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)

at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)

at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

… 1 more

Wir müssen das Modul java.xml.bind (oder auch das „Über“-Modul java.se.ee) mit angeben; dafür dient der Schalter –add-modules.

$ java –add-modules java.xml.bind Person

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<person>

<name>Chris</name>

</person>

Öffnen für Reflection

Jetzt gibt es allerdings ein anderes Problem, was auffällt, wenn wir einen anderen Typ in XML umwandeln wollen:

public class Today {

public static void main( String[] args ) {

javax.xml.bind.JAXB.marshal( new java.util.Date(), System.out );

}

}

Auf der Kommandozeile zeigt sich:

$ java –add-modules java.xml.bind Today

Exception in thread "main" javax.xml.bind.DataBindingException: javax.xml.bind.JAXBException: Package java.util with JAXB class java.util.Date defined in a module java.base must be open to at least java.xml.bind module.

at java.xml.bind@9/javax.xml.bind.JAXB._marshal(Unknown Source)

at java.xml.bind@9/javax.xml.bind.JAXB.marshal(Unknown Source)

at Today.main(Today.java:4)

Caused by: javax.xml.bind.JAXBException: Package java.util with JAXB class java.util.Date defined in a module java.base must be open to at least java.xml.bind module.

at java.xml.bind@9/javax.xml.bind.ModuleUtil.delegateAddOpensToImplModule(Unknown Source)

at java.xml.bind@9/javax.xml.bind.ContextFinder.newInstance(Unknown Source)

at java.xml.bind@9/javax.xml.bind.ContextFinder.newInstance(Unknown Source)

Die zentrale Information ist „Package java.util with JAXB class java.util.Date defined in a module java.base must be open to at least java.xml.bind module“. Wir müssen etwas öffnen; dazu verwenden wir den Schalter –add-opens:

$ java –add-modules java.xml.bind –add-opens java.base/java.util=java.xml.bind Box

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<date>2017-09-02T23:20:52.170+02:00</date>

Die Option öffnet für Reflection das Paket java.util aus dem Modul java.base für java.xml.bind. Neben –add-opens gibt es das ähnliche –add-exports, was alle öffentlichen Typen und Eigenschaften zur Übersetzungs-/Laufzeit öffnet; –add-opens geht für Reflection einen Schritt weiter.

Projektabhängigkeiten in Eclipse

Um Module praktisch umzusetzen wollen wir in Eclipse zwei neue Java-Projekte aufbauen: „com.tutego.greeter“ und „com.tutego.main“. Wir legen in das Projekt „com.tutego.greeter“ eine Klasse com.tutego.insel.greeter.Greeter an und in „com.tutego.main“ die Klasse com.tutego.insel.main.Main.

Jetzt ist eine wichtige Vorbereitung in Eclipse nötig: Wir müssen einstellen, dass „com.tutego.main“ das Java-Projekt „com.tutego.greeter“ benötigt. Dazu gehen wir auf das Projekt „com.tutego.main“ und rufen im Kontextmenü Project auf; alternativ im Menüpunkt Project > Properties oder über die Tastenkombination Alt + Return. Im Dialog navigiere links auf Java Build Path und aktiviere den Reiter Projects. Wähle Add… und im Dialog wähle aus der Liste com.tutego.greeter. Ok schließt den kleinen Dialog und unter Required projects in build path taucht eine Abhängigkeit auf.

Wir können jetzt zwei einfache Klassen implementieren. Zunächst für das Projekt „com.tutego.greeter“:

com/tutego/insel/greeter/Greeter.java

package com.tutego.insel.greeter;

public class Greeter {

private Greeter() { }

public static Greeter instance() {

return new Greeter();

}

public void greet( String name ) {

System.out.println( "Hey "+ name );

}

}

Und die Hauptklasse im Projekt „com.tutego.main“:

com/tutego/insel/main/Main

package com.tutego.insel.main;

import com.tutego.insel.greeter.Greeter;

public class Main {

public static void main( String[] args ) {

Greeter.instance().greet( "Chris" );

}

}

Da wir in Eclipse vorher die Abhängigkeit gesetzt haben, gibt es keinen Compilerfehler.

Benannte Module und module-info.java

Die Modulinformationen werden über eine Datei module-info.java (kurz Modulinfodatei) deklariert, Annotationen kommen nicht zum Einsatz. Diese zentrale Datei ist der Hauptunterschied zwischen einem Modul und einer einfachen JAR-Datei. In dem Moment, in dem die spezielle Klassendatei module-info.class im Modulpfad ist, beginnt die Laufzeitumgung das Projekt als Modul zu interpretieren.

Testen wir das, indem wir in unsere Projekte „com.tutego.greeter“ und „com.tutego.main“ eine Modulinfodatei anlegen. Das kann Eclipse über das Kontextmenü Configue > Create module-info.java für uns machen.

Für das erste Modul com.tutego.greeter entsteht:

module-info.java

/**

*

*/

/**

* @author Christian

*

*/

module com.tutego.greeter {

exports com.tutego.insel.greeter;

requires java.base;

}

Und für die zweite Modulinfodatei – Kommentare ausgeblendet:

module-info.java

module com.tutego.main {

exports com.tutego.insel.main;

requires com.tutego.greeter;

requires java.base;

}

Hinter dem Schlüsselwort module steht der Name des Moduls, den Eclipse automatisch so wählt wie das Eclipse-Projekt heißt.[1] Es folgt ein Block in geschweiften Klammern.

Zwei Schlüsselworte fallen ins Auge, die wir schon vorher bemerkt haben: exports und requires.

· Das Projekt/Modul com.tutego.greeter exportiert das Paket com.tutego.insel.greeter. Andere Pakete nicht. Es benötigt (requires) java.base, wobei das Modul Standard ist, und die Zeile gelöscht werden kann.

· Das Projekt/Modul com.tutego.main exportiert das Paket com.tutego.insel.main und es benötigt com.tutego.greeter – diese Information nimmt sich Eclipse selbstständig aus den Projektabhängigkeiten.

Info: Ein Modul required ein anderes Modul aber exports ein Paket.

Beginnen wir mit den Experimenten in den beiden module-info.java-Dateien:

Modul

Aktion

Ergebnis

com.tutego.greeter

com.tutego.main

// requires java.base;

Auskommentieren führt zu keiner Änderung, da java.base immer required wird

com.tutego.greeter

// exports com.tutego.insel.greeter;

Compilerfehler im main-Modul “The type com.tutego.insel.greeter.Greeter is not accessible”

com.tutego.greeter

exports com.tutego.insel.greeter to god;

Nur das Modul god bekommt Zugriff auf com.tutego.insel.greeter. Das main-Modul meldet “The type com.tutego.insel.greeter.Greeter is not accessible”

com.tutego.greeter

exports com.tutego.insel.closer;

Hinzufügen führt zum Compilerfehler “The package com.tutego.insel.closer does not exist or is empty”

com.tutego.main

// requires com.tutego.greeter;

Compilerfehler “The import com.tutego.insel.greeter cannot be resolved”

com.tutego.main

// exports com.tutego.insel.main;

Keins, denn c.t.i.m wird von keinem Modul required

Die Zeile mit exports com.tutego.insel.greeter to god zeigt einen qualifizierten Export.

Übersetzen und Packen von der Kommandozeile

Setzen wir in der Wurzelverzeichnis vom Modul com.tutego.greeter ein Batch-Skript compile.bat:

compile.bat

set PATH=%PATH%;C:\Program Files\Java\jdk-9\bin

rmdir /s /q lib

mkdir lib

javac -d bin src\module-info.java src\com\tutego\insel\greeter\Greeter.java

jar –create –file=lib/com.tutego.greeter@1.0.jar –module-version=1.0 -C bin .

jar –describe-module –file=lib/com.tutego.greeter@1.0.jar

Folgende Schritte führt das Skript aus:

1. Setzen der PATH-Variable für die JDK-Tools

2. Löschen eines vielleicht schon angelegten lib-Ordners

3. Anlegen eines neuen lib-Orders für die JAR-Datei

4. Übersetzen der zwei Java-Dateien in den Zielordner bin

5. Anlegen einer JAR-Datei. –create (abkürzbar zu –c) instruiert das Werkzeug eine neue JAR-Datei anzulegen. –file (oder kurz –f) bestimmt den Zielnamen. –module-version unsere Versionsnummer und –C wechselt das Verzeichnis und beginnt ab dort die Dateien einzusammeln. Die Kommandozeilensyntax beschreibt Oracle auf der Webseite https://docs.oracle.com/javase/9/tools/jar.htm.

6. Die Option –describe-module (oder kurz –d) zeigt die Modulinformation und führt zu folgender (vereinfachten) Ausgabe: com.tutego.greeter@1.0 jar:file:///C:/…/com.tutego.greeter/lib/com.tutego.greeter@1.0.jar/!module-info.class exports com.tutego.insel.greeter requires java.base.

Für das zweite Projekt ist die compile.bat sehr ähnlich, dazu kommt ein Aufruf der JVM, um das Programm zu starten.

compile.bat

set PATH=%PATH%;C:\Program Files\Java\jdk-9\bin

rmdir /s /q lib

mkdir lib

javac -d bin –module-path ..\com.tutego.greeter\lib src\module-info.java src\com\tutego\insel\main\Main.java

jar -c -f=lib/com.tutego.main@1.0.jar –main-class=com.tutego.insel.main.Main –module-version=1.0 -C bin .

java -p lib;..\com.tutego.greeter\lib -m com.tutego.main

Änderungen gegenüber dem ersten Skript sind:

1. Beim Compilieren müssen wir den Modulpfad mit –module-path (oder kürzer mit -p) angeben, weil ja das Modul com.tutego.greeter required ist.

2. Beim Anlegen der JAR-Datei geben wir über –main-class die Klasse mit der main(…)-Methode an.

3. Startet die JVM das Programm, lädt sie das Hauptmodul und alle abhängigen Module. Wir geben beide lib-Order mit den JAR-Dateien an und mit –m das sogenannte initiale Modul für die Hauptklasse.

Automatische Module

JAR-Dateien spielen seit 20 Jahren eine zentrale Rolle im Java-System; sie vom einen zum anderen Tag abzuschaffen würde große Probleme bereiten. Ein Blick auf https://mvnrepository.com/repos offenbart über 7,7 Millionen Artefakte; es gehen auch Dokumentationen und andere Dateien in die Statistik ein, doch es gibt eine Größenordnung, wie viele JAR-Dateien im Umlauf sind.

Damit JAR-Dateien unter Java 9 eingebracht werden können gibt es zwei Lösungen: das JAR in den Klassenpfad oder in den neuen Modulpfad zu setzen. Kommt ein JAR in den Modulpfad und hat es keine Modulinfodatei entsteht ein automatisches Modul. Bis auf eine kleine Einschränkung funktioniert das für die meisten existierenden Java-Bibliotheken.

Ein automatisches Modul hat gewisse Eigenschaften für den Modulnamen und Konsequenzen in den Abhängigkeiten:

· Ohne Modulinfo haben die automatischen Module keinen selbstgewählten Namen, sondern sie bekommen vom System einen Namen zugewiesen, der sich aus dem Dateinamen ergibt.[2] Vereinfacht gesagt: Angehängte Versionsnummern und die Dateiendung werden entfernt und alle nicht-alphanummerischen Zeichen durch Punkte ersetzt, jedoch nicht zwei Punkte hintereinander.[3] Die Version wird erkannt. Die Dokumentation gibt das Beispiel foo-bar-1.2.3-SNAPSHOT.jar an, was zum Modulnamen foo.bar und der Version 1.2.3-SNAPSHOT führt.

· Automatische Module exportieren immer alle ihre Pakete. Wenn es also eine Abhängigkeit zu diesem automatischen Modul gibt kann der Bezieher alle sichtbare Typen und Eigenschaften verwenden.

· Automatische Module können alle anderen Module lesen, auch die unbenannten.

Auf den ersten Blick scheint eine Migration in Richtung Java 9 einfach: Alle JARs auf den Modulpfad und nacheinander Modulinfodateien anlegen. Allerdings gibt es JAR-Dateien, die von der JVM als automatisches Modul abgelehnt werden, wenn sie nämlich Typen eines Paketes enthalten, und dieses Paket sich schon in einem anderen aufgenommen Modul befindet. Module dürfen keine „split packages“ enthalten, also das gleiche Paket noch einmal enthalten. Die Migration erfordert dann entweder a) das Zusammenlegen der Pakete zu einem Modul, b) die Verschiebung in unterschiedliche Pakete oder c) die Nutzung des Klassenpfades

Unbenanntes Modul

Eine Migration auf eine neue Java-Version sieht in der Regel so aus, dass zuerst die JVM gewechselt und geprüft wird, ob die vorhandene Software weiterhin funktioniert. Laufen die Testfälle durch und gibt es keine Auffälligkeiten im Testbetrieb kann der Produktivbetrieb unter der neuen Version erfolgen. Gibt es keine Probleme, können nach einiger Zeit die neuen Sprachmittel und Bibliotheken verwendet werden.

Übertragen wir das auf den Wechsel von Java 8 auf Java 9: Eine vorhandene Java-Software muss inklusive aller Einstellungen und Einträge im Klassenpfad weiterhin laufen. Das heißt, eine Java 9 Laufzeitumgebung kann den Klassenpfad nicht ignorieren. Da es intern nur einen Modulpfad gibt, müssen auch diese JAR-Dateien zu Modulen werden. Die Lösung ist das unbenannte Modul (eng. unnamed module): jedes JAR im Klassenpfad – dabei spielt es keine Rolle, ob es eine modul-info.class enthält – kommt in das unbenannte Modul. Davon gibt es nur eines, wir sprechen also im Singular, nicht Plural.

„Unbenannt“ sagt schon, dass das Modul keinen Namen hat, und folglich auch keine Abhängigkeit zu den JAR-Dateien im unbenannten Modul existieren kann; das ist der Unterschied zu einem automatischen Modul. Ein unbenanntes Modul hat die gleiche Eigenschaft wie ein automatisches Modul, dass es alle Pakete exportiert. Und weil es zur Migration gehört, hat ein unbenanntes Modul auch Zugriff auf alle anderen Module.

Lesbarkeit und Zugreifbarkeit

Die Laufzeitumgebung sortiert Module in einem Graphen ein. Die Abhängigkeit der Module führt dabei zu sogenannten Lesbarkeit (engl. readibility): Benötigt Modul A das Modul B, so ist liest A das Modul B und B wird von A gelesen. Für die Funktionsweise vom Modulsystem ist dies elementar, denn so werden zur Übersetzungszeit schon Fehler ausgeschlossen, wie Zkylen, oder gleiche Pakete in unterschiedlichen Modulen. Die Lesbarkeit ist zentral für eine zuverlässige Konfiguration, engl. reliable configuration.

Ein Schritt weiter geht der Begriff der Erreichbarkeit/Zugänglichkeit (engl. accessibility). Wenn ein Modul ein anderes Modul grundsätzlich lesen kann, bedeutet es noch nicht, dass es an alle Pakete und Typen kommt, denn nur die Typen sind sichtbar, die exportiert worden. Lesbare und erreichbare Typen nennen sich erreichbar.

Die nächste Frage ist, welcher Modultyp auf welchen anderen Modultyp Zugriff hat. Eine Tabelle fasst die Lesbarkeit am Besten zusammen:

Modultyp

Ursprung

Exportiert Pakete

Hat Zugriff auf

Plattformmodul

JDK

explizit

 

Benannte Module

Container mit Modulinfo im Modulpfad

explizit

Plattformmodule, andere benannte Module, automatische Module

Automatische Module

Container ohne Modulinfo im Modulpfad

alle

Plattformmodule, andere benannte Module, automatische Module, unbenanntes Modul

Unbenanntes Modul

Klassendateien und JARs im Klassenpfad

alle

Plattformmodule, benannte Module, automatische Module

Lesbarkeit der Module

Der Modulinfodatei kommt dabei die größte Bedeutung zu, denn sie macht aus einer JAR ein modular-JAR; fehlt die Modulinfomation bleibt es eine normale JAR, wie sie Java-Entwickler seit 20 Jahren kennen. Die JAR-Datei kann neu in den Modulpfad kommen oder in den bekannten Klassenpfad. Das ergibt vier Kombinationen:

 

Modulpfad

Klassenpfad

JAR mit Modulinfomation

wird benanntes Module

wird ubenanntes Modul

JAR ohne Modulinfomation

wird automatisches Modul

wird unbenanntes Modul

JARs im Pfad

JAR-Archive im Klassenpfad sind das bekannte Verhalten, weswegen auch ein Wechsel von Java 8 auf Java 9 möglich sein sollte.

Modul-Migration

Nehmen wir an, unsere monolithische Applikation hat keine Abhängigkeiten zu externen Bibliotheken und soll modularisiert werden. Dann besteht der erste Schritt darin, die gesamte Applikation in ein großes benanntes Modul zu setzen. Als nächstes müssen die einzelnen Bereiche identifiziert werden, damit nach und nach die Bausteine in einzelne Module wandern. Das ist nicht immer einfach, zumal zyklische Abhängigkeiten nicht unwahrscheinlich sind. Bei der Modularisierung des JDK hatten die Oracle-Entwickler viel Mühe.

Das Problem mit automatischen Modulen

Traditionell generieren Build-Werkzeuge wie Maven oder Gradle JAR-Dateien und ein Dateiname hat sich irgendwie ergeben. Werden jedoch diese JAR-Dateien zu automatischen Modulen spielt der Dateiname plötzlich eine große Rolle. Doch bewusst wurde der Dateiname vermutlich nie gewählt. Referenziert ein benanntes Modul ein automatisches Modul bringt das zwei Probleme mit sich: Ändert sich der Dateiname, lassen wir die Versionsnummer einmal außen vor, heißt auch das automatische Module anders, und die Abhängigkeit kann nicht mehr aufgelöst werden. Das zweite Problem ist größer. Viele Java-Bibliotheken haben noch keine Modulinformationen, und folglich werden Entwickler eine Abhängigkeit zu diesem automatischen Modul über den abgeleiteten Namen ausdrücken. Nehmen wir z. B. die beliebte Open-Source Bibliothek Google Guava. Die JAR-Datei hat den Dateinamen guava-23.0.jar – guava heißt folglich das automatische Modul. Ein benanntes Modul (nennen wir es M1) kann über required guava eine Abhängigkeit ausdrücken. Konvertiert Google die Bibliothek in ein echtes Java 9-Modul, dann wird sich der Name ändern – geplant ist com.google.guava. Und ändert sich der Name, führen alle Referenzierungen in Projekten zu einem Compilerfehler; ein Alias wäre eine tolle Idee, das gibt es jedoch nicht. Und das Problem besteht ja nicht nur im eigenen Code der Guava referenziert; Referenziert das eigene Modul M1 ein Modul M2, das wiederum Guava referenziert, so gibt es das gleiche Problem – wir sprechen von einer transitiven Abhängigkeit. Die Änderung vom Modulnamen von Guava wird zum Problem, denn wir müssen warten, bis M2 den Namen korrigiert, damit M1 wieder gültig ist.

Eine Lösung mildert das Problem ab: In der JAR-Manifest-Datei kann ein Eintrag Automatic-Module-Name gesetzt werden – das „überschreibt“ den automatischen Modulnamen.

Beispiel: Apache Commons setzt den Namen so:

Automatic-Module-Name: org.apache.commons.lang3

Benannte Module, die Abhängigkeiten auf automatische Module besitzen, sind also ein Problem. Es ist zu hoffen, dass die zentralen Java-Bibliotheken, auf die sich so viele Lösungen stützen, schnell Modulinformationen einführen. Das wäre eine Lösung von unten nach oben, englisch Bottom-Up. Das ist das einzige, was erfolgversprechend ist, aber wohl auch eine lange Zeit benötigen wird. Im Monat vom Java 9-Release hat noch keine wichtige Java-Bibliothek eine Modulinformation, Automatic-Module-Name kommt häufiger vor.


[1] Zur Benennung von Modulen gibt es Empfehlungen in dem englischsprachigen Beitrag http://mail.openjdk.java.net/pipermail/jpms-spec-experts/2017-May/000687.html

[2] Automatic-Module-Name in die META-INF-Datei zu setzen ist eine Alternative, dazu später mehr.

[3] Für Details siehe http://download.java.net/java/jigsaw/docs/api/java/lang/module/ModuleFinder.html#of-java.nio.file.Path…-

JMOD-Dateien und JAR-Dateien

Der Klassenlader bezieht .class-Dateien nicht nur aus Verzeichnissen, sondern in der Regel aus Containern. So müssen keine Verzeichnisse ausgetauscht werden, sondern nur einzelne Dateien. Als Container-Formate finden wir JMOD (neu in Java 9) und JAR. Wenn Java-Software ausgeliefert wird bieten sich JAR- oder JMOD-Dateien an, denn es ist einfacher und platzsparender, nur ein komprimiertes Archiv weiterzugehen als einen großen Dateibaum.

JAR-Dateien

Sammlungen von Java-Klassendateien und Ressourcen werden in der Regel in Java-Archiven, kurz JAR-Dateien, zusammengefasst. Diese Dateien sind im Grunde ganz normale ZIP-Archive mit einem besonderen Verzeichnis META-INF für Metadateien. Das JDK bringt im bin-Verzeichnis das Werkzeug jar zum Aufbau und Extrahieren von JAR-Dateien mit.

JAR-Dateien behandelt die Laufzeitumgebung wie Verzeichnisse von Klassendateien und Ressourcen. Zudem haben Java-Archive den Vorteil, dass sie signiert werden können und illegale Änderungen auffallen. JAR-Dateien können Modulinformationen beinhalten, dann heißen sie engl. modular JAR.

JMOD-Dateien

Das Format JMOD ist speziell für Module und neu in Java 9 – es organisiert Typen und Ressourcen. Zum Auslesen und Packen gibt es im bin-Verzeichnis des JDK das Werkzeug jmod.

Hinweis

Die JVM greift selbst nicht auf diese Module zurück. Achten wir auf die Ausgabe vom letzten Programmm, dann steht in der ersten Zeile:

[0.015s][info][class,load] opened: C:\Program Files\Java\jdk-9\lib\modules

Die Datei module ist ca. 170 MiB groß und in einem proprietären Dateiformat.

JAR vs. JMOD

Module können in JMOD- und JAR-Conatainer gepackt werden. Wenn ein JAR kein modular-JAR ist, also keine Modulinformationen enthält, so fehlen zentrale Informationen, wie Abhängigkeiten oder eine Version; ein JMOD ist immer ein benanntes Modul.

JMOD-Dateien sind nicht so flexibel wie JAR-Dateien, denn sie können nur zur Übersetzungszeit und zum Linken eines Runtime-Images – dafür gibt es das Kommandozeilenwerkzeug jlink – genutzt werden. JMOD-Dateien können nicht wie JAR-Dateien zur Laufzeit verwendet werden. Das Dateiformat ist proprietär und kann sich jederzeit ändern, es ist nichts Genaues spezifiziert.[1] Einziger Vorteil von JMOD: Native Bibliotheken lassen sich standardisiert einbinden.


[1] Die http://openjdk.java.net/jeps/261 macht die Aussage, dass es ein ZIP ist.

Was Eclipse bisher bei Java 9 unterstützt

Siehe https://wiki.eclipse.org/Java9/Examples

Feature / Steps
Expected Result

The Pre-requisite: Java 9 JRE Support

Add Java 9 JRE
Use Eclipse Preferences -> Java -> Installed JREs -> Add

Addj9.jpg

Java 9 JRE recognized as a valid JRE

Project JRE
In Package Explorer Use Project Context Menu and add Java 9 JRE
JRE specific (eg Object) gets resolved in the project.

Package Explorer
Go to Package Explorer and expand the Java 9 JRE
Modules (eg java.base etc) are listed in the package explorer view

The First Step: Module Creation

Manual
Context Menu of src -> New -> File – give the module-info.java as name
no compiler errors

Automatic
Context Menu of Project -> Cofigure -> Create module-info.

AutomoduleCreate.jpg

A default module-info.java with all packages exported should be created

Basic Necessity : Compilation, Module Dependency & Error Reporting

Unspecified Dependency
create projects "first" and "second" and create module-info.java files in each of giving the module names "first" and "second" respectively.

In the first module add the directive requires second; . This initial configuration would look something similar to the one shown in the figure.
Initmods.jpg

Compiler gives error "second cannot be resolved to a module"

Define Dependency
In the above scenario, add Project second as a dependent project for project first
Compiler error goes away

Duplicate Dependency
Continuing from the above scenario, add a duplicate requires second; directive in the module-info.java file of the first
Compiler gives error "Duplicate requires entry: second"

Circular Dependency
add a circular dependency ie

add second project dependent on first
add requires first; directive in the module-info.java file of the second project ie replace // empty by design comment by this directive

Two compiler errors " Cycle exists in module dependencies, Module second requires itself via first"

Editing with Ease: Completion in module-info.java file

Keyword Completion (1)
In the module-info.java file of say second project, after module first {, press completion key (for eg, ctrl+space in windows)

Keycomplete1.jpg

keywords exports, opens, requires, provides and uses shown

Keyword Completion (2)
after exports packagename, or opens packagename press completion key
keyword to is shown as an option

Package Completion
after exports, opens, provides or uses, press completion key
package completion shown.

Type Reference Completion
after exports, opens, provides or uses, or optionally after a dot after a package, ie exports packagename. press completion key
Type completion shown.

Implementation TypeRef Completion
after provides typename with press completion key
Type completion shown and these typereferences are implementations of the type given before with.

The Essential Utilities: Code Select, Hover, Navigate, Search and Rename

Module Select & Hover

In the module-info.java file of the first project, select second in the requires second; directive
Hover.jpg

Hover appears

Module Select, Hover & Navigate
In the above scenario, after hover appears, click on the navigate
module-info.java file of second opened

Module Select, & Search

In the module-info.java file of the second project, select second in module declaration module second { and search for references
Modsearch2.jpg

In the search view, the reference in directive requires second; in file first -> module-info.java is shown.

Package Search

create package pack1 to the project first.
add exports pack1; directive in module-info.java file of first.
search for references of pack1

In the search view, the reference of pack1 in directive exports pack1; in file first -> module-info.java is shown, similar to other pack1references if any

Type Search
create Type X in the project first, add directive uses X; in module-info.java file of first, and search for references of X
In the search view, the reference of Xin directiveuses X; in file first -> module-info.java is shown, similar to other Xreferences if any

Code Select & Rename
in module-info.java file of first, select X in directive uses X; and rename to X11
rename exhibits usual behavior – renames definition and references of Xto X11

The Outlier: Milling Project Coin Enhancements

@Safevarargs
@SafeVarargs is now allowed on private instance methods. There is even a support of quick assist for that. Use the following code which has warnings, and use the quick assist at the point mentioned in the comment

package packsafe;
import java.util.ArrayList;
import java.util.List;   public class SafeVar {
	private int getLen(List<String>...list) {
		List<String>[] l = list;
		return l.length;
	}   public static void main(String[] args) {
		SafeVar x = new SafeVar();
		List<String> l = new ArrayList<>();
		int len = x.getLen(l); // Use Quick Assist of SafeVarargs here<br>
		System.out.println("Length:" + len);
	}
}

@SafeVarargsinserted before getLen() and the warnings go away

Effectively Final Autoloseables

Effectively-final variables are allowed to be used as resources in the try-with-resources statement. The code below has an error. Try removing the line t1 = null; // Remove this code .

package packtry;
import java.io.Closeable;
import java.io.IOException;   class Two implements Closeable {
	@Override
	public void close() throws IOException {
		// nothing
	}
}
public class TryStmtTwo {   public void foo() throws IOException {
		Two t1 = new Two();
		try (t1; final Two t2 = new Two()) {
		// Empty by design
	}
	t1 = null; // Remove this code
	}
	public static void main(String[] args) {
		System.out.println("Done");
	}
}

Code without errors. For the more inquisitive, check the generated code to see that the close is generated for t1 as well which is not a final variable but an effectively final variable.

Anonymous Diamond

In the following code, there is a warning about Y being a raw type and need to be parameterized. with Java 9 support, just add a diamond operator after Y.

public class Dia {
@SuppressWarnings("unused")
	public static void main(String[] args) {
		Y<?> y1 = new Y(){}; // Change this to new Y<>(){}
	}
}
class Y<T> {}

Diamond operator <>accepted and code compiles without warning

Illegal Underscore

Underscore is an illegal identifier from Java 9 onwards. Uncomment the commented line in the following example

public class UnderScore {
	//Integer _ ;
}

error: "’_‘ should not be used as an identifier, since it is a reserved keyword from source level 1.8 on"

Private Methods

private interface methods are allowed. Change the default of worker to private

public interface I {
	default void worker() {}<   default void foo() {
		worker();
	}   default void bar() {
		worker();
	}
}

Code compiles with privateas well. Note that this is a useful feature if two default methods wants to share the worker code and does not want the worker to be an public interface method.

Reaktive Programmierung und die Flow-API

Sollen Produzenten und Konsumenten entkoppelt werden, so gibt es eine Reihe von Möglichkeiten. Nehmen wir zum Beispiel einen Iterator, der eine einfache API definiert, sodass sich auf immer die gleiche Art und Weise Daten von einem Produzenten abholen lassen. Oder nehmen wir einen Beobachter (Observer/Listener): ein Produzent verschickt seine Daten an Interessenten. Oder nehmen wir ein Bussystem: Publisher (Sender) und Subscriber (Empfänger) senden Datenpakete über einen Bus. All diese Design-Pattern sind für bestimmte Anwendungsfälle gut, haben jedoch alle eine Schwäche: es entstehen oft Blockaden und die Gefahr der Überflutung mit Nachrichten besteht.

Neu in Java 9 sind Schnittstellen eingezogen, die vorher unter http://www.reactive-streams.org/ diskutiert und als Defacto-Standard gelten. Ziel ist die Standardisierung von Programmen, die „reaktiv“ programmiert sind. Grundidee ist, dass es Publisher und Subscriber gibt, die einen asynchron Datenstrom austauschen aber nicht blockieren und mit „Gegendruck“ (engl. back pressure) arbeiten. Vergleichen wir das mit dem bekannten Bussystem, dann wird das Problem schnell klar; es kann passieren, dass ein Publisher viel zu schnell Ereignisse produziert, und die Subscriber nicht mitkommen, wo sollen dann die Daten hin? Oder, wenn der Publisher nichts sendet, dann wartet der Subscriber blockierend. Genau diese Probleme möchte das reaktive Modell vermeiden, in dem zwischen Publisher und Subscriber liegt ein Vermittler eingeschaltet wird, der über Gegendruck steuert, dass der Subscriber Daten anfordert und der Publisher dann so viel sendet, wie nicht-blockierend verarbeitet werden kann. Das realisiert eine Flusskontrolle, sodass wir auch von der Flow-API sprechen.

Die API ist untergebraucht in einer finalen Klasse java.util.concurrent.Flow, die vier statische innere Schnittstellen deklariert:

@FunctionalInterface  

public static interface Flow.Publisher<T> { 

    public void    subscribe(Flow.Subscriber<? super T> subscriber); 

}  

 

public static interface Flow.Subscriber<T> { 

    public void    onSubscribe(Flow.Subscription subscription); 

    public void    onNext(T item) ; 

    public void    onError(Throwable throwable) ; 

    public void    onComplete() ; 

}  

 

public static interface Flow.Subscription { 

    public void    request(long n); 

    public void    cancel() ; 

}  

 

public static interface Flow.Processor<T,R>  extends Flow.Subscriber<T>, Flow.Publisher<R> { 

}

Es ist gar nicht Aufgabe der Java SE diverse Implementierungen für die Schnittstellen bereitzustellen; es gibt lediglich von Publisher eine Implementierung, java.util.concurrent.SubmissionPublisher<T> . Allgemein sind die Implementierungen aber nicht trivial und daher ist es ratsam, sich mit zum Beispiel RxJava, „Reactive Extensions for the JVM“ (https://github.com/ReactiveX/RxJava) und Akka Streams (http://doc.akka.io/docs/akka/2.5.3/java/stream/index.html) zu beschäftigen, die Datenströme sehr schön in eine Kette verarbeiten können. Der eigentliche Wert der Java SE-Schnittstellen ist, dass es damit zu einer offiziellen API wird und Algorithmen problemlos unter den reaktiven Bibliotheken ausgetauscht werden können.

Asynchrones Programmieren mit CompletableFuture (CompletionStage)

So schöne an Future-Objekten ist die Abarbeitung im Hintergrund und die Möglichkeit, später abzufragen, ob das Ergebnis schon da ist. Allerdings fehlt der Future-Schnittstelle eine Methode, automatisch nach der Fertigstellung einen Folgeauftrag abzuarbeiten. Dafür bietet die Java-Bibliothek eine spezielle Unterklasse CompletableFuture. Die Klasse implementiert die Schnittstelle CompletionStage, die vermutlich die größte Anzahl Operationen in der gesamten Java SE hat. Der Typname drückt aus, das es um die Fertigstellung (engl. completion) von Abschnitten (engl. stage) geht.

Ein Beispiel für einen trinkfesten mutigen Piraten:

package com.tutego.insel.concurrent;




import java.time.LocalTime;

import java.util.concurrent.CompletableFuture;

import java.util.concurrent.TimeUnit;

import java.util.logging.Logger;




class Pirate {




  public static void main( String[] arg ) throws Throwable {




    String result = CompletableFuture.supplyAsync( Pirate::newName )

                                     .thenApply( Pirate::swears )

                                     .thenCombine( drinkRum(), Pirate::combinePiratAndDrinks )

                                     .thenCombine( drinkRum(), Pirate::combinePiratAndDrinks )

                                     .get();

    System.out.println( result ); // Pirat Guybrush flucht und trinkt dann 10 Flaschen Rum und trinkt dann 11 Flaschen Rum

  }




  static String newName() {

    Logger.getGlobal().info( "" + Thread.currentThread() );

    return "Pirat Guybrush";

  }




  static String swears( String pirate ) {

    Logger.getGlobal().info( "" + Thread.currentThread() );

    return pirate + " flucht";

  }




  static CompletableFuture<Integer> drinkRum() {

    Logger.getGlobal().info( "" + Thread.currentThread() );

    try { TimeUnit.SECONDS.sleep( 1 ); } catch ( Exception e ) { }

    return CompletableFuture.supplyAsync( () -> LocalTime.now().getSecond() );

  }




  static String combinePiratAndDrinks( String pirat, int bottlesOfRum ) {

    Logger.getGlobal().info( "" + Thread.currentThread() );

    return pirat + " und trinkt dann " + bottlesOfRum + " Flaschen Rum";

  }

}

Die Ausgabe ist:

Juni 15, 2017 10:41:56 NACHM. com.tutego.insel.thread.concurrent.Pirate drinkRum

INFORMATION: Thread[main,5,main]

Juni 15, 2017 10:41:56 NACHM. com.tutego.insel.thread.concurrent.Pirate newName

INFORMATION: Thread[ForkJoinPool.commonPool-worker-1,5,main]

Juni 15, 2017 10:41:56 NACHM. com.tutego.insel.thread.concurrent.Pirate swears

INFORMATION: Thread[ForkJoinPool.commonPool-worker-1,5,main]

Juni 15, 2017 10:41:57 NACHM. com.tutego.insel.thread.concurrent.Pirate combinePiratAndDrinks

INFORMATION: Thread[main,5,main]

Juni 15, 2017 10:41:57 NACHM. com.tutego.insel.thread.concurrent.Pirate drinkRum

INFORMATION: Thread[main,5,main]

Juni 15, 2017 10:41:58 NACHM. com.tutego.insel.thread.concurrent.Pirate combinePiratAndDrinks

INFORMATION: Thread[main,5,main]

Pirat Guybrush flucht und trinkt dann 57 Flaschen Rum und trinkt dann 58 Flaschen Rum

Zum Programm: Zunächst muss die Kette von Abschnitten aufgebaut werden. Das kann entweder mit dem Standardkonstruktor geschehen, oder mit statischen Methoden. In unserem Fall nutzen wir supplyAsync(Supplier<U> supplier). Die Methode nimmt sich einen freien Thread aus dem ForkJoinPool.commonPool() und lässt den Thread den supplier abarbeiten. Das Ergebnis ist über die Rückgabe, einem CompletableFuture, abrufbar. Als nächsten wenden wir thenApply(Function<? super T,? extends U> fn) an, die vergleichbar ist mit einer map(…)-Operation eines Streams. Interessant wird es bei thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn); sie verbindet das Ergebnis der eigenen CompletionStage über eine Funktion mit einer anderen CompletionStage, die wir in unserem Fall auch wieder mit supplyAsync(…) aufbauen. So kombinieren wir zwei unabhängige CompletionStage miteinander und synchronisieren das Ergebnis. Wir können das gut an der Ausgabe auslesen, dass drinkRum ganz am Anfang schon ausgeführt wird, und zwar vom Thread[main,5,main], nicht vom ForkJoinPool, weil es unabhängig von den anderen läuft.

Versionskennungen aufbauen, parsen, vergleichen

In Java 9 wurde die Kennung für die Java-Version standardisiert. Wo früher von zum Beispiel 1.9.0_31-b08 die Rede war, heißt es heute 9.1.4+8; die 1. ist also verständig verschwunden. Auch eigene Programme können Versionskennungen nutzen und die Java SE stellt eine Klasse bereit, mit der sich zum Beispiel Versionen vergleichen lassen. Details zu den sogenannten semantischen Versionierung liefert http://semver.org/lang/de/.

Versionskennung

Ein Versionskennung hat die Form $MAJOR.$MINOR.$SECURITY. Das Schema besteht aus drei Teilen, die durch Punkte voneinander getrennt sind:

  1. Hautversion (engl. major version). Wird immer bei zentralen Änderungen und Updates um eine Stelle erhöht. Oft verbunden mit inkompatiblen API-Änderungen. Dann wird die Unterversion zurück auf 0 gesetzt.
  2. Unterversion (engl. minor verson). Wird erhöht bei kleineren Änderungen, wie Bug-Fixes. Das Release selbst bleibt rückwärtskompatibel.
  3. Sicherheitsrelease/Patch. Wird erhöht nach kritischen Sicherheitsupdates. Wichtig: Die Release-Nummer wird nicht auf 0 zurückgesetzt, wenn die Unterversion erhöht wird. Ein hoher Zähler der Sicherheitsreleases weist auf viele Änderungen hin, unabhängig von der Unterversion.

Die Versionen sind rein numerisch und der Gesamtstring matcht auf den regulären Ausdruck [1-9][0-9]*((\.0)*\.[1-9][0-9]*)*. Die Kennung endet nicht mit 0 – so wird $SECURITY weggelassen, wenn sie 0 ist und $MINOR wird nicht gesetzt, wenn $MINOR und $SECURITY beide 0 sind.

Versionsstring

Ein Versionsstring ist eine Versionskennung mit einem optionalen Zusatz von Informationen wie einer Vorversion (engl. pre release) oder Build-Informationen. Den Aufbau erklärt die API-Dokumentation an der Klasse Runtime.Version.

Version-API

Die Runtime-Klasse hat unter Java 9 eine innere Klasse Version bekommen, die so einen Versionsstring aufbauen und parsen kann. Die Version der aktuellen Laufzeitumgebung liefert die statische Methode version().

Beispiel

Gib alle Informationen über den Versionsstring aus:

System.out.println( Runtime.version() ); // 9-ea+159

System.out.println( Runtime.version().major() ); // 9

System.out.println( Runtime.version().minor() ); // 0

System.out.println( Runtime.version().security() ); // 0

System.out.println( Runtime.version().pre() ); // ea

System.out.println( Runtime.version().build() ); // 159

System.out.println( Runtime.version().optional() ); // Optional.empty

Eigene Version-Objekte lassen sich aus einem Versionsstring aufbauen mit der einzigen statischen Methode der Klasse Version, und zwar parse(String).

Beispiel

Version version = Version.parse( "9.2.4+45" );

System.out.println( version.version() );  // [9, 2, 4]

An dem Beispiel ist auch version() abzulesen, die eine numerische Liste mit Versionsnummern liefert.

Neben diesen Abfragemethoden kommen weitere Methoden hinzu. Zunächst überschreibt Version die Object-Methoden equals(…), hashCode() und toString(). Und da Version die Schnittstelle Comparable<Runtime.Version> implementiert, hat die Klasse eine Methode compareTo(Runtime.Version), was die Versionsnummern in eine Ordnung bringt. Zusätzlich gibt es compareToIgnoreOptional(Runtime.Version) und equalsIgnoreOptional(Object), was so etwas wie Build-Informationen ignoriert.

Java 9 wieder verschoben um 2 Monate

Auf den vermutlich 21. September 2017.  Aus der E-Mail http://mail.openjdk.java.net/pipermail/jdk9-dev/2017-May/005864.html von Mark Reinhold:

As you probably know by now, the JCP Executive Committee (EC) recently voted [1] not to approve JSR 376, the Java Platform Module System [2], for the next stage of the process.

This vote does not mean that JSR 376 is dead, nor that Jigsaw has been rejected. It only means that the EC raised a number of concerns that it wanted the JSR 376 Expert Group (EG) to address. The JCP rules give the EG thirty days, until 7 June, to submit a revised specification for a second EC vote, which will end no later than 26 June [3].

The JSR 376 EG held a series of conference calls over the past two weeks in order discuss the EC’s concerns [4]. The net impact of those meetings on JDK 9 itself was to clarify the specification of the module system’s resolution algorithm, work on which had already begun, and to add one five-line method to the module-system API. These changes, together with additional clarifications to the JSR 376 and JSR 379 (Java SE 9) [5] Specifications, will hopefully address the EC’s concerns.

In order to be ready for all possible outcomes I suggest that here in the JDK 9 Project we continue to work toward the current goal of producing an initial Release Candidate build on 22 June [6], but adjust the GA date in order to accommodate the additional time required to move through the JCP process. To be specific, I propose that we move the GA date out by eight weeks, from 27 July to 21 September.

Comments on this proposal from JDK 9 Committers are welcome, as are reasoned objections. If no such objections are raised by 23:00 UTC next Tuesday, 6 June, or if they’re raised and satisfactorily answered, then per the JEP 2.0 process proposal [7] this will be the new schedule for JDK 9.

Wird es bei Jigsaw (Module) in Java 9 bleiben?

RedHat hat mit  „The critical missing pieces and a path forward“ (http://mail.openjdk.java.net/pipermail/jpms-spec-observers/2017-May/000874.html) ausgedrückt, Jigsaw so nicht unterstützen zu können.

Tim Ellison von IBM legt noch einmal nach in http://mail.openjdk.java.net/pipermail/jpms-spec-observers/2017-May/000870.html und adressiert mit Links weitere Probleme.

Insgesamt sehe ich drei Ausgänge:

  1. Alles bleibt wie es ist und Oracle ignoriert die Probleme bzw. verschiebt Lösungen auf Java 10
  2. Das Release von Java 9 wird noch einmal nach hinten verschoben
  3. Jigsaw fliegt aus Java 9 raus

Was meint ihr?

JShell, die interaktive REPL-Shell in Java 9

Im JDK 9 ist ein neues Programm eingezogen: die JShell. Mit ihr lassen sich auf einfache Weise kleine Java-Programme und einzelne Anweisungen testen, sogenannte Snippets, ohne eine große IDE starten zu müssen. Die JShell ist eine Befehlszeile (Shell), die nach dem Read-Evaluate-Print-Loop-Prinzip arbeitet:

  • Read (Lesen): Eingabe des Programms von der Kommandozeile. Eine gute Shell bietet eine Historie der letzten Kommandos und Tastaturvervollständigung.
  • Eval (Ausführen): Compiliert das Snippet und führt es aus.
  • Print (Ausgaben): Die der Anweisungen und Programme werden in der Umgebung ausgegeben.
  • Loop (wiederholen): Es folgt ein Rücksprung auf den Zustand Lesen.

Das bekannteste Beispiel für eine REPL-Umgebung ist die Unix-Shell. Doch viele Skriptsprachen wie Lisp, Python, Ruby, Groovy und Clojure bieten solche REPL-Shells. Nun auch Java seit Version 9. Die Rückmeldung ist schnell, und gut zum Lernen und Ausprobieren von APIs.

Im bin-Verzeichnis vom JDK finden wir das Programm jshell. Rufen wir sie auf:

|  Welcome to JShell -- Version 9-ea

|  For an introduction type: /help intro




jshell>

Die JShell besitzt eingebaute Kommandos, die mit / beginnen, um zum Beispiel alle deklarierten Variablen ausgeben oder das Skript speichern. /help gibt eine Hilfe über alle Kommandos, /exit beendet JShell.

Nach dem Start wartet JShell auf die Snippets. Gültig sind:

  • Import-Deklarationen
  • Typ-Deklarationen
  • Methoden-Deklarationen
  • Anweisungen
  • Ausdrücke

Es sind also Teilmengen der Java-Sprache und keine eigene Sprache.

Anweisungen und einfache Navigation in der JShell

In der JShell lässt sich jeder Code schreiben, der im Rumpf einer Methode gültig ist. Ein Semikolon am Ende einer Anweisung ist nicht nötig:

jshell> System.out.println( "Hallo Welt" )

"Hallo Welt"

Compilerfehler zeigt die JShell sofort an:

jshell> System.out.pri()

|  Error:

|  cannot find symbol

|    symbol:   method pri()

|  System.out.pri()

|  ^------------^

Ausnahmen müssen nicht behandelt werden, es lassen sich alle Methoden ohne try-catch aufrufen; falls es zu Ausnahmen kommt werden diese direkt gemeldet:

jshell> Files.exists( Paths.get("c:/") )

$2 ==> true




jshell> Files.exists( Paths.get("lala:/") )

|  java.nio.file.InvalidPathException thrown: Illegal char <:> at index 4: lala:/

|        at WindowsPathParser.normalize (WindowsPathParser.java:182)

…

|        at (#3:1)

Die letzte Zeile zeigt die Zeilennummer im Skript an. Eine Liste der bisher eingegebenen Zeilen listet /list auf, und das inklusive Zeilennummern. Diese sind nützlich, wenn es zu Ausnahmen wie oben kommt.

jshell> /list




   1 : System.out.println( "Hallo Welt" );

   2 : Files.exists(Paths.get("c:/"))

   3 : Files.exists(Paths.get("lala:/"))

Die JShell pflegt eine Historie der letzten Kommandos, die sich mit den Cursor-Tasten abrufen lässt. Es funktioniert auch die Vervollständigung mit der Tabulator-Taste wie in einer IDE, wobei die Groß-Kleinschreibung relevant ist:

jshell> Sys

System        SystemColor   SystemTray




jshell> System.out.println(java.time.LocalDateTime.n

jshell> System.out.println(java.time.LocalDateTime.now(

now(




jshell> System.out.println(java.time.LocalDateTime.now())

2017-03-23T11:50:43.859385900

Mit dem Cursor lässt sich in die Zeile vorher gehen und die Zeile nacheditieren.

Variablendeklarationen

Variablen lassen sich deklarieren und später jederzeit verwenden:

jshell> String name = "Christian"

name ==> "Christian"

Die JShell gibt die Variable mit der Belegung zur Kontrolle aus.

Variablen lassen sich mit einem ganz neuen Typ redefinieren:

jshell> StringBuilder name = new StringBuilder( "Christian" )

name ==> Christian

Es lassen sich auch ohne Zuweisung Ausdrücke in die JShell setzen. Das Ergebnis des Ausdrucks wird einer temporären Variablen zugewiesen, die standardmäßig mit einem Dollar beginnt und der einer Zahl folgt, etwa $1. Auf diese Variable lässt sich später zugreifen:

jshell> BigInteger.TEN.pow(10)

$1 ==> 10000000000




jshell> $1

$1 ==> 10000000000




jshell> $1.bitLength()

$2 ==> 34




jshell> System.out.println(2*$2)

68

Welche Variablen in welcher Reihenfolge in der Sitzung deklariert wurden zeigt das Kommando /vars auf:

jshell> /vars

|    StringBuilder name = Christian

|    BigInteger $1 = 10000000000

|    int $2 = 34

Unvollständige Eingabe

Wenn die JShell auf einen nicht kompletten Code trifft, symbolisiert die Ausgabe …> die Notwendigkeit einer weitere Eingabe:

jshell> System.out.println(

   ...> "Hallo"

   ...> +

   ...> " Welt"

   ...> )

Hallo Welt

Import-Deklarationen

Standardmäßig sind für den Java-Compiler alle Typen vom Paket java.lang direkt importiert. Die JShell erweitert das um eine ganze Reihe weiterer Typen. Wir können sie mit dem Kommando /imports erfragen:

jshell> /imports

|    import java.io.*

|    import java.math.*

|    import java.net.*

|    import java.nio.file.*

|    import java.util.*

|    import java.util.concurrent.*

|    import java.util.function.*

|    import java.util.prefs.*

|    import java.util.regex.*

|    import java.util.stream.*




jshell> import java.awt.*




jshell> /imports

|    import java.io.*

|    import java.math.*

|    import java.net.*

|    import java.nio.file.*

|    import java.util.*

|    import java.util.concurrent.*

|    import java.util.function.*

|    import java.util.prefs.*

|    import java.util.regex.*

|    import java.util.stream.*

|    import java.awt.*

Methoden- und Typ-Deklarationen

Methoden und Klassen lassen sich deklarieren und auch wieder überschreiben, wenn eine neue Version eine alte ersetzen soll. JShell schreibt dann „modified“ bzw. „replaced“.

jshell> String greet(String name) { return "BÖLK " + name; }

|  created method greet(String)




jshell> String greet(String name) { return "Mit vorzüglicher Hochachtung " + name; }

|  modified method greet(String)




jshell> class MyFrame extends java.awt.Frame {}

|  created class MyFrame




jshell> class MyFrame extends java.awt.Frame { MyFrame() { setTitle("FENSTER"); } }

|  replaced class MyFrame




jshell> new MyFrame().show()

Welche Methoden und neue Typen in der Sitzung deklariert sind listet /methods und /types auf:

jshell> /methods

|    String greet(String)




jshell> /types

|    class OkButton

|    class MyFrame

Exceptions müssen wie üblich behandelt werden, eine Sonderbehandlung, wie bei der direkten, in die JShell eingegeben Anweisungen, gibt es nicht.

Forward-Reference

Greift eine Methoden- oder Klassendeklaration auf Typen und Methoden zurück, die in dem Kontext noch nicht vorhanden sind, ist das in Ordnung; allerdings müssen alle Typen und Methoden spätestens dann bekannt sein, wenn der Code ausgeführt werden soll.

jshell> double cubic(double v) { return sqr(v) * v; }

|  created method cubic(double), however, it cannot be invoked until method sqr(double) is declared




jshell> cubic(100)

|  attempted to call method cubic(double) which cannot be invoked until method sqr(double) is declared




jshell> double sqr(double v) { return v*v; }

|  created method sqr(double)




jshell> cubic(100)

$14 ==> 1000000.0

Laden, speichern und ausführen von Skripten

Snippets können in der JShell mit /save Dateiname gespeichert, mit /open Dateiname geöffnet und mit /edit in einem Standard-Editor bearbeitet werden.

Auf der Kommandozeile werden JShell-Skripte auf vorhandenen Skripten einfach ausgeführt mit:

$ jshell datei

JShell API

Anders als die Benutzung von JavaScript aus Java heraus integriert sich die JShell nicht als Skript-Sprache. Stattdessen gibt es eine eigene API, in der die Klasse JShell im Mittelpunkt steht, wobei sich die Möglichkeiten der JShell-Kommandozeile eins zu eins in der API – dokumentiert unter http://download.java.net/java/jdk9/docs/jdk/api/jshell/overview-summary.html – wiederfinden lassen.

Ein einfaches Beispiel:

try ( JShell shell = JShell.create() ) {

  // Semikolon wichtig!

  String program = "java.math.BigInteger.TEN.pow( 10 );";

  List<SnippetEvent> events = shell.eval( program );

  for ( SnippetEvent snippetEvent : events ) {

    System.out.println( snippetEvent.status() );

    System.out.println( snippetEvent.value() );

    System.out.println( snippetEvent.snippet().source() );

    System.out.println( snippetEvent.snippet().kind() );

    if ( snippetEvent.snippet() instanceof VarSnippet ) {

      VarSnippet varSnippet = (VarSnippet) snippetEvent.snippet();

      System.out.println( varSnippet.typeName() );

    }

  }

}

Die Ausgabe ist:

VALID

10000000000

java.math.BigInteger.TEN.pow( 10 );

VAR

java.math.BigInteger

Ein paar Dinge sind an der API bemerkenswert, und zwar die Typen und Ergebnisse: sie sind Strings. varSnippet.typeName() ist ein String und snippetEvent.value() ebenso. Es ist nicht möglich, eine echte Objektrepräsentation zu bekommen, was die Nutzung als eingebettete Skriptsprache einschränkt.

Zum Weiterlesen

Weitere Informationen lassen sich aus dem JEP 222: jshell: The Java Shell (Read-Eval-Print Loop)[1] entnehmen und von den Quellen, die bei Java 9 dabei sind. Fragen über das Produkt lassen sich in der Mailingliste http://mail.openjdk.java.net/mailman/listinfo/kulla-dev stellen.

[1]            http://openjdk.java.net/jeps/222

Java DB nicht mehr in Java 9

Gerade habe ich angefangen mein JDBC-Kapitel zu aktualisieren, weg von HSQLDB hin zur mitgelieferten  Java DB. Ich habe die Java DB-Datenbank gestartet, den Text und Beispiele umgeschrieben, usw. Irgendwie habe ich das db-Verzeichnis von Java 8 genutzt, ohne das mir das aufgefallen wäre. Das Kapitel ist fertig gewesen, da wollte ich schauen, ob Java 9 die aktuelle Version von Apache DB nutzt und was ist? Arrrg. In Java 9 ist die Java DB rausgeflogen. Verdammt. Alle Änderungen wieder rückgängig machen, es bleibt vorerst bei HSQLDB.

Private Schnittstellenmethoden

Seit Java 9 müssen die statischen und Default-Methoden nicht mehr public sein, sie können auch private sein. Das ist gut, denn das beugt Codeduplikate vor; mit privaten Methoden können Programmteile innerhalb der Schnittstelle ausgelagert werden. Private Methoden bleiben natürlich in der Schnittstelle und werden nicht in die implementierenden Klassen vererbt.

StackWalker and Stack-Walking API

Der Stack-Trace, den Java über die StackTraceElement bietet, ist relativ arm an Informationen, und die Standardausgabe über die Throwable-Methode printStackTrace(…) ist unübersichtlich. Aus Performance-Gründen können sogar Einträge fehlen, so dokumentiert es die Javadoc an der Methode:

Some virtual machines may, under some circumstances, omit one or more stack frames from the stack trace. In the extreme case, a virtual machine that has no stack trace information concerning this thread is permitted to return a zero-length array from this method.

Zudem fehlen spannende Informationen wie Zeitpunkte, und ein Thread.getAllStackTraces() ist bei vielen Threads und tiefen Aufrufhierarchien sehr langsam. Summa summarum: Ein guter Debugger oder ein Analysetool mit Zugriff auf die JVM-Eigenschaften ist für die ernsthafte Entwicklung unumgänglich.

In Java 9 hat Oracle die “JEP 259: Stack-Walking API” umgesetzt. Ein java.lang.StackWalker wandert die Aufrufhierarchie ab und repräsentiert die Aufrufhierarchie als StackFrame-Objekte von dort, wo der Stack generiert wurde, nach unten zur Aufrufstelle. Es gibt mehrere überladenen statische getInstance(…)-Methoden, die einen StackWalker generieren. Wir können dann mit

  • void forEach(Consumer<? super StackFrame> action) oder
  • <T> T walk(Function<? super Stream<StackFrame>, ? extends T> function)

über die StackFrames laufen.

Beispiel:

public static void showTrace() {

  List<StackFrame> frames =

    StackWalker.getInstance( Option.RETAIN_CLASS_REFERENCE )

               .walk( stream  -> stream.collect( Collectors.toList() ) );

  for ( StackFrame stackFrame : frames )

    System.out.println( stackFrame );

}

Da alle Informationen zu liefern die Geschwindigkeit senkt, und vielleicht unnötig viel Arbeit verursacht, deklariert die Aufzählung StackWalker.Option die Konstanten RETAIN_CLASS_REFERENCE, SHOW_HIDDEN_FRAMES, SHOW_REFLECT_FRAMES für unterschiedliche Vollständigkeit der Informationen.  Die Aufzählungen sind ein Argument für getInstance(…). Um die Class-Objekte über getDeclaringClass() abrufen zu können, muss getInstance(…) mit der Option.RETAIN_CLASS_REFERENCE gesetzt sein.

Beispiel: Die forEach(…)-Methode eines Streams konsumiert einen Consumer auf jedem Element. Finde heraus, ob unsere Consumer-Methode accept(…) indirekt von einer Klasse aus dem Paket  java.util.concurrent aufgerufen wurde:

Consumer<String> walkStack = String -> {

  StackWalker.getInstance( Option.RETAIN_CLASS_REFERENCE )

        .walk( stream -> stream.map( StackFrame::getDeclaringClass )

                               .map( Class::getPackageName )

                               .filter( s -> s.startsWith( "java.util.concurrent" ) )

                               .findFirst() )

                               .ifPresentOrElse( e -> System.out.println( "Durch j.u.c gelaufen" ),

                                                 () -> System.out.println( "Nicht durch j.u.c gelaufen" ) );

};

Stream.of( "Hallo" ).forEach( walkStack );            // Nicht durch j.u.c gelaufen

Stream.of( "Hallo" ).parallel().forEach( walkStack ); // Durch j.u.c gelaufen

 

ProcessHandle und Prozess-IDs

Ein ProcessHandle ist ein neuer Typ in Java 9, der native Prozesse identifiziert. Wir bekommen Exemplare entweder über

  • die Process-Objektmethode toHandle(),
  • die statische Methodecurrent(),
  • allProcesses(), die alle Prozesse über einen Stream<ProcessHandle> liefert,
  • die statische ProcessHandle-Methode of(long pid), die uns das ProcessHandle in ein Optional verpackt,
  • mit einem vorhandenen ProcessHandle können wir weiterhin mit children() und descendants() einen Stream<ProcessHandle> erfragen und mit parent() auf die Eltern zugreifen.

Jeder Prozess verfügt über eine identifizierende long-Ganzahl, die getPid() auf dem ProcessHandle liefert. Weitere Details zu den Startparametern offenbare ProcessHandle.Info-Ojekte.

Beispiel: Gib alle vorhanden Informationen über alle Prozesse aus:

Consumer<ProcessHandle> log = handle ->

  System.out.printf( "PID=%s, Root?=%s, Info=%s%n",

                     handle.getPid(), !handle.parent().isPresent(), handle.info() );

ProcessHandle.allProcesses().forEach( log );

Die Ausgabe kann so aussehen:

PID=0, Root?=true, Info=[]

PID=4, Root?=true, Info=[]

...

PID=4368, Root?=true, Info=[]

PID=4568, Root?=true, Info=[user: Optional[Yoga\Christian], cmd: C:\Windows\System32\sihost.exe, startTime: Optional[2017-02-04T20:54:07.601Z], totalTime: Optional[PT3M39.703125S]]

PID=4592, Root?=true, Info=[user: Optional[Yoga\Christian], cmd: C:\Windows\System32\svchost.exe, startTime: Optional[2017-02-04T20:54:07.621Z], totalTime: Optional[PT14.9375S]]

PID=4628, Root?=true, Info=[]

...

ProcessHandle implementiert vernünftig equals(…) und auch Comparable<ProcessHandle>; die Sortierung ist nach der Prozess-ID.

Methoden-Delegation

Einige Methoden aus Process delegieren an den assoziierten ProcessHandle. Die Methoden heißen gleich.

Process-Methoden Implementierung
long getPid() return toHandle().getPid();
ProcessHandle.Info info() return toHandle().info();
Stream<ProcessHandle> children() return toHandle().children();
Stream<ProcessHandle> descendants() return toHandle().descendants();

Weiterhin gibt es onExit(), supportsNormalTermination(), isAlive(), destroy() und destroyForcibly() auf beiden Typen Process und ProcessHandle.

 

Über Objekte vom Typ ProcessHandle.Info lassen sich weitere Details zum Prozess erfragen; die Rückgaben sind Optional, weil die Informationen vielleicht nicht vorliegen.

  • static interface java.lang.Info
  • Optional<String[]> arguments()
    Programmargumente beim Start.
  • Optional<String> command()
    Ausführbarer Pfadname vom Prozess.
  • Optional<String> commandLine()
    Konkatenation von command() und arguments() beste Repräsentation des Programmaufrufs.
  • Optional<Instant> startInstant()
    Startzeit des Prozesses.
  • Optional<Duration> totalCpuDuration()
    Bisher verbrauchte CPU-Zeit.
  • Optional<String> user()
    Benutzer dieses Prozesses.

 

Prozess-Status erfragen und das Ende einleiten

Mit Methoden von Process lässt sich der Status des externen Programms erfragen und verändern. Die Methode waitFor(…) lässt den eigenen Thread so lange warten, bis das externe Programm zu Ende ist, oder löst eine InterruptedException aus, wenn das gestartete Programm unterbrochen wurde. Der Rückgabewert von waitFor() ist der Rückgabecode des externen Programms, eine zweite Variante von waitFor(…) wartet eine gegebene Zeit. Wurde das Programm schon beendet, liefert auch exitValue() den Rückgabewert. Soll das externe Programm (vorzeitig) beendet werden, lässt sich die Methode destroyXXX() verwenden; das eigene Java-Programm kann nicht beendet werden.

abstract class java.lang.Process

  • abstractintexitValue()
    Wenn das externe Programm beendet wurde, liefert exitValue() die Rückgabe des gestarteten Programms. Ist die Rückgabe 0, deutet das auf ein normales Ende hin. Läuft das Programm noch, gibt es eine IllegalThreadStateException.
  • booleanisAlive()
    Lebt der von der JVM gestartete Unterprozess noch? Ruft intern exitValue() auf und prüft auf IllegalThreadStateException.
  • abstractvoiddestroy()
    Beendet das externe Programm.
  • ProcessdestroyForcibly()
    Standardmäßig wie destroy(), sollte aber von Unterklassen anders implementiert werden. Das brutale Beenden dauert etwas, sodass isAlive() noch eine kurze Zeit true zurückgeben kann.
  • boolean supportsNormalTermination()
    Liefert true, wenn das Programm mit destroy() ohne Probleme beendet werden kann. Unterklassen müssen die Methode überschreiben, sie löst standardmäßig eine UnsupportedOperationException Neue Methode in Java 9.
  • abstractvoidwaitFor()throwsInterruptedException
    Wartet auf das Ende des externen Programms (ist es schon beendet, muss nicht gewartet werden), sonst blockiert die Methode, und liefert dann abschließend den exitValue().
  • booleanwaitFor(longtimeout,TimeUnitunit)throwsInterruptedException
    Wartet die angegebene Zeit auf das Ende des gestarteten Programms. Wurde das externe Programm schon beendet, kehrt die Methode sofort zurück und liefert true; den Exit-Code liefert exitValue() weil hier, anders als bei waitFor() der Rückgabecode ein boolean Läuft das Programm noch, und ist es nicht nach timeout Zeiteinheiten beendet, kehrt die Methode mit false zurück. Die Rückgabe ist true, wenn das gestartete Programm in dem Zeitfenster beendet wurde. Hinweis: Die Methode bricht das externe Programm nicht ab, wenn es nach Überschreiten der Zeit noch läuft. Diese Logik muss ein Programmierer übernehmen und if ( ! waitFor(…) ) mit destroy() kombinieren.
  • CompletableFuture<Process> onExit()
    Kehrt nicht-blockierend direkt zurück und erlaubt später über das CompletableFuture Zugriff auf den Process. CompletableFuture ermöglicht die einfache Verkettung der Art onExit().thenApply( … ) oder process.onExit().whenComplete( (p, ex) -> System.out.printf(„Prozess %d beendet%n“, p.getPid())). Neu in Java 9.

Stream vom Scanner-Tokens generieren

Die in Java 9 eingeführte Objektmethode stream() ist eine sehr gute Ergänzung, denn sie liefert einen Stream<String> von zerlegten Strings.

Beispiel: Durch Komma getrennte String sollen durch ein Zeilenumbruch wieder zusammengefügt werden:

String s = "CNN, Politico, LA Times, New York Times";

System.out.println( new Scanner(s).useDelimiter( "\\s*,\\s*" ).tokens()

                                  .collect( Collectors.joining("\n") ) );