☕ Java

JShell

JShell, introduced in Java 9, is an interactive Read-Eval-Print Loop (REPL) bundled directly with the JDK, allowing Java statements, expressions, method, class, and variable declarations to be entered and evaluated one at a time at an interactive prompt, with immediate feedback — without requiring a class wrapper, a main method, explicit compilation, or a build tool, all of which Java had always required for even the smallest snippet of executable code before Java 9. This solved a long-standing friction point for learning, quick experimentation, and API exploration, where languages like Python or Ruby offered an immediate interactive prompt but Java required the full edit-compile-run cycle even to test a single line. This entry covers what problem JShell solves and how it differs from a script, core REPL usage including implicit variable creation and forward references, snippet management commands, and how to feed JShell external classes and files.

Motivation — Java Lacked an Interactive Prompt

Before Java 9, evaluating even the simplest expression required the full ceremony Java has always demanded: writing a class, giving it a public static void main(String[] args) entry point, saving it to a file whose name matched the class name, invoking javac to compile it, and only then running it with java — a cycle that, for a one-line experiment like checking what String.format("%.2f", 3.14159) actually prints, was disproportionately heavyweight compared to dynamically-typed scripting languages where a REPL (Read-Eval-Print Loop) let a user type an expression and see its result immediately. This friction mattered most in exactly the situations where quick iteration is most valuable: learning the language, exploring an unfamiliar API's behavior, prototyping a small algorithm, or debugging a hypothesis about how a particular method behaves. JShell, delivered as the jshell command-line tool bundled with the JDK starting in Java 9, closes this gap by providing a true REPL for the Java language: it reads one snippet (an expression, statement, or declaration) at a time, evaluates it immediately using the same compiler and runtime as ordinary Java, prints the result (including its inferred type, for expressions), and loops back for the next snippet — all without the user ever writing a class declaration or a main method, both of which JShell supplies and manages implicitly behind the scenes. This made Java meaningfully more approachable for teaching and significantly faster for the kind of small-scale, throwaway experimentation that previously required spinning up an entire scratch project, IDE, or build configuration just to test one idea.
Java
// ── Before Java 9 — testing one expression required a full class+build cycle
// Scratch.java
public class Scratch {
    public static void main(String[] args) {
        System.out.println(String.format("%.2f", 3.14159));
    }
}
// $ javac Scratch.java
// $ java Scratch
// 3.14
// Three steps (write file, compile, run) just to check one format string

// ── Java 9+ — JShell evaluates the same expression immediately ─────────
// $ jshell
// jshell> String.format("%.2f", 3.14159)
// $1 ==> "3.14"
//
// No class wrapper, no main method, no separate compile step — the result
// (and JShell's auto-assigned scratch variable $1) appears instantly.

Core REPL Behavior — Implicit Variables, Forward References, and Statement Evaluation

When an expression is entered into JShell without being assigned to a named variable, JShell automatically creates a scratch variable (named $1, $2, $3, and so on, incrementing with each unnamed expression) holding the result, and prints both the inferred type and the value — this lets a user refer back to a previous unnamed result in a later snippet, which is otherwise impossible in ordinary Java where every expression's result must be explicitly captured or discarded immediately. Declaring a named variable, method, or class behaves the same as it would in an ordinary Java program, except that JShell allows redeclaring the same name with a new definition in a later snippet, which simply replaces the earlier definition — useful for iteratively refining a method's implementation without restarting the session. A distinctive feature of JShell relative to ordinary Java source files is its tolerance for forward references: a method body can be entered that calls another method which has not been defined yet, and JShell accepts this immediately, only raising an actual error if that as-yet-undefined method is invoked before it's eventually declared. This is possible because JShell doesn't require the entire program to be assembled and resolved as one compilation unit the way a normal source file does — each snippet is independently compiled and linked into the running session's evolving symbol table, and JShell defers full resolution until execution actually reaches the point where it's needed. Bare statements (not just expressions and declarations) are also accepted directly — a for loop, an if block, or a plain method call with no return value can be entered and executed immediately, exactly as if it were a line inside an existing main method, again without requiring the user to ever write that main method explicitly.
Java
// jshell> 2 + 2
// $1 ==> 4

// jshell> $1 * 10                      // referring back to a previous scratch result
// $2 ==> 40

// jshell> int x = 10
// x ==> 10

// jshell> int x = 20                   // redeclaring "x" simply replaces the earlier one
// x ==> 20

// ── Forward references — calling a not-yet-defined method is accepted ──
// jshell> int square(int n) { return doubleIt(n) * n / 2; }   // doubleIt() doesn't exist yet
// |  created method square(int), however, it cannot be invoked until method
// |  doubleIt(int) is declared

// jshell> int doubleIt(int n) { return n * 2; }   // now define it
// |  created method doubleIt(int)

// jshell> square(5)                     // NOW it resolves and runs correctly
// $3 ==> 25

// ── Bare statements run immediately, no main() wrapper needed ──────────
// jshell> for (int i = 0; i < 3; i++) { System.out.println("Hi " + i); }
// Hi 0
// Hi 1
// Hi 2

// jshell> List<String> names = List.of("Ann", "Bo", "Cy")
// names ==> [Ann, Bo, Cy]

// jshell> names.forEach(System.out::println)
// Ann
// Bo
// Cy

Snippet Management Commands and Loading External Code

JShell distinguishes its own meta-commands, all prefixed with a leading /, from ordinary Java snippets. /vars, /methods, and /types list every variable, method, and class currently defined in the session along with their inferred types or signatures, which is useful for recovering context in a long exploratory session. /list shows every snippet entered so far in order, including ones that failed, with an ID that can be used to reference them. /edit opens a snippet (or the whole session) in an external editor for revision, and /save and /open respectively save the current session's snippets to a file and load previously-saved snippets back in, allowing an exploratory session to be persisted and resumed later, or turned into the seed for an actual source file. JShell can also load external code into a running session in two distinct ways. /open path/to/File.java reads a file containing JShell-style snippets (or even ordinary Java statements) and executes them as if they had been typed at the prompt, useful for replaying a sequence of setup statements at the start of a session. Separately, the -cp (or --class-path) command-line option when launching jshell, or the /env -class-path command within a running session, makes externally compiled classes (e.g. ordinary JARs from a Maven/Gradle build) available for use in JShell snippets via normal import statements, which is what allows JShell to be used as an exploration tool against an actual project's own compiled classes and dependencies rather than only the JDK's own classes.
Java
// ── Listing what's currently defined in the session ─────────────────────
// jshell> /vars
// |    int x = 20
// |    List<String> names = [Ann, Bo, Cy]

// jshell> /methods
// |    int square(int)
// |    int doubleIt(int)

// jshell> /list
// 1 : int x = 10;
// 2 : int x = 20;
// 3 : int square(int n) { return doubleIt(n) * n / 2; }
// 4 : int doubleIt(int n) { return n * 2; }

// ── Saving and reloading a session ──────────────────────────────────────
// jshell> /save my-session.jsh
// jshell> /exit
// $ jshell
// jshell> /open my-session.jsh        // replays all saved snippets in order

// ── Loading a plain file of statements into the running session ────────
// setup.jsh:
//   List<String> users = new ArrayList<>();
//   users.add("alice");
//   users.add("bob");
// jshell> /open setup.jsh
// jshell> users
// users ==> [alice, bob]

// ── Using external/project classes from the classpath ───────────────────
// $ jshell -cp target/classes:libs/some-library.jar
// jshell> import com.myapp.UserService
// jshell> UserService svc = new UserService()
// jshell> svc.findUser("alice")

Related Topics in Java 9 to 21 Features

Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
Collection Factory Methods
Collection factory methods — List.of(...), Set.of(...), and Map.of(...)/Map.ofEntries(...) — introduced in Java 9, provide a concise, standard way to create small, fixed-content, genuinely immutable collections in a single expression, replacing the previously common but verbose combination of Arrays.asList(...), Collections.unmodifiableList(...), and manual add() calls, none of which produced a collection that was both concise to write and fully immutable. This entry covers exactly what problem these factories solve compared to the older idioms, the specific immutability and null-rejection guarantees they provide (which are stricter than Collections.unmodifiableXxx), their behavior around duplicate keys/elements, and important caveats around mutability expectations and performance for very small collections.
Private Interface Methods
Private interface methods, introduced in Java 9, allow an interface to declare methods marked private — with a full method body — that are accessible only from within that same interface (from its default methods, its other private methods, and its private static methods), but completely invisible to implementing classes and to any code outside the interface. They close a code-duplication gap left after Java 8's default methods: when an interface has multiple default methods that share common logic, that shared logic previously either had to be duplicated in each default method or exposed as a public default method purely to enable reuse, polluting the interface's actual API surface with a method meant only for internal reuse. This entry covers the specific problem they solve, the distinction between private instance and private static interface methods, the rules governing what they can call and be called from, and how they interact with the broader default-method design Java established in version 8.
var Keyword
The var keyword, introduced in Java 10 as local variable type inference, lets a local variable declaration omit its explicit type and have the compiler infer it from the initializer expression on the right-hand side, while the variable remains just as statically typed at compile time as if the type had been written out explicitly — var is purely a source-code convenience, not a shift toward dynamic typing. This is a narrower, more conservative feature than type inference in languages like C# (var) or Kotlin (val/var): it applies only to local variables with an initializer, never to fields, method parameters, or return types, and the compiler still performs full static type checking using the inferred type. This entry covers the motivation and explicit scope restrictions, how type inference actually resolves (including with generics, diamond operator interaction, and anonymous classes), the readability tradeoffs and style guidance the OpenJDK team itself published, and the specific cases where var cannot be used.