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