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

Seite 2: Potenzial

Inhaltsverzeichnis

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.