Serguey Asael Shinder: Synchronizing on a LocalDate throws IdentityException in JDK 28
JEP 401, Value Objects (Preview), is integrated and targeted at JDK 28. The text is at https://openjdk.org/jeps/401 and it is worth reading in full, but the part that will reach most codebases first is not the performance story. It is that some existing Platform API classes become value objects, and a small set of operations that work on every object today stop working on them.
What a value object is
A value object is immutable and has no identity. Two of them are distinguished only by their field values, so == compares the fields rather than asking whether these are the same object in memory. That is the whole idea: LocalDate.of(1996, 1, 23) and another LocalDate built to the same year, month and day are not two things that happen to agree, they are the same value.
The JEP is explicit that this is not a struct feature, not a change to primitives, and not an attempt to retire equals. == is redefined only as far as an identity-free object requires.
The part that breaks
Identity-sensitive operations are not supported on value objects. Synchronization is the one you will hit:
LocalDate d = LocalDate.of(1996, 1, 23);
synchronized (d) { ... } // compile error: required a type with identity
Object o = d;
synchronized (o) { ... } // java.lang.IdentityException at run time
Two different failures from the same line of thinking. When the compiler can see the value type, it refuses. When the reference is widened to Object — a lock held in a map, a synchronized block around a cache entry, anything generic — it compiles and throws IdentityException when it runs.
Locking on a LocalDate is not something anyone designs on purpose. Locking on whatever object this map returned is extremely common, and that is the code that changes behaviour.

Three more things in the same JEP
Objects.hasIdentity and Objects.requireIdentity. New methods for asking the question in code. If you write a library that locks on, or identity-maps, objects handed to it by a caller, requireIdentity is how you fail early and with a decent message instead of at the synchronized.
A class file flag. Identity classes carry ACC_IDENTITY, which supersedes the legacy ACC_SUPER, and java.lang.reflect.AccessFlag.IDENTITY exposes it. Bytecode tools that have opinions about ACC_SUPER will need to learn this one.
Serialization of non-record value classes needs work. Value records serialize as they did. A value class that implements Serializable and is not a record must implement writeReplace and readResolve, otherwise serializing or deserializing it fails with InvalidClassException. The reason is mechanical: value classes compile with strictly initialized fields, which deserialization cannot safely populate, so a replacement object has to stand in.
What I would actually check
Grep for synchronized blocks whose lock expression is typed Object, and for IdentityHashMap. Those are the two places where "the same object" is load-bearing and the type does not tell you what arrived. Everything else — equals, collections, generics, null — keeps working, because references to value objects are still references and can still be null.
And note what String does: it stays an identity class. "aabcd".substring(1) == "abcd" is still false. The JEP is not quietly fixing the thing everyone gets wrong in their first week of Java; it is giving class authors a way to opt out of identity where identity was never meaningful.