Skip to content

Support for JEP 401 value classes - #318

Draft
hg-ms wants to merge 3 commits into
mainfrom
jep-401-value-type-support
Draft

hg-ms wants to merge 3 commits into
mainfrom
jep-401-value-type-support

Conversation

@hg-ms

@hg-ms hg-ms commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

A value class has no identity, and its instances cannot be created empty and populated
afterwards - which is what the reflective path does. This adds a persistence path of their
own, from the type handler down to the binary layout.

What it adds

  • BinaryHandlerGenericValueClass / BinaryLegacyTypeHandlerValueClass: the instance is
    constructed from its persisted state instead of populated. A field the JVM lays out inside
    its owner is reached through a handle rather than by its memory offset.
  • ValueClassConstructorContract: verifies that the constructor stores what it is given, at
    handler creation or against the first instance stored. Two same-typed fields taken in the
    wrong order, or a field computed from its argument, are refused rather than silently
    corrupting every round trip.
  • Value inlining: a field whose type has no identity can be written into its owner's
    binary form - a null marker plus the field type's layout - instead of costing an entity and
    an object id per owner. Described in the type dictionary by the new member kind
    PersistenceTypeDescriptionMemberFieldValueStruct. The default inlines what the application
    declares; PersistenceFoundation#setValueInliningResolver and
    PersistenceValueInliningResolver.Disabled() select otherwise. An inlined layout evolves
    like any other (members added, removed, renamed, translated, nested value classes included).
  • PersistenceStoreHandler#applyKnown: reference an element by an object id an earlier store
    established, so unchanged value referents are not re-assigned new ids.
  • The java.time types and Optional are composed from their persisted state, and the JDK
    wrapper constants registered at startup are gated on whether the JDK has migrated them.

Compatibility

On a JDK without value classes nothing reports as one, so no handler, layout or type
dictionary entry changes, and existing data is unaffected. A dictionary that does contain an
inlined member cannot be read by an older version, and an inlined slot cannot go back to being
a reference - inlining is a one-way decision for data already written.

A value class has no identity, and its instances cannot be created empty and
  populated afterwards - which is exactly what the reflective path does: create
  blank, write each field, register the instance under an object id. Every step
  of that fails for a value, so value classes were unpersistable. This adds a
  path of their own, from the type handler down to the binary layout, and
  re-composes the JDK types that the language change turns into value classes.

  Handlers. BinaryHandlerGenericValueClass and BinaryLegacyTypeHandlerValueClass
  construct the instance from its persisted state instead of populating one, and
  BinaryTypeHandlerCreator routes value classes away from the reflective branch.
  A field whose type is a value class may be laid out inside its owner, so it is
  reached through a handle rather than by its memory offset - on the current,
  legacy and enum paths alike.

  Constructor contract. Constructing from stored state is the inverse of storing
  only if the constructor assigns each argument to the matching field unmodified.
  ValueClassConstructorContract verifies that by observing what the constructor
  does: at handler creation, or against the first instance stored where that
  cannot answer. Two same-typed fields taken in the wrong order, or a field
  computed from its argument, are refused instead of drifting silently on every
  round trip.

  Value inlining. A field whose type has no identity costs an entity, and an
  object id, per owner on every store. PersistenceValueInliningResolver decides
  per field whether the value is written into the owner's own binary form
  instead: a null marker followed by the field type's layout, described in the
  type dictionary by the new member kind
  PersistenceTypeDescriptionMemberFieldValueStruct. The default inlines what the
  application itself declares; setValueInliningResolver and Disabled() select
  otherwise. An inlined slot carries no type information of its own, so an
  abstract declared type, a reference inside the layout and a constructor that
  cannot be invoked are refused rather than inlined.

Evolution. An inlined layout evolves like any other: members added, removed,
  renamed or translated, nested value classes included, with the shapes that
  cannot be told apart refused rather than guessed.

  Identity consequences. PersistenceStoreHandler#applyKnown routes a reference by
  an object id an earlier store established, so storing a reference list no
  longer re-assigns ids to unchanged value referents. A lazy reference keeps its
  link to a value referent. A root refuses a value instance, loading a root
  meaning that the persisted state is applied to the very instance that was set.

  JDK types. The java.time types and Optional are now composed from their
  persisted state through AbstractBinaryHandlerCustomComposedValueType, and the
  wrapper constants registered at startup are gated on whether the JDK has
  migrated them.
@hg-ms
hg-ms requested review from fh-ms and a balanced review from Copilot September 24, 2026 08:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Several value-root, constructor-validation, and non-value-JVM compatibility paths can lose identity or fail loading.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 4 High severity · 1 Medium severity

Open (5)
What changed in this PR

Adds JEP 401 value-class persistence, including constructor-based loading, field inlining, schema evolution, and adapted JDK handlers.

Changes:

  • Introduces value-class detection, handlers, deferred creation, and constructor validation.
  • Adds inline value layouts to binary storage and type dictionaries.
  • Updates roots, constants, lazy references, and java.time/Optional handling.
File Description
Serializer.java Adapts eager and skipped storage behavior.
PersistenceValueInliningResolver.java Defines value-field inlining policy.
PersistenceTypeHandler.java Adds value and deferred-creation contracts.
PersistenceTypeDictionaryParser.java Parses inline value layouts.
PersistenceTypeDictionary.java Adds the value dictionary keyword.
PersistenceTypeDescriptionMemberFieldValueStruct.java Models inline value descriptions.
PersistenceTypeDescriptionMemberAppender.java Serializes inline dictionary entries.
PersistenceTypeDescriptionMember.java Includes lengths in layout equality.
PersistenceTypeDefinitionMemberFieldValueStruct.java Adds runtime-bound inline definitions.
PersistenceTypeDefinitionMemberCreator.java Creates runtime inline definitions.
PersistenceStoreHandler.java Adds known-ID reuse and eager-state APIs.
PersistenceRootReference.java Validates explicitly assigned roots.
PersistenceObjectManager.java Resolves value-based JDK constants.
PersistenceManager.java Adds arithmetic constant lookup.
PersistenceLegacyTypeHandlerWrapper.java Delegates new handler lifecycle methods.
PersistenceLegacyTypeHandlerCreator.java Routes value classes away from reflective legacy loading.
PersistenceFoundation.java Configures value-field inlining.
Persistence.java Adds arithmetic wrapper-constant mappings.
ValueClassConstructorContract.java Validates constructor-field correspondence.
BinaryValueTranslatorProvider.java Translates inline and value-typed fields.
BinaryValueReader.java Reads persisted constructor arguments.
BinaryTypeHandlerCreator.java Selects value-class handlers.
BinaryStorer.java Reuses known IDs and excludes value instances from merging.
BinaryReferenceTraverser.java Skips reference-free inline layouts.
BinaryPersistenceFoundation.java Wires inlining and refactoring dependencies.
BinaryPersistence.java Registers new JDK value-compatible handlers.
BinaryLoadItem.java Tracks deferred creation state.
BinaryLoader.java Implements dependency-ordered deferred creation.
BinaryLegacyTypeHandlerValueClass.java Loads evolved value classes by construction.
BinaryLegacyTypeHandlerRerouting.java Moves legacy rewriting before reference traversal.
BinaryLegacyTypeHandlerCreator.java Creates value-specific legacy handlers.
BinaryHandlerGenericValueClass.java Implements generic value-class persistence.
BinaryHandlerGenericType.java Passes through the inlining resolver.
BinaryHandlerGenericEnum.java Handles value-typed enum fields safely.
Binary.java Adds known-reference list storage.
AbstractBinaryLegacyTypeHandlerTranslating.java Delegates value/deferred traits.
AbstractBinaryHandlerReflective.java Integrates inline fields into reflective layouts.
AbstractBinaryHandlerCustomComposedValueType.java Supports types constructed from referenced parts.
BinaryHandlerLazyDefault.java Preserves links to immutable value referents.
BinaryHandlerRootReferenceDefault.java Handles loaded value roots.
BinaryHandlerPersistenceRootsDefault.java Avoids registering value roots.
BinaryHandlerEntityLoading.java Delegates new load lifecycle methods.
BinaryHandlerOptional.java Constructs Optional from persisted content.
BinaryHandlerZoneRegion.java Reconstructs region-based zones.
BinaryHandlerZonedDateTime.java Reconstructs zoned date-times.
BinaryHandlerYearMonth.java Validates immutable persisted state.
BinaryHandlerYear.java Adds constructor-based Year handling.
BinaryHandlerPeriod.java Validates immutable persisted state.
BinaryHandlerOffsetTime.java Reconstructs offset times.
BinaryHandlerOffsetDateTime.java Reconstructs offset date-times.
BinaryHandlerMonthDay.java Validates immutable persisted state.
BinaryHandlerLocalTime.java Adds constructor-based local-time handling.
BinaryHandlerLocalDateTime.java Reconstructs local date-times.
BinaryHandlerLocalDate.java Validates immutable persisted state.
BinaryHandlerInstant.java Adds Instant handling.
BinaryHandlerDuration.java Adds Duration handling.
AbstractBinaryHandlerSecondsNanos.java Shares seconds/nanos persistence logic.
XTypes.java Clarifies policy-value versus JVM-value semantics.
ValueType.java Documents its relationship to JEP 401.
XReflect.java Detects value classes across JDK versions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

LocalTime.of answers one shared instance per whole hour, Instant.ofEpochSecond
  one for the epoch and Duration.ofSeconds one for zero. Building an instance from
  the persisted state is what a value class needs and what an identity class must
  not have: a storage written before these handlers existed can hold two
  separately obtained objects of such a value, and one instance built for both of
  them cannot be registered under both their object ids, so the storage does not
  open at all.

  The three handlers therefore move to #platformDependentHandlers and are
  registered per type, only where the JDK has migrated that type. Where it keeps
  its identity, the generic handler populates one instance per entity, as before.
The legacy root refactoring installs a root it has just loaded through #set, which
  delegates to #setRoot and therefore rejects a value instance - the very case the
  validation's own documentation exempts, a loaded root having no state applied to
  it. Such a storage does not open at all, and advises a change to data that is
  already written.

  It now uses #setRootSupplier, like the loaded default root in the branch below it
  and like the root reference handler. The supplier's javadoc states that this is
  the installation path for a root resolved from persisted state, and why that one
  does not validate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants