Part 10 of 16

Tooling: JShell, jlink, and Single-File Programs (JEP 222, 282, 330)

Overview

Java 9 and 11 added three tools that change how you write, test, and deploy Java code:

ToolJEPReleasePurpose
JShellJEP 222Java 9Interactive REPL for Java code
jlinkJEP 282Java 9Build minimal custom JRE images
Single-file programsJEP 330Java 11Run .java files directly

JShell — Interactive Java REPL

JShell is Java’s Read-Eval-Print Loop: a command-line tool for evaluating Java expressions, statements, methods, and classes interactively, without creating a project.

Starting JShell

jshell
# |  Welcome to JShell -- Version 11.0.22
# |  For an introduction type: /help intro
# jshell>

Basic expressions

jshell> 2 + 2
$1 ==> 4

jshell> "Hello".toUpperCase()
$2 ==> "HELLO"

jshell> var list = List.of(1, 2, 3, 4, 5)
list ==> [1, 2, 3, 4, 5]

jshell> list.stream().filter(n -> n % 2 == 0).collect(Collectors.toList())
$4 ==> [2, 4]

JShell automatically imports common packages (java.util.*, java.io.*, java.math.*, java.net.*).

Defining methods

jshell> int factorial(int n) {
   ...>     return n <= 1 ? 1 : n * factorial(n - 1);
   ...> }
|  created method factorial(int)

jshell> factorial(10)
$1 ==> 3628800

Defining classes

jshell> class Point {
   ...>     final double x, y;
   ...>     Point(double x, double y) { this.x = x; this.y = y; }
   ...>     double distanceTo(Point other) {
   ...>         return Math.hypot(x - other.x, y - other.y);
   ...>     }
   ...>     public String toString() { return "(" + x + ", " + y + ")"; }
   ...> }
|  created class Point

jshell> var p1 = new Point(0, 0)
p1 ==> (0.0, 0.0)

jshell> var p2 = new Point(3, 4)
p2 ==> (3.0, 4.0)

jshell> p1.distanceTo(p2)
$3 ==> 5.0

JShell commands

Commands start with /:

/list         — list all active snippets
/vars         — list defined variables
/methods      — list defined methods
/types        — list defined classes
/imports      — list active imports
/history      — show command history
/edit n       — open snippet n in an editor
/drop n       — remove snippet n
/reset        — clear all snippets and restart
/exit         — quit JShell
/help         — full help
/help intro   — beginner introduction
jshell> /vars
|    List<Integer> list = [1, 2, 3, 4, 5]
|    int $4 = [2, 4]

jshell> /methods
|    int factorial(int)

Loading a startup file

# Execute a script on startup
jshell --startup mysetup.jsh

# mysetup.jsh — pre-import common dependencies
import com.fasterxml.jackson.databind.ObjectMapper;
var mapper = new ObjectMapper();
# Use the JShell default startup (common imports)
jshell --startup DEFAULT

Adding classpath entries

# Add a JAR to the session classpath
jshell --class-path target/myapp.jar:libs/jackson-databind.jar

# Or within a session
jshell> /env --class-path target/myapp.jar

Practical use cases

# Test a regular expression interactively
jshell> "2024-05-04".matches("\\d{4}-\\d{2}-\\d{2}")
$1 ==> true

# Prototype a stream pipeline
jshell> IntStream.range(1, 11).filter(n -> n % 3 == 0).sum()
$2 ==> 18

# Quickly test a JSON structure before writing tests
jshell> var json = """{"name":"Alice","age":30}"""
jshell> new ObjectMapper().readTree(json).get("name").asText()
$3 ==> "Alice"

jlink creates a self-contained, minimal Java runtime image tailored to your application’s actual module dependencies. Instead of shipping a 200+ MB JDK, you ship a 30–60 MB custom JRE.

  1. You specify which modules your application needs.
  2. jlink copies those modules (and their transitive dependencies) from $JAVA_HOME/jmods.
  3. It produces a directory containing a complete, self-contained JRE — no separate JDK installation required.

Step 1: Discover required modules

# For a modular application
jdeps --ignore-missing-deps \
      --module-path libs/ \
      --print-module-deps \
      myapp.jar
# Output: java.base,java.logging,java.net.http,java.sql

# For a classpath application (non-modular)
jdeps --ignore-missing-deps \
      --class-path libs/*.jar \
      --multi-release 11 \
      --print-module-deps \
      myapp.jar

Step 2: Build the custom image

jlink \
  --module-path $JAVA_HOME/jmods \
  --add-modules java.base,java.logging,java.net.http,java.sql \
  --output dist/custom-jre \
  --compress=2 \          # compress class files
  --no-header-files \     # remove include/ headers
  --no-man-pages \        # remove man pages
  --strip-debug           # remove debug symbols

Step 3: Run with the custom JRE

dist/custom-jre/bin/java -jar myapp.jar
# or for modular apps:
dist/custom-jre/bin/java --module com.example.app/com.example.app.Main

Size comparison

Full JDK 11 (Temurin):       ~210 MB
JRE 11 (Temurin):             ~85 MB
jlink (java.base only):       ~40 MB
jlink (base + net.http + sql): ~55 MB
jlink with --compress=2:      ~35 MB

Dockerfile integration

# Stage 1: Build the custom JRE
FROM eclipse-temurin:11-jdk AS jlink-stage
RUN $JAVA_HOME/bin/jlink \
    --module-path $JAVA_HOME/jmods \
    --add-modules java.base,java.logging,java.net.http,java.sql \
    --output /custom-jre \
    --compress=2 \
    --no-header-files \
    --no-man-pages \
    --strip-debug

# Stage 2: Runtime — no JDK needed
FROM debian:bookworm-slim
COPY --from=jlink-stage /custom-jre /opt/jre
COPY target/myapp.jar /app/myapp.jar
ENV PATH="/opt/jre/bin:${PATH}"
ENTRYPOINT ["java", "-jar", "/app/myapp.jar"]

This produces a final image ~200 MB smaller than a JDK-based image.

Including the application as a module

If your application is fully modularised, include it directly in the image:

jlink \
  --module-path $JAVA_HOME/jmods:target/myapp.jar:libs/ \
  --add-modules com.example.app \
  --output dist/app-image \
  --launcher myapp=com.example.app/com.example.app.Main

# Run with the generated launcher script
dist/app-image/bin/myapp

Single-File Source-Code Programs (JEP 330, Java 11)

Java 11 allows running a single .java file directly, skipping the explicit javac step:

# Before Java 11
javac HelloWorld.java
java HelloWorld

# Java 11+
java HelloWorld.java

The JVM compiles the file in memory and executes it without creating a .class file on disk.

Example

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello from " + System.getProperty("java.version"));
    }
}
java HelloWorld.java
# Hello from 11.0.22

Source launcher details

  • The file is compiled in memory; no .class file is created.
  • The first class in the file (not necessarily matching the filename) must have main.
  • Cannot import other .java files — all code must be in the single file.
  • Standard classpath applies: --class-path and --module-path flags work.
  • Works with --enable-preview for preview features.

Passing JVM flags and arguments

# JVM flags before the filename, program args after
java -Xmx512m --enable-preview HelloWorld.java arg1 arg2

Shebang support (Unix/macOS)

On Unix systems, you can make a .java file directly executable using a shebang. The trick is that the shebang line is not valid Java, so you must use a filename without the .java extension:

#!/usr/bin/env -S java --source 11
// Note: file must NOT have a .java extension for shebang to work
// Save as: greet (no extension)

public class Greet {
    public static void main(String[] args) {
        var name = args.length > 0 ? args[0] : "World";
        System.out.println("Hello, " + name + "!");
    }
}
chmod +x greet
./greet Alice
# Hello, Alice!

-S in env splits the arguments, enabling java --source 11 as the interpreter command.

Practical use cases

# Quick data processing script
java ProcessCsv.java sales-2024.csv > summary.txt

# One-off database migration
java RunMigration.java --db-url jdbc:postgresql://localhost/prod

# Generate a file from a template
java GenerateConfig.java prod > application.properties

What single-file programs are NOT for

  • Multi-class projects (use Maven/Gradle)
  • Production services (use a proper build)
  • Performance-critical startup (compilation overhead on each invocation)

They are ideal for: developer scripts, learning exercises, simple automation, and prototyping before moving to a full project.


What’s Next

Next: Garbage Collection: G1GC, ZGC, Epsilon, and AppCDS