News and views from members of the Java team at Oracle
The OpenJDK Quality Group is promoting the testing of FOSS projects with OpenJDK builds as a way to improve the overall quality of the release. This heads-up is part of a Quality Outreach update sent to the projects involved. To learn more about the program, and how-to join, please check here.
JEP 401 introduces value classes as a preview feature in JDK 28, an addition that has uncommon consequences for compatibility and can benefit from additional testing. This article begins with a high-level overview of two test regiments before explaining the JEP's compatibility implications in more detail.
Since the new capabilities are introduced as a preview, the JDK's behavior with preview features disabled (which is the default) should remain as-is. While unlikely, it is nonetheless possible that, due to the depth of changes, the behavior with disabled previews was unintentionally impacted. Projects testing against JDK 28 should keep an eye open for unexpected behavior.
With preview features enabled, 30 JDK classes become value classes, most notably the primitive wrapper classes like Integer and Double. This has the uncommon compatibility implication that existing code will start using the new feature despite being unchanged, which can lead to a wider range of behavioral changes. They fit into three categories:
expected behavioral differences between "regular" classes and value classes - projects may have to change code to adapt to these changes
expected behavior change of record constructors - projects may have to change code to adapt to these changes
unexpected misbehavior of the JDK - this should be reported to the valhalla-dev mailing list
We hence propose that projects with a sufficiently flexible continuous integration setup build and test their code bases against JDK 28 with disabled and enabled preview features.
The following behavior changes apply if code runs on JDK 28 with enabled preview features. This is true even if the code was compiled with disabled preview features or on a version prior to 28.
The following 30 JDK classes are value classes:
in package java.lang:
Integer, Long, Float, Double, Byte, Short, Character, Boolean
Number, Record
in package java.util:
Optional, OptionalInt, OptionalLong, OptionalDouble
in package java.time:
LocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetTime, OffsetDateTime
Duration, Instant, Period, Year, YearMonth, MonthDay
in package java.time.chrono:
MinguoDate, HijrahDate, JapaneseDate, ThaiBuddhistDate
Value classes lack identity, a property that can be used to tell two otherwise indistinguishable instances apart:
var x = LocalDate.of(2026, 7, 31);
var y = LocalDate.of(2026, 7, 31);
var identical = x == y;
// the result depends on value classes:
// * without value classes, `identical` is `false`
// * with value classes, `identical` is `true`
This impacts identity-sensitive operations:
== compares value objects by their field values, so for instances of JDK value classes, == may return true in situations where it didn't before (like in the example above)
value objects have no object monitor, so they can't be synchronized on and methods like wait and notify are unsupported - any monitor interaction will lead to compile error or an IdentityException
the java.lang.ref APIs and java.util.WeakHashMap cannot be used with value objects and will throw an IdentityException
value objects are truly immutable and calling setAccessible on their fields will result in an IllegalAccessException, even if final field mutation is enabled
There are more restrictions, but they only apply to custom value classes, which are beyond the scope of this article.
From a compatibility perspective, it's relevant to note that JDK 8 introduced the concept of value-based classes that outlined the restrictions of present-day value classes and encompassed the Optional classes as well as those in the java.time.* packages. Since then, the description has been refined, for example by explicitly mentioning synchronization, and JDK 16 expanded it to include the primitive wrapper classes.
When preview features are enabled during compilation with JDK 28, pre-existing record constructors need to follow additional rules, which can lead to new compile errors. Non-compliant constructors that were compiled without preview features but executed with preview features, will execute as compiled.
Specifically, record constructor code will by default run during early construction, a phase that, among other restrictions, limits interaction with this to field assignments. (Details and motivation for early construction can be found in JEP 513, which finalized flexible contructor bodies.) Consequently, compiling with enabled preview features will lead to errors in record constructors where statements interact with this (e.g. by calling an instance method).
Existing compact constructors should contain no such statements as they expose the instance under construction with all fields set to null, 0, etc. to other code. If they do (and the statements can't simply be removed), the constructor must be refactored to its non-compact variant. If non-compact constructors contain such statements:
remove the offending statement, or
refactor it, so it works in the early construction context (very similar to a static context), or
assign all fields, then call super() or this(...) (which ends early construction), then execute the formerly-offending statements (they no longer expose the instance under construction)
Build and test your project against JDK 28 and, if possible, also against JDK 28 with preview features enabled. Feedback is welcome through the valhalla-dev mailing list (registration required). For much more details on this proposed change as well as the compatibility implications described above, check JEP 401.