Prototyping with JShell
Exploring REPL-driven development in JShell
JShell (Java Shell) is an interactive command-line REPL (Read-Evaluate-Print Loop) for Java, available since Java 9. The motivations for having such a tool for the Java ecosystem can be found in JEP 222:
- learning the Java language and APIs
- investigating APIs and prototyping (see how to use external libraries in JShell)
The Java developer ecosystem is heavily IDE-oriented. I welcome the shift to make Java more friendly to simple text editors, and having a REPL is a step in this direction.
Although it's certainly great for learning the language I was more curious about what JShell can add to the toolbox of the Java developer's day-to-day business. Because reports on JShell with hands-on experience beyond a simple hello world are so rare, I thought it would be worth to try to solve actual problems and see how it fares.
I choose Project Euler's first few problems for this challenge, as many of its problems can be solved with a short program using the standard libraries.
The good
The first good surprise was how lenient JShell is when it comes to accepting code snippets. Terminal semicolons can be omitted and expressions can be entered without enclosing them into a method. Also, there's no need to worry about many of the commonly used packages, as they are imported by default:
| Welcome to JShell -- Version 9.0.1
| For an introduction type: /help intro
jshell> LongStream.range(1, 5).sum()
$1 ==> 10
Variables can be redeclared, so it’s easy to fix any line of code (including a declaration) by loading it from history (up key) and changing the code.
Tab completion can cycle through overloaded variants of the method and show JavaDoc.
These features provided a solid ground for my trials, especially when I decided to write a one-liner solely with the Streams API to come up with a solution for a given problem.
Another cool feature is that the whole session can be dumped into a file. This came in handy when I took a break but wanted to reuse my snippets later.
The bad
For simple one-liners JShell is awesome, but when something more complex is needed, things get less and less comfortable.
One such case is implementing interfaces or extending classes.
The first pain point I've bumped into is that autocompletion does not list interfaces upon creating anonymous classes. This might be something that JShell will address later as the same works for abstract classes.
A bigger problem is that there's no aid to override the correct methods in the new class. An IDE provides many features that support this:
- autocomplete for the candidates
- generate method stubs
- peek into the source code of the interface or class you are working with
None of these are supported by JShell. In my case, it was not a huge deal to figure out the method signature,
because LongSupplier
is well-documented, and it even mentions that the method I had to implement is getAsLong
.
However, for more complex cases and most 3rd party libraries, this would have been a bigger problem.
Another thing that caught me off-guard once is that autocomplete silently dies when it encounters a syntax error instead of complaining about the problem.
For example, because Long.getLong
returns a Long
, the autocomplete in the following example
shows methods of the Long object:
jshell> Long.getLong("10", 10).<tab>
byteValue()
compareTo(
doubleValue()
equals(
floatValue()
...
However, by introducing an extra parenthesis, it will just print the list of all available classes and packages.
jshell> Long.getLong("10", 10)).<tab>
AbstractCollection
AbstractExecutorService
AbstractList
...
Of course, this is easy to spot, but there are more subtle cases:
jshell> Integer x(Integer a) { return a; }
| created method x(Integer)
jshell> x(60085147).<tab>
// shows methods on the Integer type :)
jshell> x(600851475143).<tab>
// no autocompletion :(
Luckily, the workaround is simple: evaluate the line in question, and the error will reveal itself:
jshell> x(600851475143)
| Error:
| integer number too large: 600851475143
| x(600851475143)
|
The ugly
Using JShell to evaluate one-liners is a breeze, but defining methods that span over multiple lines is a real pain.
JShell's history does not support multi-line snippets. Whenever you define a method, you can't easily recall the whole definition from history, but you have to do that line-by-line.
It does not handle indentations automatically, and whenever autocompletion is used, search results are breaking the output, so you can no longer see the whole method.
Also, if you spot a mistake in the previously entered line, you just can't go back and fix it.
So, when I had to define a method that required a bit more lines of code, I've opened an external editor
with the /edit
command. Of course, breaking out of JShell means losing all its nice features like
the built-in autocompletion but I thought that defining a single method shouldn't be too hard.
I've hit /edit
to open Vim, typed my code, hit save, only to realize that somewhere I've made a small
mistake, which made my snippet invalid.
jshell> /edit 7
| Error:
| incompatible types: int cannot be converted to java.lang.Long
| for (Long c = 2; c <= n; c++) {
| ^
| modified method x()
Oh okay, it happens, let's just reopen the code.
However, when I listed the available snippets with /list
, the code was gone. Because it was
not valid, it was not even saved. In order to fix my error, I had to retype the whole thing.
Summary
JShell is a great addition to the ecosystem, with the potential to aid new joiners as well as professionals on their day-to-day job. However, in its current form, it feels only half-baked. Many of its features, like the autocompletion, are well-thought and nice to use. On the other hand, its quirks make development hard really soon.
I hope that in the future these features will be streamlined, and JShell can become an addition or even a replacement to and IDE based workflow.