Preview Features and How to Enable Them
Know what a preview feature is, which ones Java 25 ships, and how to switch them on in javac, java, JShell, Maven and Gradle without surprises in production.
javac --release 25 --enable-preview -d out Main.java
java --enable-preview -cp out Main
java --enable-preview Main.javaA preview feature is a language or API feature that is fully designed and implemented but not yet permanent. It ships disabled, so feedback can shape it before it is final. It may change between releases, and in rare cases it is withdrawn. Many recent features went this way: scoped values were in preview from Java 21 to 24 before becoming final in 25.
Preview features in Java 25
- Structured concurrency, fifth preview (JEP 505)
- Primitive types in patterns,
instanceofandswitch, third preview (JEP 507) - Stable values (JEP 502)
- PEM encodings of cryptographic objects (JEP 470)
The Vector API is different: it is an incubator module, jdk.incubator.vector, added with --add-modules rather than --enable-preview.
Switching it on
javac --release 25 --enable-preview -d out Main.java
java --enable-preview -cp out Main
java --enable-preview Main.java # source launcher
jshell --enable-preview
javac refuses --enable-preview on its own; it must be paired with --release or -source for the current release, because preview features are tied to one exact version. The compiler marks the class file with minor version 65535, and the JVM will only load it when --enable-preview is also given at run time. Without it you get UnsupportedClassVersionError: Preview features are not enabled.
In Maven, add <compilerArgs><arg>--enable-preview</arg></compilerArgs> to the compiler plugin and <argLine>--enable-preview</argLine> to Surefire. In Gradle with the Kotlin DSL, add the flag to options.compilerArgs for JavaCompile tasks and to jvmArgs for Test and JavaExec tasks. The run command in your container image needs it too.
Should a service use preview features?
Usually not in production code. A class compiled with preview features on 25 will not load on 26, so every JDK upgrade forces a rebuild, and the API may have changed in the meantime. Structured concurrency, for example, changed its API between previews. Good places to try previews are spikes, internal tools and a branch that tracks the feature until it is final.
Mistakes people make
- Enabling preview for the compiler but not for tests or the production
javacommand. - Ignoring the compiler note. Add
-Xlint:previewto see exactly which lines use preview features. - Confusing preview features with experimental JVM options, which use
-XX:+UnlockExperimentalVMOptionsand are a separate mechanism.
Example
When to use it
- Trying structured concurrency on a branch of an aggregation service while main stays on final APIs only.
- Adding a CI check that fails the release if any class file in the JAR carries the preview minor version.
- Configuring Maven Surefire and the container start command consistently so preview code compiles, tests and runs.
More examples
Running a source file with and without the flag
The source launcher accepts <code>--enable-preview</code> on its own because it always compiles for the running release. Without the flag the file does not even compile.
void main() throws Exception {
Path dir = Files.createTempDirectory("preview-run");
Files.writeString(dir.resolve("Check.java"), """
void main() {
Object retries = 3;
if (retries instanceof int n && n > 0) IO.println("retrying " + n + " times");
}
""");
String java = Path.of(System.getProperty("java.home"), "bin", "java").toString();
for (List<String> args : List.of(List.of("Check.java"), List.of("--enable-preview", "Check.java"))) {
List<String> cmd = new ArrayList<>(List.of(java));
cmd.addAll(args);
Process p = new ProcessBuilder(cmd).directory(dir.toFile()).redirectErrorStream(true).start();
String firstLine = new String(p.getInputStream().readAllBytes()).lines().findFirst().orElse("");
IO.println("java " + String.join(" ", args) + " -> exit " + p.waitFor());
IO.println(" " + firstLine);
}
}
// Output:
// java Check.java -> exit 1
// Check.java:3: error: primitive patterns are a preview feature and are disabled by default.
// java --enable-preview Check.java -> exit 0
// retrying 3 timesPreview class files need the flag at run time too
Compiling with the flag is only half the job: the class file version 69.65535 tells the JVM to refuse it unless <code>--enable-preview</code> is also on the <code>java</code> command, which is easy to forget in a Dockerfile or a test plugin.
void main() throws Exception {
Path dir = Files.createTempDirectory("preview-class");
Files.writeString(dir.resolve("Job.java"), """
public class Job {
public static void main(String[] args) {
Object code = 7;
if (code instanceof int c) System.out.println("exit code " + c);
}
}
""");
String bin = Path.of(System.getProperty("java.home"), "bin").toString();
List<List<String>> steps = List.of(
List.of("javac", "--release", "25", "--enable-preview", "-d", "out", "Job.java"),
List.of("java", "-cp", "out", "Job"),
List.of("java", "--enable-preview", "-cp", "out", "Job"));
for (List<String> step : steps) {
List<String> cmd = new ArrayList<>(step);
cmd.set(0, Path.of(bin, step.get(0)).toString());
Process p = new ProcessBuilder(cmd).directory(dir.toFile()).redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes());
String last = out.lines().reduce((a, b) -> b).orElse("").strip();
IO.println(step.get(0) + " " + String.join(" ", step.subList(1, step.size())) + " -> exit " + p.waitFor());
if (!last.isEmpty()) IO.println(" " + last);
}
}
// Output:
// javac --release 25 --enable-preview -d out Job.java -> exit 0
// Note: Recompile with -Xlint:preview for details.
// java -cp out Job -> exit 1
// java.lang.UnsupportedClassVersionError: Preview features are not enabled for Job (class file version 69.65535). Try running with '--enable-preview'
// java --enable-preview -cp out Job -> exit 0
// exit code 7Spotting preview class files in a build
Bytes 4 to 7 of a class file hold the minor and major version. A minor version of 65535 marks a preview class, which is a quick check to add to a release pipeline that must never ship preview code.
import javax.tools.*;
import java.net.URI;
int[] compileAndReadVersion(String code, List<String> options) throws IOException {
Path out = Files.createTempDirectory("ver");
var file = new SimpleJavaFileObject(URI.create("string:///Probe.java"), JavaFileObject.Kind.SOURCE) {
@Override public CharSequence getCharContent(boolean ignore) { return code; }
};
var opts = new ArrayList<>(options);
opts.addAll(List.of("-d", out.toString()));
ToolProvider.getSystemJavaCompiler().getTask(null, null, d -> {}, opts, null, List.of(file)).call();
byte[] b = Files.readAllBytes(out.resolve("Probe.class"));
int minor = ((b[4] & 0xff) << 8) | (b[5] & 0xff);
int major = ((b[6] & 0xff) << 8) | (b[7] & 0xff);
return new int[] {major, minor};
}
void main() throws IOException {
int[] plain = compileAndReadVersion("class Probe { int x = 1; }", List.of());
int[] preview = compileAndReadVersion(
"class Probe { boolean b(Object o) { return o instanceof int; } }",
List.of("--release", "25", "--enable-preview"));
IO.println("plain " + plain[0] + "." + plain[1]);
IO.println("preview " + preview[0] + "." + preview[1] + (preview[1] == 0xFFFF ? " (needs --enable-preview)" : ""));
}
// Output:
// plain 69.0
// preview 69.65535 (needs --enable-preview)
Discussion