JEP draft: Preview Features: A Look Back, and A Look Ahead

OwnerAlex Buckley
Discussionjdk dash dev at openjdk dot java dot net
Relates toJEP 12: Preview Features
Created2023/01/18 21:56
Updated2023/11/18 15:13

It's been over four years since we introduced the idea of preview features in JEP 12, so it's a good time to review how they've worked out, and see if anything needs tweaking as we move forward.

Preview language features

In the Java language, the preview process has been effective at its primary job: ensuring that features are "done right" before they become final and permanent parts of the language. How? By providing a suitable window of time in which features can be improved based on user feedback. Switch expressions, text blocks, records, sealed classes, and pattern matching for instanceof were all successful in preview, and became much appreciated permanent features of the language.

Preview language features have also been effective at helping the community to adopt new features. We particularly wanted IDEs to support preview language features so that as many developers as possible could easily try them out, and IDE vendors have delivered strongly.

Preview language features have worked hand-in-glove with the six month cadence of JDK releases. A decade ago, a large project to evolve the Java language could not ship until all its features were worked out and the overall programming model was brought to fruition. Recall how Project Lambda introduced lambda expressions + method references + local type inference + default methods + streams -- even with groundwork laid in JDK 7, it took three years of heads-down effort to forge JDK 8. Nowadays, a project like Amber can ship a cascade of preview features in successive JDK releases (16, 17, 18, 19...) and eventually there's a release which previews the totality of the new programming model. For example, pattern matching started in JDK 14 with type patterns in instanceof, and by JDK 19 it was a recognizable programming style that encompassed instanceof, switch, records, and sealing.

One of our guiding principles is that we only include a preview feature in the JDK if it's truly on a journey to final-and-permanent status within a reasonable time frame. If it looks like the feature's journey will get derailed, either in quality or in time, then we must be willing to drop it after it has previewed and before it becomes permanent. Thankfully, we have not yet had to do that.

(Some people mistakenly believe that "Raw String Literals" was dropped after previewing in JDK 12, but in fact it never shipped in JDK 12; we changed direction and previewed "Text Blocks" in JDK 13 instead.)

While small language features have successfully previewed for a short time before becoming permanent, we've learned that large language features might need to preview more than twice, especially if they interact with other language features which are themselves in preview. For example, pattern matching for switch has previewed twice on its own merits, then twice more because of interactions with another, newer, preview feature (record patterns). Previewing three or more times reflects the ambition that all language features work together smoothly, and is incomparably better than trying to "hold the train" until all connected features are equally ready to preview.

Distinct from improvements to the HotSpot JVM implementation (e.g., for garbage collection), JEP 12 conceived of previewing changes to the layout and linking of class files and to the instruction set of the abstract Java virtual machine. Perhaps surprisingly, there have been no preview VM features as yet, except for Sealed Classes (notionally a preview language feature, but one that requires support from the Java virtual machine). We can expect preview VM features from Project Valhalla in the next few years (e.g., primitive classes).

Preview APIs

From the beginning, we realized that preview language features would often be co-developed with API points, principally to support reflection. For example, record classes previewed alongside Class::getRecordComponents, and sealed classes previewed alongside Class::getPermittedSubclasses.

We then realized that previewing APIs on their own merits would be valuable. The first "standalone" preview APIs appeared in JDK 19: the Virtual Threads API and the Foreign Function & Memory (FFM) API. Sharing the preview terminology and mechanisms (e.g., --enable-preview) across APIs and language features has worked well.

Still, APIs and language features are different in an important way. A language feature has a small surface area but a broad conceptual area: think how the -> lambda syntax is the on-ramp to the world of functional programming. Meanwhile, an API has a large surface area that more or less defines the conceptual area: "the map is the territory". It's common for a language feature to change corner-case behavior in a way that doesn't affect syntax and is little noticed by programmers, but changing almost any aspect of an API is immediately and widely visible. It impacts both the programmers who invoke the API direct from applications, and the programmers who build libraries on top of the API.

We were upfront in JEP 12 that previewing an API is likely to involve more visible churn than previewing a language feature: "a semantically stable preview API may still undergo considerable syntactic polishing (e.g., renaming classes, adding helper methods) on the way to achieving final status."

As an example, consider the FFM API. It aims to solve a difficult problem -- providing safe access to memory allocated and deallocated by other parties -- so its development effort has explored various API shapes for scoping/guarding access to memory. Its core concepts are stable (segments, allocators, linkers, etc) but their expression in specific API points has evolved in response to user feedback. In turn, libraries built on top of the FFM API during its preview period have needed to adapt to its changing API points.

New thinking for Preview APIs

With more preview APIs on the horizon, some with substantial surface area (e.g., the Classfile API), we want to set the expectation that the higher degree of syntactic drift seen in preview APIs does not undermine their "almost ready" nature.

Accordingly, we'll regard an API as ready to preview when it has achieved a high degree of completeness and stability. We'll continue to regard a language or VM feature as ready to preview only when it has achieved an extremely high degree of both completeness and stability.

We'll preserve the guideline that, in general, a preview feature should be 100% complete within 12 months. However, this guideline -- which implies a preview feature will appear in one or two JDK releases before going final -- is likely too ambitious for APIs that capture complex concepts, have exceptionally large surface areas, or engage deeply with the VM. We should anticipate additional rounds of feedback and revision for such APIs, as they will underpin the Java ecosystem for decades to come.

(We considered doubling down on incubating APIs as the model for APIs which are conceptually complete but subject to syntactic drift. However, incubating APIs are non-standard and delivered in standalone "incubator modules". Being non-standard means that standard java.* packages cannot refer to them in class/member signatures, while being delivered outside java.base makes implementing a low-level API such as Virtual Threads extremely difficult. In addition, the conceptual and practical differences between incubation and preview are lost on most developers. Offering high-quality, core-stable APIs under the familiar "preview" label will provoke far more feedback than shoehorning them into incubator modules which attract little attention. We will, however, continue to use incubation for APIs whose level of completeness and stability is considerably lower than that of preview APIs.)