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:
| Tool | JEP | Release | Purpose |
|---|---|---|---|
| JShell | JEP 222 | Java 9 | Interactive REPL for Java code |
| jlink | JEP 282 | Java 9 | Build minimal custom JRE images |
| Single-file programs | JEP 330 | Java 11 | Run .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 — Custom JRE Images
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.
How jlink Works
- You specify which modules your application needs.
- jlink copies those modules (and their transitive dependencies) from
$JAVA_HOME/jmods. - 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
.classfile is created. - The first class in the file (not necessarily matching the filename) must have
main. - Cannot import other
.javafiles — all code must be in the single file. - Standard classpath applies:
--class-pathand--module-pathflags work. - Works with
--enable-previewfor 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.