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,
@FinalMutatableByReflection
class Foo {
final String bar;
}
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. 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 SerializableThe 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.
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.
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....
The upsides are: - it generates code and does not do anything funky with internals, - it has a lot of knobs if you need something a little different.
The downside is that it does not provide you with the other Lombok annotations. In practice that has been OK!
One of the only remaining reasons I discard libraries without a further thought
Records have reduced the advantages of Lombok by a boatload. But there still are some things that can't be records.
That's a fundamental misunderstanding of hashCode, and lombok makes no exception. Not all fields need to be used to calculate hashCode, it makes the overall performance worse in most cases. Equals is of course different, however if you have an identity (i.e. database primary key), only the identity should be used.
I've seen so many performance issues with hashcode because devs will put all fields into it. Even though there's an id column or even fields that imply other fields. Hashing 1000 char strings when there's a UUID or int field that guarantees identity is silly.
I think it's because devs have an preference for symmetry. I see the same thing happen when they preferably add setters for all fields even though they aren't necessary.
Realistically, I have trained quite a few folks on my own.
>I think it's because devs have an preference for symmetry.
Another option is that's the default for all IDEs auto gen, so few clicks/taps and it's done.
My favorite question on interviews is explaining all methods of class Object, including the contract and best practices for equals/hashCode. Failure to answer this question automatically disqualifies applicants to mid-level and senior positions.
finalize() is actually is a very hard mode; I'd not expect any extra senior to be able to explain it properly (incl. the semantics of JMM, the fact half created objects can be finalized; the resurrection ability). Deprecated now, so perhaps no need?
wait/notify/notifyAll - easier, still require some practice, also not that useful any longer; but still I'd expect to know not to use a naked notify and how to properly use a loop around wait
clone() - it'd be a hard nut for many, and I have seen more than enough implementations that straight out use new XXX(); not very difficult but not intuitive
hashCode/equals -> hashCode being by default a random number generator is sort of cool; yes they are the backbone of all collection framework; also the value of the not overridden hashCode() is available through System.identifyHashCode()
getClass() - if included anonymous classes, it might puzzle some
toString() - finally something easy
---flip note: the standard templates for intellij could use some work when it comes to the quality of hashCode;
> Deprecated now, so perhaps no need?
Yes. Worth mentioning existence and “do not touch it”, but no need to go deep. Same with clone. The point of this exercise is to demonstrate that you can use the core library without shooting yourself in the foot. As for wait/notify/notifyAll, I’d expect the correct usage patterns from mid-level.
I, personally, avoid Lombok, specifically because I care about implementation details. Because if I would have wanted better Java, I'd go with Kotlin rather than this hacky way of using another Java-like language. But other people hold different opinions.
I dont use lombok and the latter has been actively removed. If you have access to records, lombok is just bad. Even if you dont have - public final fields are sufficient in most case + c-tor and validation there. Just dont use getters & setters.
The 10% lombok can do because it's peaking at internals isn't valuable. I don't, for example, need an annotation to create a static logger. That's dumb.
The issue is having plainly useless getters and setter (just use public fields); along with the fact that mutable hashCode/equals
that's actually not too hard. It has happened to get them all converted to avid opponents of lombok.
Other tools and libraries generally do not interact in such an errorprone manner.
That said, when you know how it works, what it needs, and you know how to iron all of those tiny wrinkles, it works fine and saves you some code and/or sanity. It's not the devil, it's a powerful tool with some downsides.
There are very cool libraries making use of it, e.g. mapstruct.
Now, lombok in its current approach can't make use of it, as it explicitly wants to modify the given class with its annotations. This is forbidden, so they literally hack into the java compiler classes themselves, modifying the AST in-flight. As you can imagine, this might break at a new Java update any time.
It's not a might. If you look at the lombok changelog they have a release for nearly every new jdk version because they break constantly.
When a project can't move up jvm versions it's very frequently been because of lombok.
And if you look at the commit log, it's all a single dev running the show. He's been doing it for years which means the stability of your project is pretty dependant on this one guy keeping things up to date and finding work arounds to get at the jvm internals.
There's one of these for just about every occasion
Records also literally do a huge portion of what Lombok does.
There are a lot of fairly popular alternatives to Lombok out there that don't need a constant eye on maintenance. Lombok is probably the most popular but that's really mostly because it's got the first mover advantage.
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.
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.
Sounds like a good evolution to me.
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.
So I think it's safe to say "what about serialization?" is always going to be asked.
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
Jokes aside, I thought the ability to mutate final fields was already removed/restricted after Java 17 :/
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.
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.
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.
On the other hand, I don’t think the solution to someone holding a shotgun to their foot and threatening to pull the trigger is to make everyone wear armored shoes. They’re already a lost cause, and there are a billion other ways they can shoot their foot off, if they are so inclined.
I agree with the principal of making it hard to screw things up assuming good faith efforts (making it hard to fall in the pit of despair), so overall I like the JEP.
I don't think so, either, it's just that I think there are better solutions than a test-mode flag at the level of the `java` launcher. If the mechanism that runs the tests can automatically configure the appropriate capabilities without requiring the user running the tests to do manual configuration then the problem is solved for those who just want to easily run tests just as well as a test-mode configuration.
The idea of a test-mode flag has been floated before and considered; we're not ruling it out, but if such a mode is ever added, I can't tell you now what it would mean exactly. In any event, it's better to carefully study the nature of the problem and its origins before suggesting a particular solution. As Brian Goetz likes to say, today's solutions may well become tomorrow's problems.
> They’re already a lost cause, and there are a billion other ways they can shoot their foot off, if they are so inclined.
True, but our experience shows that it's not a good idea to make the bad choice the easiest one, or people may pick it out of laziness. Let those who want to shoot themselves in the foot work for it. If nothing else, it increases the chance that they learn what their (not-entirely-trivial) configuration means, and maybe they'll realise they don't want it after all.
Someone might point out that there are still ways to do the wrong thing out of laziness by blindingly copying a configuration from StackOverflow etc., but we're not done yet.
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?
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.
The JEP also says:
> The sun.reflect.ReflectionFactory class only supports deserialization of objects whose classes implement java.io.Serializable.
In my experience, most classes being deserialized by libraries like GSON do not implement Serializable. Implementing Serializable is mostly done by classes which want to be serialized and deserialized through Java's native serialization format (which is used by nothing outside Java, unlike cross-platform formats like JSON or CBOR).
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.
[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 takeSurprise #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...
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.
In C, at least, you can usually distinguish a true pointer to immutable data by using `restrict`.
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?
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.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.
> 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.
---
missed opportunity to call it "final final"
/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.
(yes yes, I know, that would break syntax... but please come up with something to discourage mutability)
Yeah I know there's ways around it, but then the author known what they told the other party to expect.
Even the JS standard library struggles with this. You just have to remember that .sort() modifies the array in place (and returns it), but .slice() does a shallow clone of the array. (Not a deep clone - that would be different again!)
I don't think this represents struggling. There needs to be some way to sort in place. Sometimes you need to sort a big array and don't want to allocate.
And there should be some way to clone the array without mutating. That's slice. So you how do you sort a clone? .slice().sort()
I think by far the biggest problem with ES .sort() is that number arrays don't sort numerically by default.
Your parent isn't saying there shouldn't be mutation, just that the mutation should be obvious.
In Rust, the type signature for the in-place sort is
pub fn sort(&mut self)
where
T: Ord,
That `&mut self` lets you know that it's going to mutate. foo(x); // Moves or copies
foo(&x); // immutable reference
foo(&mut x); // mutable reference
I don’t need to look up the signature of foo to understand what happens to my variable. It’s obvious at a glance.ISTM that there are almost always some kinds of mutable data structures present in non-trivial programs. And outside of the program, you have databases to directories of files that get mutated by users or other parts of a program. I think this is just a fact of life.
Languages which lack the tools to do this are just harder to reason about, at least for me.
In the JS example, as a non-JS coder, I would expect that a sort function that returns nothing/void would sort in-place, while a sort function that returns an array would return a sorted copy. I would not expect it to sort in-place and return it.
However in C++ I could easily see from the function definition that it might be doing that, because a sort that returns a copy would take a const reference for the input array. So if I came across a sort function which took a non-const reference as input I'd be able to at least suspect it's doing it in-place.
While bad code can exist in any language, I get worried about too much const in code, because it means they failed at both 1) and 2) and instead there are usually seriously tricky protocols that must be observed to make the thing work. I often ran into code where people were sprinkling const all over the code to lock things down but they fundamentally did not understand the design and made it nearly impossible to evolve, unless you used casts to get rid of const, which defeats the whole purpose.
I'm not saying const doesn't have value, but it's weapon #3, not weapon #1.
That's like using a hammer on a screw, clearly not the right way.
Thankfully I've never worked on such codebases.
Records?
What man that sees the ever-whirling wheel
Of Change, the which all mortal things doth sway,
But that thereby doth find, and plainly feel,
How Mutability in them doth play
Her cruel sports to many men's decay?
(Edmund Spenser, 1596)Regardless, to me this isn't about performance, this is about "integrity" (to use the same term as in the JEP): consumers of my library should not be mucking about in private implementation details, and then inevitably complaining about problems to me when something breaks. If you need a feature that I don't expose, ask for it (or better yet, submit a patch).
Sure, I've used reflection to modify library internals before, but I recognize that whenever I do that I'm inviting a maintenance headache into my world. But some people just think things should always work, even when they are breaking them.
Up till now I always assumed the compiler would figure out on its own which variables were final, and optimize as needed. But this JEP makes it seem like there are optimizations that only happen if you manually mark the variable.
Seems like this way?