Java REPL: JShell
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):
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