Peter Chng

JShell: The Java REPL

A REPL (Read-Evaluate-Print Loop) environment is a great way to learn a programming language, try out new ideas, and get feedback immediately on how things work (or don’t work). Keeping the feedback cycle short is essential to quick prototyping and exploring new ideas effectively.

For the longest time, Java didn’t have a first-party REPL, (though there were some third-party options) and in this respect was behind other languages like Python and Scala, which had built-in support.

This changed with JDK 9, which included the Java Shell tool or JShell REPL. Though this was released back in 2017, in my experience jshell isn’t often used.

Here, I’ll go over some common use cases.

JShell Examples

These examples were done using jshell from Oracle JDK 17 running on Ubuntu.

The syntax for jshell is:

jshell <options> <files>

Before we look at what the options and files arguments are, let’s look at some simple examples:

Basic Usage: Hello World and exit

# Launch jshell environment
jshell

|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro

jshell> System.out.println("Hello World")
Hello World

jshell> 2+2
$2 ==> 4

jshell> var map = new HashMap<String, String>()
map ==> {}

jshell> map.put("key", "value")
$6 ==> null

jshell> map
map ==> {key=value}

jshell> /exit
|  Goodbye

Entering statements and expressions is supported in jshell, and each entry is called a snippet. Expressions not part of a statement/declaration get assigned to scratch variables, which are prefixed by a $. Note that semicolons are generally optional, and will be automatically added to single-line snippets.

Commands within the jshell REPL

The jshell environment has its own set of commands that can be used to control the environment. All commands are prefixed with a / and you can see a list of all commands by entering /help. You can get more information about a command by using /help <command>.

For example, you can use /list to show all the previous snippets entered:

jshell> 2+2
$1 ==> 4

jshell> var map = new HashMap<String, String>()
map ==> {}

jshell> map.put("key", "value")
$3 ==> null

jshell> /list

   1 : 2+2
   2 : var map = new HashMap<String, String>();
   3 : map.put("key", "value")

History Navigation and Autocomplete

JShell supports history navigation and the Tab key for autocomplete, much as you may be used to with your OS’s shell.

For example, you can use Up Arrow to navigate to the most previous entered command/snippet. If you start typing something (a prefix), and press Up Arrow, JShell will go back to the most preveious command that matched that prefix.

More information is available at the Shell Editing documentation.

You can Tab to autocomplete almost anything - commands, names of existing variables, class names, method names, etc. If there isn’t an unambiguous autocomplete target, a list of possible autocomplete options will be shown.

These features make JShell much easier, fun, and intuitive to use.

Startup scripts and imports

You’ll note that we could use HashMap without having to import it. This is because by default there are many imports already done on startup:

jshell> /list -start

  s1 : import java.io.*;
  s2 : import java.math.*;
  s3 : import java.net.*;
  s4 : import java.nio.file.*;
  s5 : import java.util.*;
  s6 : import java.util.concurrent.*;
  s7 : import java.util.function.*;
  s8 : import java.util.prefs.*;
  s9 : import java.util.regex.*;
 s10 : import java.util.stream.*;

In the very first example, we had to use System.out.println() and this can be a little verbose. You can start jshell and have it load the predefined PRINTING script to include print, println, and printf methods in scope:

jshell PRINTING
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro

jshell> print("Hello World")
Hello World

For access to even more of the JDK APIs without having to import them, you can launch with jshell JAVASE that imports the core Java SE APIs contained within the java.se module. However this will cause a delay in the startup time.

jshell JAVASE
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro

jshell> Instant.now()
$177 ==> 2021-10-20T05:00:31.821430100Z

The difference between startup scripts and loaded scripts, and resetting your session

When you use jshell JAVASE or jshell PRINTING it is loading those predefined scripts after the separate startup script runs. By default, the startup script is set to the predefined DEFAULT one, which results in the imports seen above when we ran /list -start.

You can explicitly set the startup script by using the --startup option. (It can also be set using a command after you’ve launched JShell) The difference between a startup script and a script that’s loaded afterward is that when you /reset the session, the startup scripts are re-run, but any scripts you loaded are not. Instead, you’d have to load them again using the /open command. See the following example:

jshell PRINTING
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro

jshell> // The default startup script imports HashMap

jshell> var map = new HashMap<String, String>()
map ==> {}

jshell> print("This works because we loaded the built-in PRINTING script on launch")
This works because we loaded the built-in PRINTING script on launch

jshell> /reset
|  Resetting state.

jshell> print("This won't work because we reset")
|  Error:
|  cannot find symbol
|    symbol:   method print(java.lang.String)
|  print("This won't work because we reset")
|  ^---^

jshell> /open PRINTING

jshell> print("This works again after re-opening PRINTING")
This works again after re-opening PRINTING

Verbose mode and viewing the variables in scope

Expressions automatically get assigned into scratch variables, as indicated before, but this isn’t immediately obvious. Enabling verbose feedback mode with the -v flag at startup makes jshell tell us exactly what it’s doing:

jshell -v
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro

jshell> 2+3
$1 ==> 5
|  created scratch variable $1 : int

jshell> var map = new HashMap<String, String>()
map ==> {}
|  created variable map : HashMap<String,String>

jshell> map.put("key", "value")
$3 ==> null

We can verify all the variables that were defined (either explicitly or with scratch variables) by using the /vars command:

jshell> /vars
|    int $1 = 5
|    HashMap<String,String> map = {key=value}
|    String $3 = null

Here you can see that the return value of Map.put() is null and was assigned to the scratch variable $3. This is because Map.put() returns the previous value associated with the key. Since there was no previous value, the return value is null.

You can alter the feedback after jshell is started by using the /set feedback command, which will show the available modes, e.g. /set feedback normal

Method and Class definitions

You can easily define a method without enclosing it in a class: (In this respect it’s more like a function declaration)

jshell> int add(int a, int b){
   ...>     return a + b;
   ...> }
|  created method add(int,int)

jshell> add(2, 5)
$2 ==> 7
|  created scratch variable $2 : int

Note that in this case, the semicolons in the method’s body (e.g. for the return statement) are required.

You can still define classes if you wish, though this tends to be a bit verbose using the default JShell experience:

jshell> class MyClass {
   ...>     int myField;
   ...>     public MyClass(int value){
   ...>         myField = value;
   ...>     }
   ...>     public int getMyField(){
   ...>         return myField;
   ...>     }
   ...> }
|  created class MyClass

jshell> var instance = new MyClass(5)
instance ==> MyClass@7cc355be
|  created variable instance : MyClass

jshell> instance.getMyField()
$5 ==> 5
|  created scratch variable $5 : int

Redefining symbols

With a REPL, we’re often doing exploratory programming: That is, trying out new things without knowing what will and won’t work. This means we may need to redefine variable names, methods, or classes, which normally isn’t supported in Java. It is, however, supported in JShell:

jshell> var map = new HashMap<String, String>()
map ==> {}
|  created variable map : HashMap<String,String>

jshell> var map = new TreeMap<String, String>()
map ==> {}
|  replaced variable map : TreeMap<String,String>
|    update overwrote variable map : HashMap<String,String>

jshell> /list

   2 : var map = new TreeMap<String, String>();

Note that after redefining the map variable, running /list only shows the most recent (or active) snippet pertaining to it.

You can also redefine methods. Suppose we first define an unbiased flipCoin() method:

jshell> String flipCoin(){
   ...>     if (Math.random() < 0.5) {
   ...>         return "Heads!";
   ...>     } else {
   ...>         return "Tails!";
   ...>     }
   ...> }
|  created method flipCoin()

jshell> flipCoin()
$2 ==> "Heads!"
|  created scratch variable $2 : String

// This is just a convenient one-liner. A for-loop would probably be better.
jshell> IntStream.range(0, 100).mapToObj(i -> flipCoin()).filter(x -> x.equals("Heads!")).count()
$3 ==> 45
|  created scratch variable $3 : long

We could now make our coin biased by redefining flipCoin() like so:

jshell> String flipCoin(){
   ...>     if (Math.random() < 0.8) { // Biased toward heads
   ...>         return "Heads!";
   ...>     } else {
   ...>         return "Tails!";
   ...>     }
   ...> }
|  modified method flipCoin()
|    update overwrote method flipCoin()

jshell> IntStream.range(0, 100).mapToObj(i -> flipCoin()).filter(x -> x.equals("Heads!")).count()
$6 ==> 79
|  created scratch variable $6 : long

Forward References

You can declare methods that reference methods, variables, or classes that haven’t been defined yet. For example, assume we don’t yet have an add() method and we’ve implemented a very naive multiply() method as follows:

jshell> int multiply(int a, int b) {
   ...>     int sum = 0;
   ...>     for (int i = 0; i < b; i++){
   ...>         sum = add(sum, a);
   ...>     }
   ...>     return sum;
   ...> }
|  created method multiply(int,int), however, it cannot be invoked until method add(int,int) is declared

jshell> multiply(9, 3)
|  attempted to call method multiply(int,int) which cannot be invoked until method add(int,int) is declared

jshell> int add(int a, int b) {
   ...>     return a + b;
   ...> }
|  created method add(int,int)
|    update modified method multiply(int,int)

jshell> multiply(9, 3)
$4 ==> 27
|  created scratch variable $4 : int

We were able to declare multiply() without add() being defined yet. But attempting to call it at this point won’t work.

After we define add(), the multiply() method is automatically updated so that it knows to call the new add() method, and things work. If we were to modify add() again, then multiply() would also be updated to reflect this change.

Snippet Transformation and Auto Imports

You can use the keystrokes Shift+Tab, i to perform an automatic import on a fully-qualified class name. For example, if you did not start JShell with the JAVASE predefined script, Instant won’t be imported. You could then do this:

jshell> Instant.now() // Not imported yet
|  Error:
|  cannot find symbol
|    symbol:   variable Instant
|  Instant.now()
|  ^-----^

jshell> Instant<Shift Tab, i>
0: Do nothing
1: import: java.time.Instant
Choice:
Imported: java.time.Instant
jshell> Instant.now()
$2 ==> 2021-10-27T04:14:30.619026200Z
|  created scratch variable $2 : Instant

The keystrokes Shift+Tab, i bring up an interactive menu where you can select which import you want to do. This may save you some time from having to import the class on your own.

Conclusion

This was a brief introduction to the jshell REPL, the structure of which was mostly geared toward my own note-taking for future reference. I mostly end up using JShell to test out how new JDK APIs or syntax changes work - especially since there have been many changes since Java 8 with the quicker pace of JDK releases in recent years. I find this to be quicker than writing a unit test, and JShell’s REPL supports a more interactive and exploratory way to test things out.

For more information about JShell, including using an external editor, including external code, or even defining custom feedback modes, check out the official user’s guide or jshell reference.

Other References

  1. JShell Tutorial (Robert Field)
  2. JShell - The Java Shell Tool (dev.java)
  3. JShell architecture (Iakovos Nomikos)