- Published on
Java 28 just made == work on LocalDate. Everything you were taught is about to change.
โ 9 min read
Java 28 just made == work on LocalDate. Everything you were taught is about to change.
๐ November 8, 2026
๐ท๏ธ โ Java Deep Dive
Somewhere right now, a candidate is sitting in an interview and someone is asking the question. You know the one. "What's the difference between == and .equals() in Java?" The candidate takes a breath and says == compares references, .equals() compares values, and the interviewer nods, and everyone goes home happy.
That answer is about to need a footnote. Let me show you why.
โฑ๏ธ 60-Second Summary
JEP 401, Value Objects (Preview), is integrated into JDK 28. It is a preview feature, so you need --enable-preview to see any of it. Switch it on, and 30 classes in the JDK stop having identity. That list includes Integer, Long, Double, Boolean, Optional, LocalDate, LocalDateTime, Duration, and friends. For those classes, == now compares field values. Synchronizing on one of them throws IdentityException. Putting one in a WeakHashMap throws too. You can also declare your own value classes with a new value modifier.
Feature freeze (Rampdown Phase One) is December 3, 2026. General availability is March 9, 2027. Nothing changes in your existing code until you opt in. But the mental model you were taught is changing, and it is changing for good.
๐คฏ The demo that makes people go quiet
Fire up JShell from a JDK 28 early-access build with preview enabled:
jshell> LocalDate d1 = LocalDate.of(2026, 7, 31);
jshell> LocalDate d2 = LocalDate.of(2026, 7, 31);
jshell> d1 == d2
$3 ==> true
Yes. Two separately constructed LocalDate objects, and == says true.
Every Java developer alive has had a bug caused by exactly this returning false. The Integer cache that works up to 127 and then silently stops. The two Long values that look identical in the debugger but fail the if. Entire StackOverflow threads. Entire careers. And now, with one flag, the JVM shrugs and says: they have the same fields, so they are the same object.
The JEP puts it plainly. For value objects, == tests for "references to instances of the same class with the same field values". Reference-typed fields inside are compared recursively with == as well. A LocalDateTime holding a LocalDate and a LocalTime compares all the way down.
๐งฑ What an identity even was
Here is the thing nobody says out loud in those interviews. Every object you have ever created in Java came with a hidden feature you rarely wanted: identity. Identity is what lets two objects with identical fields be "different." It is what synchronized locks on. It is what System.identityHashCode reads. It is what WeakReference tracks.
For a mutable object like an ArrayList, identity is essential. You need to know which list you are modifying. For an immutable LocalDate representing July 31, 2026, identity is pure baggage. There is no meaningful way two "July 31, 2026" objects can differ. You never wanted to lock on one. You never wanted to mutate one. You just wanted the date.
Project Valhalla's bet is simple: let the class author declare "my instances have no identity," and let the JVM do whatever it wants with the bytes. That is what a value class is. The value modifier makes every field implicitly final and the class itself implicitly final. In exchange, the JVM is allowed to store the fields on the stack, in CPU registers, or even encode them directly into the reference itself instead of pointing at a heap object. The JEP is careful to say it does not guarantee any particular layout. But that is the whole point of the exercise.
๐ The 30 classes that lost their identity
The JEP lists these as examples of the 30 classes that are now value classes when preview is on:
| Package | Classes |
|---|---|
java.lang | Integer, Long, Float, Double, Byte, Short, Character, Boolean |
java.util | Optional, OptionalInt, OptionalLong, OptionalDouble |
java.time | LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Duration |
Yes, that is all eight primitive wrappers. The JEP says it directly: "All instances of these classes are value objects. This includes the boxed primitives."
And one that is not on the list, because people always ask: String is not a value class. Strings keep their identity. "hello" == "hello" still depends on interning, same as it always has. Your interview answer about strings survives intact.
๐ฅ Three things that break, and one that quietly disappears
This is the part that is actually worth your attention. If you have code that treats Integer or LocalDate as a normal object with identity, preview mode will tell you about it.
1. Synchronizing on a value object. If the compiler can see the type, it is a compile error:
synchronized (d1) { d1.notify(); }
| Error:
| unexpected type
| required: a type with identity
| found: java.time.LocalDate
If the type is hidden behind Object, it compiles and blows up at runtime:
Object o = d1;
synchronized (o) { o.notify(); }
| Exception java.lang.IdentityException:
| Cannot synchronize on an instance of value class java.time.LocalDate
Think about every piece of legacy code you have seen that does synchronized (someInteger) as a cheap lock. That pattern is now a runtime exception waiting to happen.
2. wait() and notify(). Since you cannot synchronize on a value object, the JEP says calls to these methods "always fail with an IllegalMonitorStateException."
3. WeakHashMap and java.lang.ref. Straight from the JEP: "The garbage collection APIs in the java.lang.ref package and the java.util.WeakHashMap class cannot be used with value objects." Trying to create a WeakReference, SoftReference, or PhantomReference to a value object throws IdentityException. If you have a cache keyed on Integer IDs inside a WeakHashMap, that cache is about to have a very bad day. The good news is that javac has issued identity warnings for value-based classes since JDK 25, and in JDK 28 it extends those warnings to value classes too. Fix the warnings now and you will never meet the exception.
And the one that quietly disappears: finalize(). The JEP says it is "never invoked by the garbage collector" for a value object. Nobody will miss it.
๐ชค The gotcha that will show up in interviews next
If you think == on value objects is just equals() with less typing, here is the trap. Floating point.
Double has multiple bit patterns that all mean NaN. The equals method treats them as equal. But == on two Double value objects compares the raw field, so the JEP warns that "value objects that wrap different NaN values are distinguishable by the == operator."
So the new interview answer is not "they are the same now." The new answer is: == on a value object compares fields bit-for-bit, recursively. equals() compares meaning. Use equals() when you mean equals. The JEP itself says exactly that: "To test whether two value objects represent the same value, use the equals method."
One more subtlety. Because == recurses into reference-typed fields, the JEP notes that comparing two value objects "may require an unbounded number of comparisons." A == that used to be a single pointer compare is no longer guaranteed to be O(1). You will probably never notice. But now you know.
โ๏ธ Writing your own value class
You are not limited to the JDK's list. Any class whose instances are immutable and interchangeable can opt in:
value class Money {
private final long cents;
private final String currency;
Money(long cents, String currency) {
this.cents = cents;
this.currency = currency;
}
}
The value modifier makes the fields and the class implicitly final, so you could drop that keyword. Abstract value classes are allowed too, and their subclasses can be either value or identity classes. Two new helpers, Objects.hasIdentity and Objects.requireIdentity, let you check at runtime which kind of object you are holding.
For the record-heavy codebases out there: if you were already reaching for record to say "this is just data," a value class is saying the same thing to the JVM as well as to your teammates.
๐ Why you can relax, and why you should not
Relax, because: none of this happens without --enable-preview. The JEP states that these API classes become value classes "only when preview features are enabled; otherwise, they behave just as they did in JDK 27." Your production code on JDK 28 will run exactly as it does today. Preview features also change between releases, so what ships in March is not necessarily the final shape.
Do not relax, because: this is the first time in Java's history that the meaning of == has changed for existing classes. The JEP's own risk section admits developers "may be surprised by, or encounter bugs due to, changes in the behavior of the == operator and the synchronized keyword." Preview today becomes final in a release or two. The teams who find their synchronized (Integer) and WeakHashMap<Long, ...> now, while it is a warning, will have a boring upgrade. The ones who wait will have an exciting one.
๐ Dates to pin
| Milestone | Date |
|---|---|
| Rampdown Phase One (feature freeze) | December 3, 2026 |
| Rampdown Phase Two | January 14, 2027 |
| Initial Release Candidate | February 4, 2027 |
| Final Release Candidate | February 16, 2027 |
| JDK 28 General Availability | March 9, 2027 |
The schedule was proposed in October with review ending October 29. I could not find a post declaring it final, so treat the dates as very likely rather than carved in stone.
๐งช Try it this weekend
Grab a JDK 28 early-access build from jdk.java.net and run:
jshell --enable-preview
Then type Integer.valueOf(1000) == Integer.valueOf(1000) and watch a decade of trivia questions evaporate. For a real program:
javac --release 28 --enable-preview Main.java
java --enable-preview Main
Then go find every synchronized block in your codebase that locks on a wrapper type, and every WeakHashMap keyed by one. Future you will send a thank-you note.
If you enjoyed this one, forward it to the colleague who still argues about the Integer cache. They are going to have feelings.
โ Javed Shaikh, Javathoughts
Sources: JEP 401: Value Objects (Preview), Inside.java: Try Out JEP 401 Value Classes, InfoQ: Project Valhalla's First Preview, InfoQ Java News Roundup, October 19, 2026 (JDK 28 schedule)
๐ฌ Never Miss an Issue
Get AI, Java & Engineering updates 3ร a week โ delivered to your inbox.
