JShell – Read-Eval-Print Loop für Java | heise online

JShell – Read-Eval-Print Loop für Java

Andere Sprachen haben es vorgemacht, zum Beispiel Scala auf der Java Virtual Machine: Es ist möglich, Ausdrücke interaktiv einzugeben und auszuwerten. Die Rede ist von REPL (Read-Eval-Print Loop), die Java 9 enthalten wird.

vorlesen Druckansicht 4 Kommentare lesen
JShell – Read-Eval-Print Loop für Java
Lesezeit: 9 Min.
Von
  • Michael Müller
Inhaltsverzeichnis

Java 8 umfasst die JavaScript-Engine Nashorn. Sie lässt sich nutzen, um Java-Ausdrücke interaktiv auszuführen. Aber das ist recht umständlich. Insbesondere müssen Bibliotheken im Classpath liegen, der beim Start von Nashorn angegeben wird, und die Java-Ausdrücke sind in JavaScript einzukleiden.

Mehr Infos

Neues aus der Java-Welt

Für 2017 sind mit Java EE 8 und Java 9 neue Versionen der Programmierplattform angekündigt. Eine lose Folge von Artikeln zu den wichtigsten Neuerungen wird auf heise Developer Einblicke in die nahe, hier auch ferne Java-Zukunft geben.

Mit Java 9 ändert sich das. Selbst wenn diese Version für andere Dinge wie Modularisierung oder Wertetypen bekannt ist, gibt es einen kleinen Schatz zu heben: ein Juwel namens The Java Shell oder kurz JShell. Dabei handelt es sich um eine interaktive Shell, die alles bieten soll, was man sich für REPL vorstellt. Standardisiert als JEP 222 (Java Enhancement Process) ist sie auch als Projekt Kulla bekannt. Um es auszuprobieren, muss man einfach das JDK von der Projektseite herunterladen und installieren. Im Unterverzeichnis bin befindet sich jshell (bei Windows jshell.exe).

Nach dem Start meldet sich die JShell mit dem Prompt jshell>. Hier können nun Java-Anweisungen mit speziellen JShell-Kommandos eingegeben werden. Sie beginnen mit einem Schrägstrich "/". Zum Beenden der JShell ist beispielsweise /exit erforderlich, ein einfaches "exit" wie bei einer Linux-Shell oder der Windows-Kommandozeile reicht nicht. Befehle ohne Schrägstrich werden als Java interpretiert. Im Zweifelsfall gibt das System einfach die Meldung aus, es kenne die Variable exit nicht. /help oder /? listet die verfügbaren Kommandos und /help /kommando liefert eine kurze Erläuterung zum einzelnen Kommando, wobei auch hier der Schrägstrich anzugeben ist.

Die Shell ist außerdem in der Lage, die Anweisungen in verkürzter Form zu erkennen. Insbesondere ist die Angabe des Semikolons in der Regel nicht mehr erforderlich. Jeder Wert wird automatisch einer temporären Variablen zugewiesen, deren Name mit einem Dollarzeichen beginnt und einfach durchnumeriert wird. Die Shell gibt sogleich das Ergebnis nebst der genutzten Variablen aus. Der folgende Ausschnitt zeigt die Ausgabe nach Eingabe des Strings "123-abc-456".

jshell> "123-abc-456"
$1 ==> "123-abc-456"

Trotz des interpretierenden Charakters bleibt Java in der Shell streng typisiert. Sichtbar ist das beispielsweise, wenn Entwickler eine Variable selbst benennen möchten. Ohne korrekte Typangabe führt das zu einem Fehler.

jshell> start = $1.indexOf("-")
| Error:
| cannot find symbol
| symbol: variable start
| start = $1.indexOf("-")
| ^---^

Mit Typangabe ist jedoch alles gut:

jshell> int start = $1.indexOf("-")
start ==> 3

Die Java-Shell ist keine IDE mit Autocompletion und anderen liebgewonnenen Features. Aber ähnlich einer Linux-Shell bietet sie eine Vervollständigung oder Anzeige einer Auswahl mittels Tab-Taste.

jshell> $1.sub{Tab}
subSequence( substring(

Interessant ist es bei der Shell, wenn es darum geht, Anweisungen oder ganze Bibliotheken kennenzulernen. Um die Funktionsweise einer Anweisung auszutesten, ist es nicht mehr erforderlich, ein vollständiges Java-Programm zu erstellen, zu kompilieren und auszuführen. Das geht nun interaktiv. Doch wie kommt die Shell an die passende Bibliothek? Sie muss im Classpath liegen und importiert werden. Er lässt sich via /classpath dynamisch zuführen, der Rest ist bekanntes Java:

jshell> /classpath ~/Dropbox/Vortrag/ParallelStreams/
ParallelStreams/target/ParallelStreams.jar
| Path '~/Dropbox/Vortrag/ParallelStreams/ParallelStreams/target/
ParallelStreams.jar' added to classpath

jshell> import de.muellerbruehl.parallelstreams.PersonManager

jshell> PersonManager.getInstance()
$6 ==> de.muellerbruehl.parallelstreams.PersonManager@cb0ed20

Die Importanweisung ist eine der wenigen, die – sofern OK – keine Ausgabe der Shell zur Folge hat. Der hier gezeigte PersonManager hat die Aufgabe, eine Menge zufälliger Personen zu erzeugen, die sich als Liste abrufen lässt. Damit ist es beispielsweise möglich, im Dialog den Umgang mit Streams und Lambda-Ausdrücken zu üben.

jshell> $6.getPersons().stream().filter(p -> p.isFemale()).count()
$7 ==> 25410

Da in der Shell kein Semikolon erforderlich ist, um das Ende einer Anweisung zu markieren, versucht die Shell, eine Zeile immer als eine Anweisung zu interpretieren.

jshell> $6.getPersons().stream()
$8 ==> java.util.stream.ReferencePipeline$Head@cb0ed20

jshell> $8.count()
$9 ==> 50000

Nur wenn die Shell eindeutig erkennt, dass die Anweisung unvollständig ist, wird sie fortgesetzt. Zur visuellen Darstellung schaltet sie dazu dem Prompt um:

jshell> $6.getPersons().stream().
...> count()
$10 ==> 50000

Allerdings scheint die Shell noch nicht ganz ausgereift. Im Beispiel liefert $9.getPersons() eine List<Person>. Soll die Liste direkt einer Variablen zugewiesen werden, kommt es derzeit zum Crash.

shell> $6.getPersons()
| State engine terminated.
| Restore definitions with: /reload restore
Exception in thread "main" java.io.UTFDataFormatException
at java.io.ObjectOutputStream$BlockDataOutputStream.
writeUTF(java.base@9-ea/ObjectOutputStream.java:2169)
at java.io.ObjectOutputStream$BlockDataOutputStream.
writeUTF(java.base@9-ea/ObjectOutputStream.java:2012)
at java.io.ObjectOutputStream.writeUTF(java.base@9-ea/
ObjectOutputStream.java:869)
at jdk.internal.jshell.remote.RemoteAgent.
commandLoop(jdk.jshell@9-ea/RemoteAgent.java:136)
at jdk.internal.jshell.remote.RemoteAgent.
main(jdk.jshell@9-ea/RemoteAgent.java:61)
$11 ==>

Dies liegt allerdings in der Datenmenge begründet. Enthält die Liste rund 17.000 Einträge oder mehr, crasht die JShell. Bei kleineren Datenmengen funktioniert die Liste wie gewohnt. Die Daten lassen sich jedoch entsprechend dem angezeigten Vorschlag mit /reload restore wiederherstellen. Das geschieht im Prinzip so, dass alle erfolgreich ausgeführten Anweisungen wiederholt werden. Das ist möglich, da die Shell ein Log über die bisherigen Aktivitäten führt. Es lässt sich mit /list abrufen.

jshell> /list

1 : "123-abc-456"
2 : int start = $1.indexOf("-");
3 : int stop = $1.indexOf("-", start+1);
4 : $1.substring(start+1, stop)
5 : import de.muellerbruehl.parallelstreams.PersonManager;
6 : PersonManager.getInstance()
7 : $6.getPersons().stream().filter(p -> p.isFemale()).count()
8 : $6.getPersons().stream()
9 : $8.count()
10 : $6.getPersons().stream().
count()

Sollen nicht nur die (erfolgreich) ausgeführten Anweisungen, sondern alle Eingaben des Anwenders ausgegeben werden, kommt stattdessen /history zum Einsatz. Die Shell-Kommandos lassen sich soweit abkürzen, wie sie noch eindeutig sind. So können Entwickler /history mit /hi abkürzen, nicht jedoch mit /h, da das nicht mehr von /help zu unterscheiden wäre. Wie bei den meisten modernen Shells ist es möglich, mittels der Pfeiltasten (auf, ab) in der Historie zu blättern.

Die Liste der bisherigen Anweisungen lässt sich nicht nur anzeigen, sondern mit /edit auch in einen Editor laden und ändern. Der Standard-Editor verfügt dann über eine Schaltfläche "accept", mit der die geänderte Anweisungsfolge ausgeführt wird. Bei Bedarf können Anwender mit /set editor <pfad zum Editor> ihren Lieblingseditor einstellen. Das funktioniert, sofern er aus der Shell heraus gestartet wird. Läuft der Editor bereits, werden die Code-Snippets in der vorhandenen Instanz angezeigt, jedoch nach dem Speichern nicht übernommen. Wie der angeführte Crash bei großen Listen deutet das darauf hin, dass sich die Shell noch in der Entwicklung befindet.

Die Anweisungsliste lässt sich aber nicht nur editieren, sondern mit /save filename speichern und zu einem späteren Zeitpunkt via /open filename wieder laden und ausführen. Interessanterweise führt die Anweisungsliste nur Java-Anweisungen auf. Ein eventuell erforderlicher Classpath wird nicht festgehalten und ist bei Bedarf wieder manuell vorab zuzufügen. Es ist jedoch möglich, den Classpath außerhalb der Shell zu skripten, indem die Shell mit dem Parameter -cp Klassenpfad aufgerufen wird. Alternativ können Entwickler anstelle der Anweisungen mittels /save history filename auch die Historie speichern und erneut laden. Führt die Eingabefolge jedoch wie oben zu einem Crash, endet der Replay genau an der Stelle. Die Datei lässst sich jedoch mit einem Texteditor öffnen und vor dem Replay entsprechend bereinigen.

Im Laufe einer Session können Entwickler zahlreiche Variablen anlegen. Insbesondere die automatisch erstellten verfügen über keinen sprechenden Namen. Um hier den Überblick zu behalten, ist es jederzeit möglich, über /vars eine Liste der aktuell belegten Variablen abzurufen.

Weiter oben wurde veranschaulicht, dass Klassen – wie in Java üblich – zu importieren sind. Andernfalls wird ein Fehler gemeldet. Dennoch hat der Artikel einige Anweisungen gezeigt, ohne dass hierfür ein expliziter Import durchgeführt wurde. Das liegt darin begründet, dass die Shell einige Importanweisungen beim Start automatisch durchführt. Die versteckten Anweisungen werden mit /list all sichtbar. Welche Klassen bereits importiert wurden, zeigt /imports.

Die Shell bietet noch einige weitere Anweisungen, die sich beim Aufruf der Hilfe auflisten lassen. Mit dem bisherigen Wissen sollten sie verständlich sein, sodass sie hier nicht im Detail besprochen werden.

Auch wenn die Shell derzeit noch mit kleineren Fehlern daherkommt, ist doch erkennbar, was in ihr steckt. Insbesondere ist sie dazu geeignet, Anweisungen mal eben kurz zu testen. Eine weitere Einsatzmöglichkeit ist das Erkunden von Bibliotheken. Oder es lassen sich kleine Codeabschnitte interaktiv entwickeln und in den eigentlichen Sourcecode übernehmen. Ein Beispiel hierfür sind komplexe reguläre Ausdrücke, die oft nicht im ersten Wurf gelingen. Hier kann das interaktive Vorgehen Zeit sparen.

Michael Müller
ist als Bereichsleiter Softwareentwicklung der InEK GmbH verantwortlich für Projekte im Web-, Java- und .NET-Umfeld. Daneben betätigt er sich als freier Autor und verfasst Fachartikel zu diversen Entwicklungsthemen sowie Buchrezensionen.
(ane)