JEP draft: Prepare to make final mean final
119 comments
·March 31, 2025cogman10
nightpool
They actually have a very similar proposal in the draft already:
The sun.reflect.ReflectionFactory class only supports deserialization of objects whose classes implement java.io.Serializable
That is, you'll still be able to mutate final fields using the ReflectionFactory class, as long as that class inherits from Serializableowlstuffing
The issue is that many essential libraries and tools rely on setting internal final fields. I assume that's why the options around this have remained open-ended.
The problem with these various "integrity by default" options is that, in most cases, granting access to one effectively grants access to all. For instance, JNI, agent libraries, and JPMS options can each be used to bypass restrictions, making the separation between them largely illusory. Integrity, as framed here, is ultimately binary.
The unfortunate reality of the "integrity by default" crusade is that applications relying on libraries and tools that modify internals will continue to do so. The JDK hasn’t filled any gaps—it has only made an already delicate situation worse.
pron
First, it's not a "crusade" but the steps necessary to deliver the features Java's users demand. Second, the prevalence of the use of JDK internals has dropped drastically, and demonstrably so. For example, many programs broke before internals were encapsulated during the upgrade from 8 to 9; 99% of the causes were libraries relying on internals, which had changed. Access to internals was closed off in JDK 16, although, as you say, it can be selectively allowed. And yet, between JDK 17 and JDK 23, changes of similar magnitude to the JDK internals caused nearly no upgrade problems. Upgrading the JDK now is smoother and easier than it's been in the last two decades. Why? Because there's been a large reduction in libraries' access to internals.
I think Java's handling of this transition compares very favourably to how other languages have handled similar transitions from some old model to a new one (or evolution in general) in terms of balancing the needs of both old and new projects.
cogman10
I can attest to how easy it's become to update jdks for our org.
8->11 was really a pain in the neck. 11->17 had some pain, but mostly was nothing serious.
17->21 has been painless.
And I have some projects running on 24 already with no problems.
The feature delivery has been great and we are getting pretty close to not needing to do anything but update the jdk to move forward.
Now, if only I could get devs to stop using lombok....
Supermancho
> First, it's not a "crusade" but the steps necessary to deliver the features Java's users demand
Semantics. Nobody demanded anything, if we want to play word games.
The java philosophy of final, makes software less extensible. This is the point; to have less overriding. Regardless of what the voting body decides (the meaning of users being subtly repurposed), the feature is anti-developer-agency past the point of healthy balance. I dont understand the enthusiasm.
twic
Not in my experience. I run my applications with default integrity, and when i hit a problem, i find out what caused it, and fix it. Often it's just a matter of upgrading a library to a newer version which doesn't do naughty reflective things, or changing some config to stop it doing so, or changing our own code. We had a serialisation library which did deep reflection to be slightly more efficient at serialising BigDecimal; there was a system property to turn that off, so i set it. We had code doing deep reflection into JMX to get the current PID; i changed it to use ProcessHandle instead. My colleague wrote a little test data library which does some horrific things; i might delete it and adopt Instacio instead.
I think there's only one case where i ended up relaxing integrity, and i'm hoping that's temporary - it will take more time to fix than i was willing to spend.
sgammon
One could easily generate the configurations via annotations, and I imagine Micronaut will do just that
null
elric
I've written my fair share of nasty reflexive code for testing or for abusing libraries, but I don't think I've ever overwritten final fields in this way. Private fields, sure. But not final.
Sounds like a good evolution to me.
exabrial
I'm 100% onboard with this. My thought was how are they going to make Serialization work, but looks like they thought of that.
I was trying to think of an edge case with JsonB or JAXB that would be affected by this... but generally those frameworks have told you for quite awhile not to do stupid stuff like:
``` @Getter public class HelloMessage { @JsonbProperty private final String helloMessage; } ```
I can't think of any frameworks offhand that do this.
hyperpape
Brian Goetz, chief architect of Java, once posted a "what they think I do" vs. "what I actually" do tweet. If I remember correctly, 25% - 50% of the "what I actually do" category was something like "get angry at serialization."
So I think it's safe to say "what about serialization?" is always going to be asked.
dstine
In this 2014 talk, Brian shows a slide in which he characterizes a visible fraction of his job as “regretting serialization” (somewhat tongue-in-cheek).
https://www.youtube.com/watch?v=2y5Pv4yN0b0&t=930s
Link is to the start of a sequence of three slides, the third of which is the slide in question.
For a more recent update on serialization, watch this talk “Serialization: A New Hope”: https://www.youtube.com/watch?v=mIbA2ymCWDs
int_19h
It's not just Java, either. The original .NET serialization system is very obviously inspired by Java, and has many of the same issues - to the point where it's effectively deprecated these days.
exabrial
Serialization is unfortunately important though... want to suspend a program and resume it later, or transfer it over a network, etc. The real world is kinda a let down.
esprehn
That's a hydration problem though, it doesn't require magically serializing the running state like java Serializable wants to. It just needs a way to produce minimal inputs to then reconstitute the state again on the other side.
magicalhippo
So this is about warning about using deep reflection or such to modify fields marked as final.
Not a Java dev, so I thought it might be related to classes marked final somehow. But this seems like a reasonable proposal, at least in spirit.
theanonymousone
If there was a r/nottheonion for tech :D
Jokes aside, I thought the ability to mutate final fields was already removed/restricted after Java 17 :/
bironran
A cursory glance at "setAccessible" usage reveals popular libraries such as serializers like gson and jaxb, class manipulation and generation like cglib, aspectj and even jdk.internal.reflect, testing frameworks and libraries including junit, mockito and other mocking libraries, lombok, groovy, spring, and the list goes on and on.
My bet is that this will be yet another "checked exception" or "module system", where many applications now need to add "--add-opens". If you'll use ANY of many of the more popular frameworks or libraries you'll end up giving this assurance away, which will make library developers not able to rely on it and we're back to square one.
pron
We've addressed that in the JEP. Serialization libraries have a special way to circumvent this (until we get Serialization 2.0), and mocking libraries may, indeed, need to set the flag, but they're rarely used in production, so if they don't enjoy some new optimisation -- no big deal.
BTW, this JEP does not apply to setAccessible generally, as that's been restricted since JDK 16, but only to the particular (and more rare) use of setAccessible to mutate instance final fields. As the JEP says, static final fields, records' internal instance fields, and final instance fields of hidden classes cannot be mutated with that approach currently, so it's never been something that's expected to work in all cases.
LadyCailin
Would be nice to have a single “--test-mode” flag that is only meant to be set when running tests, and allows for all this leniency, (add opens, etc) in a single flag.
pron
We should separate the problem from the solution. The problem is that running tests may require relatively many integrity-busting flags. That is true.
There are, however, better solutions than a global test-mode flag that, invariably, will be used by some in production out of laziness, leaving no auditable record of what integrity constraints need to be violated and why. When a new team lead is appointed some years later they will have a hard time trying to reduce the entropy.
The better solutions will arrive in due course, but until then, build tools can automatically add most of the necessary flags. They should be encouraged to do that.
PathOfEclipse
setAccessible is also used to be able to access private fields, and not just to be able to write to final fields. Most libraries shouldn't need to set final fields, and I say this as someone who was very against when they deprecated java.lang.misc.Unsafe. I've only had to set a final field once in my career and it was related to some obscure MySql/JDBC driver bug/workaround. This particular deprecation seems very sensible to me.
eastbound
So how should GSON initialize an object?
The theory is, go through the constructor. However, some objects are designed to go through several steps before reaching the desired state.
If GSON must deserialize {…, state:”CONFIRMED”}, it needs to call new Transaction(account1, account2, amount), then .setState(STARTED) then .setState(PENDING) then .setState(PAID) then .setState(CONFIRMED) ? That’s the theory of the constructor and mutation methods guarding the state, so that it is physically impossible to reach a wrong state.
There is a convention that deserialization is an exception to this theory: It should be able to restore the object as-is, after for example a transfer over the wire. So it was conventionally enabled to set final variables of the object, but only at initialization and only for its own good. It was assumed that, even though GSON could reach a state that was unachievable through normal means, it was, after all, the role of the programmer to add the right annotations to avoid this.
So how do we do it now?
steveklabnik
> So how do we do it now?
The JEP says:
> the developers of serialization libraries should serialize and deserialize objects using the sun.reflect.ReflectionFactory class, which is supported for this purpose. Its deserialization methods can mutate final fields even if called from code in modules that are not enabled for final field mutation.
I don't know enough about the details here to say if that's sufficient, but I imagine that it at least should be, or if it's not, it will be improved to the point where it can be.
vips7L
Why would you use GSON for objects that go through steps of state? Why would you mark fields like State as final when it is actually mutable? This just sounds like poorly designed code.
Maybe I don't know of your use case, but GSON/Jackson/Json type classes are strictly data that should only represent the data coming over the wire. If you need to further manipulate that data it sounds like the classes have too much responsibility.
twic
It strikes me that we could have a way to reflectively create an object from values for all its fields in a single step - similar to what record constructor does, but for any class (could even be Class::getCanonicalConstructor, returning a java.lang.reflect.Constructor). It would be equivalent to creating an uninitialised instance and then setting its fields one by one, but the partly-initialised object would never be visible. This should probably be restricted, because it bypasses any invariants the constructor enforces, but as you say, ultimately serialisation libraries do need to do that.
n_plus_1_acc
Just do it like Rust
merb
Yeah like the module system. Looks good on paper, is probably hard to deal with. There are still tons of popular libraries that have no module-info. Java does evolve, but the direction it does is so weird. And than the tooling is strange and it’s worse that there are basically two build tools, both with their upsides and downsides but they still feel more complicated than tools for other languages like cargo, go (if you consider that), msbuild (the modern csproj stuff/slnx)
nightpool
[Speculative optimizations] may not suffice in this case as future planned optimizations may wish to rely not only on immutability within the lifetime of the process, but also on the immutability of fields from one run of the application to the next.
Can someone elaborate a little more on what this means? I'm very surprised to hear that this was considered a blocker important enough to add all of this complicated machinery (and breaking several deserialization libraries...), when I've never even heard of such an optimization and can't imagine what sort of form it would takepron
There's ongoing work, as part of Project Leyden, to cache certain computations -- either performed by the user code or the JVM -- from one run of the program to the next, including the caching of JIT-compiled machine code. The early parts of this work were discussed here: https://www.morling.dev/blog/jep-483-aot-class-loading-linki...
xxs
This is rather old[0] but still relevant.
Surprise #2! A popular open-source framework writes to final fields after object construction via generated bytecodes!... This optimization of the final field is *the* main optimization performed on final fields.
[0] https://web.archive.org/web/20121016082428/http://www.azulsy...
Misdicorl
I suppose serializing the JVM state itself to avoid the cold start problem might take advantage of this?
GauntletWizard
When I had the brief displeasure of working on HDFS at Facebook, we took a series of customer meetings to figure out how to get our oldest customers to upgrade their clusters. I was in a meeting with the photos team about what their requirements were and what was blocking them from upgrading, and they were very frank - they asked if the upgrade preserved the internal struct types associated with blocks on the disc servers. They didn't actually use hdfs as a file system, they allocated 1 GB files with zero replication, then used deep reflection to find the extent that comprised them on the discful storage servers, then built their own archival backup file system on top of that. I was horrified. The some of the older hats on the team were less surprised, having had some inkling of what was going on, even though they clearly didn't understand the details. Others considered it tantamount to sacrilege.
I think about this a lot. What they had built was probably actually the best distributed file system within Facebook. It was similarly structured to unraid, and had good availability, durability, and space saving properties, but the approach to engineering was just so wrong headed in my opinion that I couldn't stomach it. Talking about it with other Java programmers within facebook, nobody seemed to mind. Final was just a hint after all.
adrianmonk
That reminds me of a quote from some Perl documentation[1]:
> Perl does not enforce private and public parts of its modules as you may have been used to in other languages like C++, Ada, or Modula-17. Perl doesn't have an infatuation with enforced privacy. It would prefer that you stayed out of its living room because you weren't invited, not because it has a shotgun.
It's not exactly the same situation, but the point is, at the end of the day, you need to be able to rely on the people involved being willing to act reasonable. If you can't, then you're going to have problems.
---
dapperdrake
This approach is surprisingly (or unsurprisingly) one of the most robust.
jjmarr
From my perspective as a C++ developer, every attempt to use `const` for compiler optimization appears to be stymied by the existence of `const_cast`, because modifying a `const` value is only undefined behaviour if the underlying object is `const`. Glad to see that Java is willing to break the language to improve it.
PhilipRoman
The implementation of "const" in C/C++ is really annoying, in the end all you get is a bit of semantic documentation and a bunch of compiler errors or casts whenever some library doesn't use it. You can often trick the compiler into stronger optimizations by making a local variable marked as "const" (in which case it is a "true const") and copying the value to/from it.
int_19h
The real problem is that "const" in C/C++ is a misnomer - the literal meaning of the word means "unchanging", and yet if you have a pointer or reference to a const T, it can, in fact, change. A "const pointer" in C is really a read-only pointer to some data that may or may not be actually constant.
In C, at least, you can usually distinguish a true pointer to immutable data by using `restrict`.
mb7733
> modifying a `const` value is only undefined behaviour if the underlying object is `const`.
I found this sentence confusing. You mean that modifying a value that has been const_cast is undefined behaviour only if the original variable was const right? Or something else?
oconnor663
(Rereading your comment, it sounds like you might already know all of this. Apologies.)
If I understand correctly, here's a C++ example that has undefined behavior:
int foo() {
const int x = 42;
int *p = (int *)&x;
*p += 1;
return x;
}
foo() is UB, because it modifies the const object x, using a pointer cast to "cast away const". (Unfortunately, UBSan doesn't catch this, and I'm not aware of any sanitizer that does.) It's tempting to say that the pointer cast is "at fault" for the UB, but consider this very similar example that's not UB: int bar() {
int x = 42;
const int *const_p = &x;
int *p = (int *)&const_p;
*p += 1;
return x;
}
bar() has exactly the same pointer cast as foo(), however in this case the original x object is not const. That makes "casting away const" legal in this case. So the problem we're left with, is that knowing all the types isn't enough for us to tell whether this cast is going to cause UB. We have to know where the pointer originally came from, which might be in another function or another file.mb7733
Right, gotcha, I just found it confusing to say "modifying a `const` value" but if course you meant modifying the underlying value [through a cast]. All good.
satyanash
> Prepare to make final mean final
missed opportunity to call it "final final"
aardvark179
So happy to see this, thank you Ron and Alan.
TOGoS
> Application developers can avoid both current warnings and future restrictions by selectively enabling the ability to mutate final fields where essential.
/me raises hand
Maybe if you want to mutate a field, don't mark it `final`?
I know, I know, people like to pretend things are one way and then hand their objects over to some horrid framework that breaks all the rules, because apparently giant web of mutable spaghetti is just fine, not an anti-pattern at all if you let some third-party bull$#!7 ORM/dependency-injection-framework-for-people-who-don't-like-constructors do it.
Hmm, not a bad approach.
I think the one thing that'd be nice is if I could somehow tell the JVM from a class that this class is open for final mutation rather than needing special flags passed into the JVM or special manifests in the Jar. It's often pretty clear to me, as a dev, what I when I need something to have final mutation (generally only with serialization objects).
For example,
That'd allow me to transition code by just adding an annotation where it needs to be while also getting the benefit that final is really final everywhere else in code that isn't working with serialization.