skip to content
everything.modern.previewFeatures

preview and incubator features

--enable-preview, and how features graduate.

chapter
chapter 08.02
reading time
6 min read

a preview feature is finished but not permanent: fully specified and implemented, shipped so people can try it, and allowed to change or disappear. you opt in twice, with --enable-preview on javac and on java.

an incubator is an earlier, rougher API in a jdk.incubator.* module. an experimental feature is a JVM option behind -XX:+UnlockExperimentalVMOptions. all three are how big things reach java without being locked in forever on day one.

the picture

you know how a java feature, once it's in, is in forever? Vector, Date, Thread.stop and the rest of the museum are proof. java is serious about backward compatibility, which makes every design mistake permanent. previews are the escape hatch: ship it for real, let millions of people kick the tires, then decide.

think of a restaurant trying a new dish as a "special" before it goes on the printed menu. it's a real dish, cooked properly, you can order it. but next month it might come back with a different sauce, or not at all. you have to ask for it by name (that's --enable-preview).

try =
source
int n = 300; if (n instanceof byte b) ... // primitive pattern: preview in 25
javac
$ javac --release 25 --enable-preview Main.java
Note: Main.java uses preview features of Java SE 25.
Note: Recompile with -Xlint:preview for details.
class file
ca fe ba be ff ff 00 45 …version 69.65535 (preview)
java
$ java Main
java.lang.UnsupportedClassVersionError: Preview features are not enabled
  for Main (class file version 69.65535). Try running with '--enable-preview'

javac stamped the class file with minor version 0xFFFF, which means 'depends on preview features'. the JVM checks that stamp before running a single line.

pick a scenario. the 0xFFFF in the class file header is javac's stamp that says "this class needs preview features of exactly this release". every command and message is real output from jdk 25 (and jdk 21 for the old class).

the rules of the game

the process is written down in JEP 12. the parts worth knowing:

what can preview
language features (instanceof byte b), JVM features, and java.* APIs (StableValue in 25). an API preview fails to compile without the flag too: error: StableValue is a preview API and is disabled by default.
how long
the target is final within about a year: preview in release N and N+1, final in N+2. big ones take longer. structured concurrency has been previewing since 21.
what happens after
it goes final (maybe with changes), previews again (that needs a new JEP), or gets removed. string templates were removed.
the stamp
a class file that uses preview features gets minor version 65535 (all 16 bits set): 69.65535 for java 25. it runs only on that exact release, and only with --enable-preview.
the noise
with the flag on, javac reminds you on every compile (Note: Main.java uses preview features of Java SE 25.). that's on purpose.
terminal
$ javac --release 25 --enable-preview Main.java
Note: Main.java uses preview features of Java SE 25.
Note: Recompile with -Xlint:preview for details.
$ javap -v Main.class | grep version
  minor version: 65535
  major version: 69
$ java --enable-preview Main
300 doesn't fit in a byte
 
$ java --enable-preview --source 25 Main.java     # the source launcher, both in one go

maven and gradle need the flag in two places too: the compiler plugin (<compilerArgs>--enable-preview</compilerArgs>) and whatever runs the code, tests included (surefire's argLine, gradle's jvmArgs). forgetting the second one is the classic "it compiles but every test fails" afternoon.

quick check
you compiled with --enable-preview on jdk 25. your server runs jdk 27, with --enable-preview. what happens?

incubator and experimental

previews are for things that are almost done. there are two earlier stages:

doincubator: a whole module, opt in with --add-modules
terminal
$ java --add-modules jdk.incubator.vector Main.java
WARNING: Using incubator modules: jdk.incubator.vector

the API lives in jdk.incubator.vector, not java.*, so the package name itself tells you it's temporary. when it graduates, it moves and your imports change.

doexperimental: a JVM option, opt in with an unlock flag
terminal
$ java -XX:+UseEpsilonGC -version
Error: VM option 'UseEpsilonGC' is experimental and must be enabled via -XX:+UnlockExperimentalVMOptions.

this is how runtime features start: a new GC mode, a new header layout. no API, just a switch.

stageforopt in withcan change?example
incubator (JEP 11)APIs, early--add-modules jdk.incubator.xcompletelyvector API, since 16
experimentalJVM features-XX:+UnlockExperimentalVMOptionsyescompact object headers in 24
preview (JEP 12)language, VM, APIs, nearly done--enable-previewyes, or be removedprimitive patterns in 27
finaleverythingnothingnorecords, since 16

see every feature's path through these stages in the release timeline, and what's in preview right now in what's in preview.

keylines
  • preview = finished but not permanent. it can change, re-preview, or be removed.
  • --enable-preview on javac and on java. the source launcher also needs --source N.
  • preview class files are stamped N.65535 and only run on that exact release.
  • incubator = jdk.incubator.* module + --add-modules. experimental = -XX:+UnlockExperimentalVMOptions.
  • great for trying things. a liability in production.