11 minute read

What is REPL?

REPL stands for Read - Eval - Print - Loop. It’s simple interactive computer programming environment for a given programming language, where a user can input data and commands using command line (terminal). The commands and data are read from the command line. Then they are evaluated by the environent, and the result is printed back to the terminal. This sequence of actions can be iterated (reproduced) like in a loop. REPL is similar to Bash and other shells. It skips compilation process and therefore runs faster for small code fragments.

What is it for?

REPL is used for:

  • creating experimental code snippets in terminal, i.e. prototyping
  • ad-hoc testing
  • creating executable scripts
  • learning and playing with code

REPL is like a simple IDE for command line.

JShell - Java REPL

First REPL had been introducted for Lisp. REPL’s are available for many languages, including Scala 2, Scala 3, Python and Groovy. Java REPL is JShell. Jshell has been introduced rather late, only with Java 9 and higher. JDK 9 or newer must be installed before working with JShell. JShell used with Java 17 LTS appears to be still useful. All examples below were tested with JDK 17.

More on JDK 17 JShell - Oracle tutorial


IMPORTANT In case when multiple JDK’s are installed, it’s good to check current version of Java used by the terminal:

~$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing)

If JAVA_HOME points to older Java version than recently installed JDK, and it’s preventing us from using JShell, it should be fixed (most often by .bashrc file editing). Setting environment variables is not a straightforward task further complicated by the fact that sometimes Linux terminal may different Java version than IntelliJ embedded console and so on. JAVA_HOME configuration under Linux and macOS may be easier with jEnv tool. It gives an option of switching JDK version in terminal on the run.


Now, invoke JShell: CTRL + ALT + T for Ubuntu or Tools -> JShell Console in Intellij.

~$ jshell
|  Welcome to JShell -- Version 17.0.5
|  For an introduction type: /help intro

jshell> 

To type a command, use slash (/). For a good start, let’s try /help intro and /help. /help prints command list. /help can be followed by a specific command to get more details, e.g. /help drop and so on. I am not pasting any snippets here as they are self-explanatory and easy to execute in real JShell terminal.

Useful tricks & shortcuts:

CTRL + L - clear terminal. Same thing may be achieved by: System.out.print("\033[H\033[2J");

Simple examples

We can execute simple operations and their result is automatically assigned to a variable named $1, $2 and so on. These variables are reusable:

jshell> "commodore" + "&" + "amiga"
$1 ==> "commodore&amiga"

jshell> 64 * 2
$2 ==> 128

jshell> var concatResult = $1 + $2
concatResult ==> "commodore&amiga128"

Methods can be defined and reused later:

jshell> void doSomething(String param) {
   ...>     System.out.println("Let's do something with this " + param);
   ...> }
|  created method doSomething(String)

jshell> doSomething("today")
Let's do something with this today

Classes can be created and reused. Just like the methods and variables, they are saved as code snippets.

jshell> class Maker {
   ...> }
|  created class Maker

jshell> jshell> var maker = new Maker()
maker ==> Maker@506e1b77

jshell> System.out.println(maker)
REPL.$JShell$22$Maker@506e1b77

TAB after dot [.] shows full extension of typing hints, and shows variants also after incompleted methods, like .getClass(). [press TAB]… Auto-completion is enabled. Recurring TAB also displays documentation (if available) and class or method details.

jshell> maker.
equals(       getClass()    hashCode()    notify()      notifyAll()   toString()    wait(

jshell> doSomething(maker.getClass().getName())
Let's do something with this REPL.$JShell$22$Maker   
jshell> Stream
Stream                     StreamCorruptedException   StreamSupport              
StreamTokenizer            

Signatures:
java.util.stream.Stream<T>

<press tab again to see documentation>
jshell> Stream
java.util.stream.Stream<T>
<no documentation found>
jshell> Stream.
Builder       builder()     class         concat(       empty()       generate(     
iterate(      of(           ofNullable(   
jshell> Stream.generate()
Signatures:
Stream<T> Stream<T>.<T>generate(Supplier<? extends T> s)

<press tab again to see all possible completions>

/imports shows libraries imported by default and import from startup snippets. So we can use for example java.lang.Math or streams from the start.

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> /help imports
|  
|                                  /imports
|                                  ========
|  
|  List the current active imports.  This will include imports from
|  startup snippets.

jshell> 

Forward reference

We can even define a method that uses undefined variables! To put it mildly, such method can use variables that “have not been defined yet in the scope”. (Normally, IDE complains in this way). This feature is being called forward reference

jshell> int multiply() {
   ...>     return n * m;
   ...> }
|  created method multiply(), however, it cannot be invoked until variable n, 
| and variable m are declared

jshell> var n = 64
n ==> 64

jshell> multiply()
|  attempted to call method multiply() which cannot be invoked until variable m is declared

jshell> var m = 128
m ==> 128

jshell> multiply()
$10 ==> 8192

Example of use: throwing exceptions

Suppose I want to use BigDecimal class, but I am not sure where can I expect some pitfalls. Will I get an exception if null is passed to the constructor? Let’s say I am fed up with reading documentation and I want to have a quick look into a practical example. It is possible in JShell with no need of messing with the real project code base:

jshell> new BigDecimal(null)
|  Error:
|  reference to BigDecimal is ambiguous
|    both constructor BigDecimal(java.lang.String) in java.math.BigDecimal and constructor BigDecimal(java.math.BigInteger) in java.math.BigDecimal match
|  new BigDecimal(null)
|  ^------------------^

We can see we are getting a clear error message. Nice. Another try:

jshell> String rate = null
rate ==> null

jshell> var convertedRate = new BigDecimal(rate)
|  Exception java.lang.NullPointerException: Cannot invoke "String.toCharArray()" because "val" is null
|        at BigDecimal.<init> (BigDecimal.java:900)
|        at do_it$Aux (#2:1)
|        at (#2:1)

jshell> 

Obviously I need to defend against null. What about not properly formatted String input?

jshell> var convertedRate = new BigDecimal("0,64")
|  Exception java.lang.NumberFormatException: Character , is neither a decimal digit number, decimal point, nor "e" notation exponential mark.
|        at BigDecimal.<init> (BigDecimal.java:586)
|        at BigDecimal.<init> (BigDecimal.java:471)
|        at BigDecimal.<init> (BigDecimal.java:900)
|        at do_it$Aux (#3:1)
|        at (#3:1)

Functional programming, lambdas and streams in JShell

I can declare functional interface with Single Abstract Method (SAM) and write implementation:

jshell> interface DoSomething {
   ...>    void something();
   ...> }
|  created interface DoSomething

jshell> DoSomething sth = () -> System.out.println("Do sth!")
sth ==> $Lambda$21/0x0000000800c0a830@9807454

jshell> sth.something()
Do sth!
jshell> interface Application {
   ...>     Set<Class<?>> getClasses();
   ...> }
|  created interface Application

jshell> Application a = () -> new HashSet<Class<?>>(Arrays.asList(Math.class, Integer.class))
a ==> $Lambda$20/0x0000000800c0a000@7530d0a

jshell> a.getClasses()
$6 ==> [class java.lang.Integer, class java.lang.Math]

Suppose we have test data text file:

1
2
3
4
null
8
89
null

Let’s upload it and covert it to Stream<String>

jshell> String fileName = "/home/IdeaProjects/blog/assets/sample.txt"
fileName ==> "/home/IdeaProjects/blog/assets/sample.txt"

jshell> Stream<String> stream = Files.lines(Paths.get(fileName))
stream ==> java.util.stream.ReferencePipeline$Head@7eda2dbb

jshell> stream.takeWhile(s -> !"null".equals(s)).forEach(s -> System.out.println(s))
1
2
3
4

We’ve processed the stream until first “null” string (exclusive).

takeWhile() has been introduced in Java 9. It accepts values while the filter is true, then stops the stream (when filter() processes all the values that match the filter).

Another trick: how to get rid of Optional.empty() from stream in an elegant but maybe too sophisticated way? Let’s have an input collection and map to empty when null:

jshell> var list = Arrays.asList(1, 2, 3, null, 5, null, 7, 8, null)
list ==> [1, 2, 3, null, 5, null, 7, 8, null]

# the old way
jshell> list.stream().map(Optional::ofNullable).forEach(System.out::println)
Optional[1]
Optional[2]
Optional[3]
Optional.empty
Optional[5]
Optional.empty
Optional[7]
Optional[8]
Optional.empty

# Java 9 introduced Optional::stream variant
jshell> list.stream().map(Optional::ofNullable).flatMap(Optional::stream).forEach(System.out::println)
1
2
3
5
7
8

Another example. Let’s open a stream of data:

jshell> InputStream is = new URL("http://ip.jsontest.com").openStream()
is ==> sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@533ddba

and transfer it to an output stream:

jshell> var os = new ByteArrayOutputStream()
os ==>

jshell> is.transferTo(os)
$3 ==> 24 # 24 bytes transferred

jshell> os
os ==> {"ip": "81.204.165.99"} # web returned json containing IP of the sender

Here we used some smart website jsontest.com. And the inspiration for this was a great JUG presentation by Jakub Marchwicki

Running Java code before JShell and JDK9

Without JShell, showing working Java code wasn’t easy nor fast. Before JDK 9 introduced JShell, executing even the simpliest Java code required creating a class first, and then its compilation to .class file (containg bytecode) that would be executed next with JRE command. For example, sample Java class:

public class HulloDarling {
    
    public static void main(String[] args) {
        System.out.println("Hullo, darling!");
    }
}

has been saved to .java file: HulloDarling.java

Now let’s compile it.

javac HulloDarling.java

If the compilation process is successful, it will create a file with .class extension: HulloDarling.class

Execute the code (but note there shall be no file extension this time):

java HulloDarling

Stdout prints String Hullo, darling! from the main method.


IMPORTANT Further complication is fact that .java file should have the same name as the class in this file, if the class is public. The compiler complaints if the class name is different. After changing the name to HulloDarling2:

javac HulloDarling.java
HulloDarling.java:1: error: class HulloDarling2 is public, should be declared in a file named HulloDarling2.java
public class HulloDarling2 {
       ^
1 error

A top-level class cannot be private nor protected, because it does not have an entry point. Only nested and inner classes can be private or protected.

javac HulloDarling.java
HulloDarling.java:1: error: modifier private not allowed here
private class HulloDarling2 {
        ^
1 error

The default modifier (i.e. no modifer) is allowed. In this case, we are also allowed to compile .java file that contains a class with different name:

javac HulloDarling.java
java HulloDarling2
Hullo, darling!

Please note that compiler created .class file named accordingly to the class name, ignoring the file name. To be able to launch the code, a class must contain main() method.


Running Java code: JDK 9+

Since JDK 9, it is possible to execute simple Java code skipping compilation step. It is possible even without JShell. Let’s just run .java file:

java HulloDarling.java

In case of error: class found on application class path: HulloDarling we should remove .class file of the same name, to launch the code directly from the source file. It looks like it was previously compiled “the older way”.

JShell: code snippets

JShell facilitates our work as well. We do not even need to create a file. A code snippet in terminal is enough. Once created or executed, these pieces of code are not lost.

jshell> /list

It shows what we’ve created before, even if it’s been not saved.

Save to file is possible with /save:

jshell> /save snippet.jsh

With /edit on given snippet, we can use graphical Swing editor to refactor the code (even if it had not been explicilty saved to a file):

Edit JShell snippet in Swing editor


Even after closing and reopening JShell, the snippet will be available end executed:

jshell> /open snippet.jsh

To quit JShell, simply use

jshell> /exit
|  Goodbye

Java scripting

From now on, we can create scripts like in Bash, Groovy or Python. No need of compilation.

To create executable script in Java, start a file with shebang: #! and full path to java, like /home/[PATH]/bin/java or a path to Java symbolic link, redirecting to Java location in file system: #!/usr/bin/java. Either of them should be followed by --source 17.

#!/usr/bin/java --source 17

Then simple Java class with classic HelloWorld example:

public class HulloJava {
    public static void main(String[] args) {
        System.out.println("Hullo, it's Java script!");
    }
}

We can even pass arguments to the scipt! Let’s use them in the code. The entire file can have any filename extension, and it should look like:

#!/usr/bin/java --source 17
public class HulloJava {
    public static void main(String[] args) {
        System.out.println("Hullo, it's Java script! You passed following args: " + String.join(" ", args));
    }
}

Now, let’s invoke the script from terminal (not from JShell!):

./HulloJava.script never ever always
Hullo, it's Java script! You passed following args: never ever always

Key takeaways

  • JShell is useful and powerful tool for learning, development, testing and debugging
  • now it is easier to run Java code in terminal directly from .java file
  • it is possible to execute Java code in terminal as Shell-like script

Update 31.10.2023

JShell offers plenty of useful shortcuts. You can check them by typing

jshell> /help shortcuts

For example, while declaring a variable of type:

jshell> new Random()

press Shift+TAB, and then v:

jshell> Random  _= new Random()

It will add type of variable and place cursor when the variable name is expected.

jshell> Random generator = new Random()
generator ==> java.util.Random@52cc8049

Now, pressing TAB after the variable name shows signature of the variable. TAB after dot shows available methods:

jshell> generator
Signatures:
generator:java.util.Random

<press tab again to see documentation>
jshell> generator
generator:java.util.Random
<no documentation found>
jshell> generator.
doubles(            equals(             getClass()          hashCode()          
ints(               isDeprecated()      longs(              nextBoolean()       
nextBytes(          nextDouble(         nextExponential()   nextFloat(          
nextGaussian(       nextInt(            nextLong(           notify()            
notifyAll()         setSeed(            toString()          wait(   

Typing string after dot and then TAB limits hints to matching method names:

jshell> generator.next
nextBoolean()       nextBytes(          nextDouble(         nextExponential()   
nextFloat(          nextGaussian(       nextInt(            nextLong(           
jshell> generator.next

Arrows up and down are scrolling through command history.

Shift + TAB and then i automatically look up for possible imports (when something is not resolvable in current context, meaning: it’s not been imported yet, and it does not belong to default imports):

jshell> ZonedDateTime
0: Do nothing
1: import: java.time.ZonedDateTime
Choice: 
Imported: java.time.ZonedDateTime