They want basically to solve the main Java design flaw with (almost) everything is a reference paradigm. C++ and Rust have had value-types from day one.
> 64 bits, including the null flag
So, this basically makes every value-object optional, adds extra overhead and makes code less safe to null pointer dereference errors.
> but a class with, say, two int fields or one double may not fit in an atomic write and end up as an ordinary object on the heap anyway
So, the whole optimization is applied only for very small structs with no more than two scalars (or so). Did it worth to spend 10+ years of development to achieve this?
top-level page: https://openjdk.org/projects/jdk/28/spec/
JEP status: https://bugs.openjdk.org/secure/Dashboard.jspa?selectPageId=...
I'd really like to see someone trace related developments in C#, Swift, Java, and Rust, since they all have been racing to catch up to hardware, and I believe they are cross-pollinating.
(My concern is how all this will affect the FFI memory shares.)
1. Can someone remind me why it was so important/intentional at the start of the language that every object has identity? 2. Why is it important that we not synchronize on these value objects?
Scalarization can fail in surprising ways just due to what a maximal atomic write can be on the target platform, and then it fall back to heap allocated objects.
Even if there's type erasure.
I much rather have the compiler balk at me than let me write something that may or may not work as expected.
If you want to change an element of such an array you need to create a new immutable struct which in practice it is quite fast, but a bit verbose to write.
That seems off. They're still objects, the new thing is that they can give up identity.
Fun read.
> The model was powerful, but also mentally heavy
No it isn't! it is this interpretation that kills off the null-safety debate entirely. Saying you have a variable that cannot be null is not a mentally taxing distinction, especially since everything is labelled thoroughly.
> The team, faithful to the lesson “simplify the model for the user, even at the cost of the performance ceiling,” ultimately dismantled this dualism.
but it would have simplified it for the user.
The whole attitude and process around this and the other topics gives me very little faith that Java can be steered in a sensible direction here. The type system of a programming language is supposed to give convenient guarantees to the developer on a CPU that can only do numbers. There is no reason to reduce the optional(!) safety guarantees you can offer with the excuse of "too mentally taxing".
Hell, they even get there half way by recognising:
> the language model and the JVM model don’t have to overlap one hundred percent
How much was this article proof-read? Didn't they just get finished talking about how heap flattening won't work for objects with > 64-bit representations? Their `Point` is at least 65 bits (two 32-bit ints plus the null flag). The "plus a possible null flag" and oddly short following statements seem to suggest this was some AI that got sidetracked by trying to make emphatic statements... oh and also the "[IMAGE: the same Point[] array in two variants..." block halfway down the page is unfortunate.
If Java was a child, imagine it being brought up by loving parents for the first few years (Sun) then it was thrown in a garage with some other children and neglected by its evil guardian (Oracle)
Neglected and unloved till JDK 8, its basically been playing catch up.
So when people say "oh so its now got structs or value types of X", yes it has but that's because it has been stunted in its development due to big bureaucratic and hostile corporate processes, but its free now and is getting love through the OpenJDK family.
I will continue to enjoy writing once and deploying anywhere!
I’ve been reading the mailing lists and watched all videos on the topic and it is truly inspiring how much they managed to consolidate the design to something that always looked like java.
But while also going far deeper in granularity and understanding what it even means to be a value type and what optimizations can be done where
So == for value classes will basically be like memcmp(). That is a bit unfortunate, as it breaks encapsulation, exposing implementation details. Client code can use this to do case distinctions based on how a given value is internally represented. In a way, it’s worse than identity comparison, because identity comparison at least doesn’t expose internal state.
So where in other languages, the struct/class taxonomy is binary, Java allows more granular control, reflection the semantics of the underlying domain. Snd as it turns out, structs have a wide range of footguns, especially in a parallel context.
I really hope they give an escape hatch for this. It will make it really hard to extract a lot of the benefit of valhala if you can't make a thread unsafe value class. It's also one of those problems that will be quite hard to run into. You basically need something like this
class Bar {
static Foo value[] = new Foo[10];
static void setFooFromManyThreads(Foo foo) {
value[0] = foo;
}
value record Foo(int x, int y, int z) {};
}
Not something you typically run into and generally already a thread safety problem.The solution is also simple, a `synchronized{}` block will fix it if you need to have a tearable class that's written from multiple threads.
But the other thing is that for SIMD operations, you really need flattening, and that really does typically mean having something like `Foo(double x, double y, double z)` in play. It'd be a shame if the way we have to do this is a struct of arrays.
Sad. Hope they can do this by the next LTS JDK.
What will this code print:
Point a = new Point(10, 10);
Point b = a;
a.x = 100;
System.out.println(b.x);
Until now the answer was obvious. Now with the addition of value classes, the answer depends on whether Point is a value class or a reference class. So readability suffers with this design.This is a violation of the principle of uniformity. In The Psychology of Computer Programming, Weinberg explains that uniformity is a psychological principle which says that users/programmers expect that things that look similar should do similar things, and conversely that things that look different should do different things.
If a programming language lets two constructs look nearly identical at the use site while having meaningfully different semantics, it increases the cognitive burden on the reader. Programmers must inspect the type declaration or rely on tooling to understand whether assignment, equality, identity, and mutation behave like ordinary reference objects or like values. That can make code harder to reason about and maintain.
This could have been fixed by requiring the use of the "value" keyword not just at declaration time but also at use time like this:
value Point a = new Point(10, 10);
Point b = a;
a.x = 100;
System.out.println(b.x);If I have a function that has a value `x` that erases to `java.lang.Object` (e.g. a parametric function with no lower bound); then it used to be safe to check for nullity and then synchronize on the object.
This is no longer safe: This can now throw `IdentityException` into your face. (it was _never_ a good idea)
In other words, a lot of old code must be reviewed.
I suspect that `-XX:DiagnoseSyncOnValueBasedClasses=2` will need to stay (with the semantics: if user tries to synchronize on identity-less object, then log a JFR event and make it a NOP, don't throw an exception)!
The current JEP text is a little too ambiguous to figure out whether that is the plan, anyways.
> In 1995, a memory access cost roughly the same as a CPU operation
Uhm... no?!
Here's a CS paper from 1993(!) about prefetching from cache(!!) because the cache was slower than the ALU. https://www.eecs.umich.edu/techreports/cse/93/CSE-TR-152-93....
It would perhaps make Java look a little bad to say that, in 1995, the prevailing attitude in certain circles was "If it's too slow, just wait for faster hardware - Moore's Law forever baby!" (Of course, Sun was selling, at the time, relatively fast hardware - the slower the software, the faster the required hardware)
Not sure if it covers exactly the same terrain, but perusing the article, it seems to be the case, with a single instance being the degenerate case.
if you really want a fun drawing get a human artist to do it. it doesn't need to be complicated, for example https://www.code-cartoons.com/ is mostly just stick figures and does an excellent job
but you don't even need any of that, a mermaid diagram would have worked perfectly fine too. instead you chose to use a technology that is known to be harmful
What is unclear to me is why the decision to use a Point instance as a value or as a reference is made in the class definition rather than by the caller.
> Point[] point = new Point[10];
For the same class, I might need an array of values in one place and an array of references elsewhere within the same codebase.
Let's take a stroll down memory lane. First of all, .NET literally started as a Java copy. On top of it, a non-cross-platform one for almost two decades! After having shamed Linux for so long Microsoft finally started porting .NET to other platforms in a non-backward compatible way. A lot of .NET proponents will tell you porting from legacy .NET to .NET Core (which was renamed once again to .NET) would be a quick fix, but it isn't. For example, the shop I used to work in had some important cryptographic libraries which were very painful to port. And then, there's .NET's simplistic garbage collector, which can be quite annoying because it tries to be a one-fit-all solution that basically cannot be tweaked at all, often resulting in unresolvable latency problems. There’s a lot of other stuff, like its ghetto-like ecosystem and the insane fragmentation of GUI libraries.
I also don't get the C# praise. Over the years, it has become quite the bloated language. It feels like Microsoft tries to implement every feature possible without realizing that an enterprise language is supposed to be streamlined. Async/await? Very ugly, very annoying. Java has solved this a lot better with virtual threads and structured concurrency.
I could go on, but these "language wars" are silly and pointless. Both platforms have their pros and cons. Besides, I have a lot of bad things to say about the JVM as well, but it's nice to see Valhalla finally beocming reality. Too late for me personally though.
I do not think you can do stack allocation in Java.
> "The defining trait: no identity"
I get that this makes objects behave like primitive types. Maybe thats reason enough. But is it necessary for the performance boost and de-fluffing the objects? Seems like an orthogonal objective
> There’s a catch worth knowing about here, though: flattened data has to be readable and writable atomically (otherwise it risks “tearing” under concurrent access).
Isn't this a race condition and "undefined bahvior"..? Having to limit yourself to atomic sizes seems like a huge limitation, to accomodate what is most likely buggy code. Is all the effort only gunna help lil toy ColorRGB examples?
> The points array is a million pointers. Each pointer leads to a separate Point object lying somewhere on the heap.
Does this happen in actuality? One would assume the allocator tries to put stuff sequentially on the heap? Its not a guarantee as with these Value Types, but I'd think you could get similar-ish perf with prefetching in cache. I dunno whats happening under the hood.. But when writing Clojure apps the JVM always reserves absurd amounts of heapspace on my machine (to my annoyance). Id assume it can find some place to do contiguous allocations..
Which i guess gets me to my last question... where are the benchmarks broski? It all sounds great, but does it actually yield the insane speedups promised?
Great article, well written. But a benchmark would have been a nice "punchline"
If you have language-wars about a concept going in and out of existence, that is a hint that there is demand and the language does not properly handle the demand or when it handles it, it creates mental overload.
> Value
> Errorstates
> Null
> IoExceptions
> WeirdOsStatesNeededToHandleUpstairs
https://fsharpforfunandprofit.com/rop/As the pythons said: Get on with it!
This seems heavier? Having two representations and manually having to refer to .val or .ref?
You can argue that the extra flexibility lets you write safer (non-nullable) code but naively it seems more complex at the language level.
Saying the mental model is too hard is basically saying your userbase is stupid. This stuff is not tricky.
The ramifications for backwards compatibility is that the JVM won't have CLR features such as stackalloc (allocation of blocks of memory on stack), ref parameters (pass-by-reference of stack allocated value types), and all the other low-level/high-performance programming features available in the CLR.
What? It’s been getting better with each release. Valhalla brings features that address key problems, and they didn’t rush to it either.
Given that the JVM could already do escape analysis and allocate regular classes on the stack in certain scenarios, it's very unclear what benefit, if any, this will bring for normal processors for anything except the base wrapper types - even after implementing generic support and nullability for value types in a future JVM.
That is, the situation you are afraid of should be impossible.
No, there’s no way to know from those four lines that will happen, but we have that problem today. If Point was a record the same thing would happen.
value Point a = new Point(10, 10);
value Point b copy= a;
a.x uniq= 100;
System.out.println(b.x);
Now it's much more obvious that cloning/copying takes place, and mutating a field won't affect fields of any other objects.I've made something like this in the past. And I did it exactly because `List<Foo>` was too expensive and slow.
class FooSOA extends Collection<Foo> {
double x[];
double y[];
double z[];
Foo get(int index) { return new Foo(index); }
record Foo(int index) {
double x() { return FooSOA.this.x[i]; }
double y() { return FooSOA.this.y[i]; }
double z() { return FooSOA.this.z[i]; }
}
}If you don't have the time or put in the effort to make your article, I'm not going to spend time and effort reading it. You really don't need some generic cartoon guy hovering over your graphs, draw them in MS paint or something.
Java is generally backwards compatible, so unless you're using fat frameworks that use shady internals or known-deprecated APIs, you should generally be fine immediately upgrading to the latest LTS, possibly even non-LTS versions if you have confidence in your stack.
Mostly just hit the LTSes is what we've been doing and since about 17 it's been a pretty easy process in general.
Protip: If you ditch lombok everything gets a lot easier.
Yes. The one part of the JVM GC that can't run concurrently is heap compaction; objects that can be moved by copying and then deleting would be a huge help for that. And it would be awkward to say the object has an identity but can't be wait/notify'd, at which point you need somewhere for the monitor to go.
> Does this happen in actuality? One would assume the allocator tries to put stuff sequentially on the heap?
Yes. Of course it tries, but semantically the pointers are just pointers and the prefetcher can guess but the system still has to chase them.
It feels like an orthogonal objective and honestly arbitrary distinction, yes.
> Isn't this a race condition and "undefined bahvior"..? Having to limit yourself to atomic sizes seems like a huge limitation, to accomodate what is most likely buggy code.
I think they meant it like the appearance of atomic behavior from a java multithreading view.
> Does this happen in actuality?
Yes, it does happen. Having guarantees on this front leads to better performance.
> But when writing Clojure apps the JVM always reserves absurd amounts of heapspace on my machine (to my annoyance)
Might be a configuration problem?
Arguably flattening mostly makes sense for these only.
And yeah, you are right that allocations happen on something called a thread local allocation buffer, which is basically just a pointer bump in cost and objects allocated one after the other should be physically close in memory for the most part (though an object's creation may require a bunch of other object's creation that would sit in-between). But these have headers, so not as dense as they could be (though due to GCs being generational, they may end up actually closer in the next gen? The in-between temporary objects wouldn't survive for the most part)
-XX:MaxRamPercentage=70
But they are working on removing that: https://openjdk.org/jeps/8377305I agree. The stewardship of Java seems rather lacking - particularly when compared to that of .net, where MS etc. mostly seemed to make the correct decisions from the start.
Does Java even have any value or mindshare at Oracle nowadays? The company seems to be a datacentre/compute business at this point, with appendiges for its legacy activities and a vast overhang of debt.
I sometimes wonder if the only parts of Oracle that are still profitable are the Legal and Lawnmower divisions.
also, null markers are coming too: https://openjdk.org/jeps/8303099
Its just that they have to deliver things incrementally. This PR that introduces value classes/objects is already 200k lines long.
That said, we've been gnawing on this limb for a while...
I think you've missed what this is referring to. It isn't about null safety (which is orthogonal) but about having reference/value projections analogous to Integer/int.
What the Valhalla team ended up doing is, instead of having two projections for each type, one with identity and one without, value types never have identity and so Integer and int are synonymous, and the memory layout is determined automatically based on context and optimisation decisions. This is why the semantics of == for the primitive wrappers (like Integer) were changed, as they now don't depend on whether the "reference projection" or the "value projection" is used.
> There is no reason to reduce the optional(!) safety guarantees you can offer with the excuse of "too mentally taxing".
This is not what happened here.
But a huge mistake (IMHO) was not having nullability part of the type system. You can still do this with type erasure.
Anyway, I read your comment as "nullability isn't complex" (paraphrased) but that's not the author's point. What's complex is having a value class and a regular class of every class and you don't necessary know which one you're dealing with at the language level.
C++ is a great example of this. You can create an object ont he stack or the heap and that's really what we're talking about with that proposal. And that's a nightmare. Combined with pointers it meant you never knew if you could free something or not and that ownership had to be passed around with vague comments like "// retains ownership".
Anyway, the whole article is a great tale of how difficult it is to retrofit things later and how difficult it can be to fix mistakes later (eg java.util.Date).
that smells of AI [1], and thus lazy writing. I'm all in for using AI to help you write, but if you don't put your voice to it then there's no reason to read it.
[1] https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#...
For me, a struct in C/C# can be modified and is passed by copy while a value class can not be modified and is passed by value.
I do not think you can do stack allocation in Java.
The false dichotomy of
> A struct in C# has identity and mutation, so the semantics of copying on assignment or passing have to be precisely defined, which gives a heavier model for the programmer and less freedom for the runtime.
Doesn't really match with what they're describing. While yes, it will not have identity in a java class ref sense, it of course will still have identity in being a unique structure in memory at a certain address. This is just splitting hairs about Java nomenclature.
Please, if you write a technical blog, or anything really: Stop. Stop letting the AI write for you. Nobody wants to read this.
In this initial commit. As was made clear in the JEP, this is just the first deliverable of a huge feature that, like all Java features in recent years, is being delivered piecemeal. Obviously, the point is to flatten larger values (the mechanism is already in the JVM; what remains is exposing the intent of "I allow tearing" in the language).
Our work uses modern Java (26 w/ preview features - mainly for StructuredConcurrency), and it's fantastic. Do not regret it one bit, and that's coming from using both Haskell and Python at previous companies.
> Neglected and unloved till JDK 8, its basically been playing catch up.
These two statements are contradictory. The last Java version under Sun was in 2006. Oracle bought Sun in 2010. JDK 7 came out in 2011 and JDK 8 in 2014.
The team largely remained the same, and the main difference was that Oracle ended the neglect and funded us more, which is why Java picked up the pace after the acquisition.
> its basically been playing catch up.
Catch up with who or what? There are only two languages in the world as popular as Java or more: JS/TS, and Python. People who are saying Java is "playing catch up" usually compare it to languages that are doing far, far worse than Java. It's just that people who like certain features think that the language that has them is doing poorly despite them and not because of them. Many times I see people insist that other languages are "doing it right" (or better than Java) even though it is clear that the people who say this are in the minority when it comes to preferred features.
> So when people say "oh so its now got structs or value types of X", yes it has but that's because it has been stunted in its development due to big bureaucratic and hostile corporate processes, but its free now and is getting love through the OpenJDK family.
If anything, the opposite is the case. Managers love to see things ship quickly. It is our technical leadership - all people who were there in the Sun days - who insist we have to move deliberately and carefully and get things right. You can agree or disagree with the decisions, but comparing Java unfavourably to languages that are doing far worse is unconvincing.
Rather, what I think the vibe is because Java is not as popular as it was in, say, 2003. And it certainly isn't. But guess what? No other language is, either, because that time was anomalous not only for Java, but for the entire software ecosystem, which had never been as consolidated and unfragmented before or since.
Anyway, I wouldn't even call Java "stunted". It made choices, some reasonable, some not, and those are incredibly hard to fix later. Heck, just look at C++. Semi-compatibility with C is (IMHO) an unfixable 150 foot albatross around its neck and so many versions from C++11 onwards have simply been about making that 150 foot albatross more bearable.
I personally think treating all value classes as a single L-type in the JVM (like primitive types, basically) is a fairly neat solution to a difficult problem. But all this comes down to the original Java 2 decision to implement generics as type erasure to maintain backwards-compatibility, something that C3 NOPEd out of as a result.
C# copies C++ behavior where you can pass a struct by value or reference, and you can mark the parameter as readonly. C# also has in/out parameters. Essentially, you can program in C# exactly like you would in C++.
https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
The footgun with C# structs are that you can accidentally box them onto the heap. To avoid that you can define `ref struct`s that cannot be boxed. `ref struct`s follow the C# disposable pattern.
https://learn.microsoft.com/en-us/dotnet/csharp/programming-...
https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
The mutability difference is that part of a struct can be modified in place, which value classes can’t: the value of a complete value-class variable (or array slot) can only be modified (reassigned) as a whole. This is presumably because object references to value-class objects can be created, and those objects should be immutable so their identity doesn’t matter.
No, it will not. The design allows multiple objects to share one structure in memory across multiple records, or not have such a structure at all (see Scalarization in the article).
Again, not trying to turn this into a .NET vs Java thing, I'd have been much happier if they reached some new and interesting conclusions.
But the distinction still matters. Value-vs-reference semantics affect equality, identity, nullability, arrays, collections, boxing, and performance.
So this is not just an implementation detail. If a language hides that distinction at the use site, it increases the burden on the reader and makes code harder to reason about.
The Z80 took 3 cycles to load from memory. A register to register transfer took 4 cycles (including fetching the instruction). Only one of those cycles was instruction execution.
I think the only reasonably mainstream scenario where the CPU would be significantly slower than memory would be the serial CPU designs such as the PDP-8/s.
That said, at the time people were doing cool stuff with 8-bit CPUs, they weren't running software remotely like what we're discussing here. That would have been done on a VAX, which had instruction and data caches.
What really happened, that the article is alluding to is that memory didn't get much faster in absolute terms since the 1980s. CPUs on the other hand did.
E.g. in the 1980s we had 60ns DRAM. Today DDR5 I believe allows about 10ns random access reads best case (6X). Over the same period CPU clock speeds have increased from about 8MHz to 5GHz (600X).
The current code will help with `Integer[]`, `Char []`, etc, as well as combinations of `byte`, `char`, and `int`. Past that it doesn't really help much.
It would be fantastic if we could also flatten something like `Pair` or `Tuple`. However, even with compressed pointers, that is 64 bits, so that, plus the `null` bit, means it can't be flattened, which is a real shame. For various reasons, I have `List<Long>` in numerous places in my code, It would be great if that could also be flattened. However, since a Long is 64 bits, it _also_ can't be flattened. https://openjdk.org/jeps/8316779 would go a long way to to helping here, since then at least the null bit could be thrown away, which would allow more things to be flattened.
And then, if you want to go Wishlist land, something that would allow SSO (Small String Optimisation) would also be awesome, but that would require something akin to unions in Java, which we can _kind_ of do with sealed classes, but, since String is a final class, can't be retrofitted back into the language.
Does anyone know if Valhalla will flatten "simple" sealed classes, where every sealed class is small enough to be flattened? Since that would also be a powerful example to share.
Now, as a member of the Java team (although I'm not directly involved in Valhalla), I'm obviously biased so let me just say that both designers and fans of programming language features would do well to remember two things:
1. Opinions about features are almost never universal, even among experts, and almost each of them is about a tradeoff where different people prefer different sides. It is rare that some scientific study settles the issue.
2. These preferences are often not evenly split. Even when both sides are equally confident that their preference is the right one, sometimes 80% or 90% of programmers share a preference. The people with the strongest opinions are more often than not in the minority, because most programmers don't think so much about the programming language (nor, I would say, should they).
All of the language differences between .NET and Java fall in this "non-consensus" zone, and at least in one area I was deeply involved with, virtual thread, I can say that we thought that whatever we do we mustn't do what .NET did and that what they chose didn't work out well for them at all.
In what way? If anything Java's main developers (employed by Oracle for the most part, working on the completely open source and free OpenJDK) are extremely knowledgeable and are responsible a big jump in how fast the platform evolves. They have added proper algebraic data types to the language, delivered virtual threads and garbage collectors that decouple pause times from heap size. Like if anything, Java is at the best place it has ever been.
I was at a conference on scientific programming in Java very early on that Geoff Fox put on up at Syracuse and we had a list of requests from Sun that they didn't give us but Microsoft gave many of them right away.
On the other hand I really like Java's all-virtual approach to inheritance because the .NET model gives programmers more ways to screw up and get confused.
Both languages slipped in generics after 1.0. Java used type erasure in a way that made it so a List<String> is really a List so generics could be retrofitted easily to existing code. .NET's implementation of generics let you do more but caused a rift in the ecosystem between generic and non-generic collections.
I'd say long term Oracle's stewardship of Java has been very good. JDK 8 puts lambdas on your fingertips with a very fluent syntax that belies the idea that Java is terribly verbose. Since then Java has gotten steadily better release after release while maintaining great compatibility.
I work with people who are conservative about updates because they are worried about breaking things but for the last few LTS releases I've said "it ought to be really easy, let's give it a try" and it is really easy and we get performance improvements we can feel.
It is all about having AI on the framework, Aspire, multiple Web and Desktop frameworks all over the landscape.
Those interceptors and inline arrays via attributes instead of proper language grammar aren't that great either.
Except they're not, as I can do Integer x = null, but not int x = null. So an Integer is forced to occupy more memory, for very very unclear reasons. And this is also deeply weird - there is no other (mainstream?) language that allows null value types.
TIL that Rust has NonZeroU64 which you can combine with Optional to get the required behaviour with only 64 bits per entry. [1]
Is there a way we can request a "flag as AI garbage" downvote for articles? Or should we just flag them?
Whether you like oracle or not, this is simply not a correct description of Java's history. It was brought up by loving parents, who due to financial problems had to put Java into a foster home where she was neglected.
But later it was adopted by new, loving parents (Oracle) and she bloomed and become a healthy and stable adult.
Like, it was Oracle that completed the open-sourcing of the platform, making OpenJDK the reference implementation. They also open-sourced the previously proprietary jfr, mission control etc tools.
They also managed to keep many of the original members of the language team, which is quite rare during these acquisitions, and Java has seen a huge improvement both on the language and runtime front.
Same with MySQL, btw. "Dead" according to this site, risen from the dead under Oracle for those who actually know it.
It should work even for strings: They will surely continue to be heap-allocated, and memcmp-ing pointers (inside the new "structs") is exactly an identity comparison.
Java separates checking identity and equality for objects. == basically checks if two pointers are the same. Equality is a subjective concept based on an interface (ie equals/hashCode). So this means:
new Integer(1000) == new Integer(1000) // true, used to be false
new Integer(1000).equals(new Integer(1000)) // true
new Integer(10) == new Long(10) // compiler error, used to false
new Integer(10) == new Integer(10) // true
There's a lot going on here. The complication is that in previous versions of Java (and I'm not sure when this changed), integers below a certain value would be replaced with canonical types below a certain value. I think it was 128 but its's been awhile. This led to the difference between 10 and 1000. That's now changed, I suspect because the above comparisons are being implicitly unboxed. That didn't used to happen either. I saw this because the Integer/Long comparison used to return false and it's now a compiler error so there must be unboxing going on.You may still be able to get the old behavior through variables too.
Anyway, if value classes lose identity then == changes from pointer equality to bitwise equality. That will hopefully resolve a bunch of corner cases like this but it is a breaking change, technically.
This is often given as the defensible reason, but it's not even that true. Java 1.5/5 had several "breaking" changes in it regardless including the newly reserved 'enum' and a whole freaking memory model update.
And besides if any of your dependencies updated for all practical purposes you did, too, since you had to use a newer runtime to run their code regardless, it never really made sense to keep using an older javac out of spite
Don't be all-in. It's important for humans to be able to write for themselves, and also to stand by what's been written in their name, which is much less likely if someone/something else has done the writing.
(proofreading is another matter though.)
Good news is, Oracle extended extended extended support for Java 8 will not last forever, and eventually - if you work in a regulated industry - the company WILL have to pull the trigger.
On the other hand, "where there is muck, there is brass", so a little bit of legacy can be beneficial for some.
This is fine if you hand-roll all your code yourself, but I often use mapping libraries to lower the code footprint and the problems resulting from schema changes are subtle and fly under the radar. This is different from classes with hard construction guarantees, which Java would offer with their "integrity by default" mantra. Where you can opt out of integrity for performance benefits (which is also part of the design).
And Nullability in C# is an absolute nightmare. The type system has completely different rules for nullable types that generalize over classes and structs and there is no generic such as a "Nullable type".
It's just lots of minor annoyances that don't form a cohesive whole.
Oversimplifying a big semantic and backend change to a huge codebase on which some of the most crucial customer and government and business systems depend on, and which has to be made as seamless, correct, and performant as possible, to "they just copied .NET", just because .NET has the same functionality, is an even worse look.
It's a "HN "Dropbox is just rsync + some scripts"-style bad look.
> On June 15, Oracle engineer Lois Foltan confirmed what a good chunk of the industry had stopped believing: JEP 401: Value Classes and Objects will be integrated into the main OpenJDK repository and is targeting JDK 28.
> The change is so large that the remaining committers were asked to hold off on bigger commits during the integration. The pull request alone adds over 197 thousand lines of code across 1,816 files.
What in those paragraphs is obviously AI?
In fact, much of the software industry, which writes the software that matters to our lives the most and holds most of the value delivered by software in general - the software that processes your credit-card transactions, runs your bank, sorts your mail, routes your phone calls, manages the manufacturing of your car and the shipping of your packages, holds your healthcare information, schedules and tracks your flights, and manages your law enforcement and your government - is barely represented here because the organisations that write most software aren't software companies, and they don't tend to publish technical blogs.
For example, you might have a value class for representing (limited-precision) fractions using two longs internally, for the numerator and denominator. For efficiency trade-off reasons, you don’t want to always shorten the fraction. But now client code can distinguish 2/3 from 4/6 using ==.
Scenarios of that sort are conceivable where this actually leaks sensitive information. In any case, it creates dependencies on implementation details where you don’t want to have them.
When designing a value class, you are now in the dilemma of either always having to normalize the representation, costing performance, or having your class be a funnel for leaking implementation details.
new Integer(10) == new Integer(10) // true
Before value classes this would always be false. The only time comparing Integer objects with == could be true is if Integer object was create by going through Integer.valueOf (or obviously if they were the same object reference.) By default the cached values where -127 to 127, but that is tuneable at runtime.https://github.com/openjdk/jdk/blob/jdk-27%2B27/src/java.bas...
For example, Imagine you have an api like `void do(List<Foo> foos)`. In the erasure environment of the JVM that looks like `void do(List foos)`. From python it's pretty easy to call with a `foos = [Foo()]`. But not so much if your python implementation needs to figure out how and if it can coarse it's `List` type into a `List<Foo>` type.
The other solution is to stack allocate and pass a pointer but as i said, unlike in C#, i do not think it's possible to do that in Java.
In Go, you can stack allocate but when you send a pointer (that escapes), the compiler will heap allocate the object.
Well, it is - because they had to make it with almost perfect backwards compatibility for one of the most popular languages with trillions of lines of code produced over decades.
Sure, adding it to a new language is not hard. Adding it to Java which has primitives, generics and boxing, finding ways that seamlessly cover the differences between objects and primitives, while trying to plan for the future is hard.
As a general note, if you come to the conclusion that one of the best designer teams on Earth "basically copied what .NET did from year 1 is not a good look", then maybe your mental model needs adjusting on how these stuff works? Java has a public mailing list, you can browse through the related discussions. Implementation is the least of these things. But I can assure you they most definitely know what they are doing.
It’s when they land next part (nullability) it will shine fully - particularly on the intersection of not null and value. Alternatively if they introduce tearable semantics it will also shine - it would be possible to still optimize array of value classes, even if they are nullable (for example by having correspondent nullability mask).
So they are taking right step in a right direction. They are just trying to land this incrementally.
To me it felt a bit less like a religion and more like a language. It didn't force me to do things a particular way, quite as much. (Still more than I would have liked, though! After all, it's called that[0] for a reason :)
[0] https://www.reddit.com/r/ProgrammerHumor/comments/ddc4b0/mic...
That goal is an ideal and can't be reached perfectly. Converting a type to a value type will break clients that synchronize on them, or rely on identity for some reason. But such cases are rare, and can be weighed up on an individual basis when making the decision about whether to do it. Storing things in a nullable variable on the other hand is very common and changing the rules to prevent it would make every such change a source incompatible breaking change.
Also, apparently, shifting a negative number to the left is UB in C.
Luckily we have Valhalla, which is an admission that Gosling was partially wrong, and programmers who want to have an unsigned nullable non-zero 64-bit integral value type can just make one, and not have to pay outsized memory costs to do so.
If we're done paying homage to Gosling, can we get operator overloading for our fancy value types please? I have no idea if this is on the radar for Valhalla.
But you don't know which parts of it are true.
There is a lot wrong with that: complexity, bloat, and slowness.
> But now client code can distinguish 2/3 from 4/6 using ==
That's a great way to obfuscate code. Not a good idea. The right way to do the comparison is, just make a function called CompareRational().
On June 15, Oracle engineer Lois Foltan confirmed what a good chunk of the industry had stopped believing: JEP 401: Value Classes and Objects will be integrated into the main OpenJDK repository and is targeting JDK 28.
The change is so large that the remaining committers were asked to hold off on bigger commits during the integration. The pull request alone adds over 197 thousand lines of code across 1,816 files.
Before we pop the champagne, though: this is preview, disabled by default, and, as Brian Goetz was quick to cool everyone down, “only the first part of Valhalla.” Goetz added a great observation that the “they’ll never ship it” crowd will now smoothly switch over to “but they didn’t ship the most important part” (and a joke has been going around the community for years that we’ll sooner end up in Valhalla ourselves, the Norse-afterlife one, than the project ships).
You have to earn your own haters.
So this is a good moment to tell the whole story. This issue is one big deep-dive, written on the assumption that you’ve never followed the work on Valhalla before: from the 2014 problem, through the evolution of ideas (a fair number of which ended up in the trash), all the way to what exactly we’ll be getting our hands on in JDK 28. Brew yourself a coffee. I’ve been sitting on this edition for a long time, saving it for exactly this occasion.
The slogan Valhalla has carried from the start is: “codes like a class, works like an int.” In a single sentence it captures the whole point of the project: we want to write normal, readable classes with methods, constructor validation, and sensible field names, but we want the JVM to be able to treat them as efficiently as primitives.
To understand why this is a problem, you have to go back to Java’s foundation. In this language, with the exception of the eight primitives (int, long, double, boolean, and the rest), everything is a reference type. When you write Point p = new Point(1, 2), the variable p isn’t a point. The variable p is a pointer, a coat-check number: somewhere on the heap sits an object, and you’re holding a slip of paper with its address. Every time you want to read a field, the JVM has to “go to the coat check,” performing a hop through the pointer (pointer indirection).
For a single object, that’s nothing. The problem starts at scale. Every object on the heap has its own header (a dozen-or-so bytes of metadata: among other things, so the JVM knows what type it is and whether anyone is synchronizing on it). Incidentally, this is exactly the problem Project Lilliput has been tackling lately, helping to shrink object header sizes. But header size isn’t everything. Every object has to be allocated, and later garbage collected. And since objects are scattered across the heap, an array of a million Points is in practice a million slips of paper pointing at a million boxes strewn across the whole warehouse.
Brian Goetz, in his “State of Valhalla” documents, calls such a memory layout “fluffy”: puffed up, bloated. What we dream of is a dense layout, one where the data lies side by side.
Why does density matter? Because the hardware changed faster than Java did. In 1995, a memory access cost roughly the same as a CPU operation. Today the CPU is two orders of magnitude faster than main memory, and the whole gap is bridged by the cache. The processor reads memory in chunks called cache lines (usually 64 bytes). If the data lies densely and in order, one such chunk brings in a ton of useful values at once. If we’re hopping across pointers, every access risks a cache miss, and that can be a hundred times slower than a hit. This is locality of reference, and it’s the real stake in this whole game.
“But the JVM has escape analysis,” someone sharp will say. True: the virtual machine can recognize that some object never “escapes” beyond a local fragment of code, and then it doesn’t allocate it at all. From the programmer’s point of view it looks as if the object exists, but in reality its fields get spread out into ordinary variables or CPU registers. In the best case, the cost of allocation and the later cleanup by the garbage collector drops to practically zero.
The trouble is that this optimization is unpredictable and fragile. It works only when the JIT compiler can trace the object’s entire flow with high confidence. But all it takes is for the object to land in a field of another class, get stored in an array, get passed into a more complex method, or appear beyond the boundary of code the JIT can analyze, and the whole trick stops working. The source code stays identical, but the performance behavior can change dramatically.
This is precisely why experienced JVM programmers treat escape analysis as a nice bonus, not a project’s foundation. If an application’s performance depends on whether a particular JIT version manages to apply this optimization, it’s very easy to fall into the trap of hard-to-predict regressions. A minor refactor, a JDK update, or a change in code structure can send objects back onto the heap, and the costs of allocation and garbage-collector work return in full force.
That leaves the brute-force option: give up on objects and encode the data by hand. Instead of a Color class, hold three bytes r, g, b. This isn’t just an academic example. The approach has been used for years in game engines, graphics libraries, image-processing systems, databases, analytics engines, and HPC code, where every byte of memory and every allocation matters. The trouble is that the speed comes at the cost of safety and readability. We lose names, private state, validation, and methods. JEP 401 gives a simple example: a developer working on “raw” color bytes might mistakenly interpret them as BGR instead of RGB, swap red with blue, and quietly corrupt the entire image. A class wouldn’t have allowed it. A bare int? Sure it would.
And it’s exactly this dichotomy, either convenient classes, or fast primitives, that Valhalla is trying to erase.
Officially, Project Valhalla started in 2014. James Gosling described it at the time as “six PhDs tied into a single knot,” and that was no exaggeration. Interestingly, the idea is older than the project itself: Java’s creators wanted value types as early as the first version of the language, but in 1995 they gave up, because the problem was too hard.
The goal was set ambitiously: to restore alignment between the programming model and the performance characteristics of modern hardware. In other words, to let programmers declare their own types that are flat and dense in memory like primitives, but look and behave like normal classes.
Easier said than done. Over the following years the team built five different prototypes, each probing a different aspect of the problem. And this is where the most interesting part of the story begins, because to appreciate Valhalla’s current shape, you have to see how many ideas died along the way.
The early prototypes went in a direction we now call “Q World.” It assumed that the new value types were a fundamentally different beast from objects, with separate type descriptors, separate bytecodes, and separate top types, exactly like primitives. Sounds logical: if they’re supposed to work like int, let them be represented like int. The trouble is that such a separation flooded the entire JVM type system with extra complexity: everything had to be done in two variants.
The breakthrough came with a prototype christened “L World” (roughly around 2019). The name comes from the fact that value types started sharing the same “L carrier” (the L descriptor, the same one the JVM uses for ordinary references) with object references. The team expected such a unification to be too hard, and yet, to their own surprise, it worked without major compromises and incidentally solved a whole pile of problems from the earlier rounds.
L World produced one more fundamental “aha” that shaped everything that came after: the language model and the JVM model don’t have to overlap one hundred percent. L World is the right model for the virtual machine, but you can treat it as a translation target and offer the programmer something more convenient in the language. This separation of layers turned out to be the key to the rest of the project.
That’s also when the plan to split the work into two phases crystallized: first value classes (still called something else at the time, more on that shortly), and only then, specialized generics. We’ll come back to generics in section 6, because that’s a separate, longer treatise.
If you’ve ever tried to read about Valhalla and bounced off a wall of contradictory terms, it’s not your fault. The naming changed several times here, and not cosmetically: behind each name change stood a change in the model. Let’s trace it, because it’s the best illustration of how this feature was designed.
Stage 1: value types: The earliest term. Vague, because it wasn’t yet clear what exactly these things were supposed to be.
Stage 2: inline classes: Around 2019–2020 a distinction settled in that has survived to this day in its essence: classes split into identity classes (the ones with identity, that is, everything we’ve known until now) and the new inline classes (without identity). That’s when the slogan “codes like a class, works like an int” was coined, and the basic constraints were set: inline classes are final by default, their fields are final, you can’t synchronize on them.
Stage 3: “primitive classes” and the two-projection model. And here it gets interesting, because this is exactly the idea that got significantly cut down. In the 2021 “State of Valhalla” documents, Valhalla promised three things: value objects, primitive classes, and specialized generics. The idea for a “primitive class” was that a single type would have two projections: a value variant (flat, never null, behaving like a primitive) and a reference variant (a box that allows null). Across various iterations this was written as Point.val/Point.ref, and later they experimented with the Point! and Point? syntax.
The model was powerful, but also mentally heavy. A programmer would have to juggle two forms of the same type day to day and understand when a conversion between them happens. The team, faithful to the lesson “simplify the model for the user, even at the cost of the performance ceiling,” ultimately dismantled this dualism.
Stage 4 (today): “value classes” and “value objects.” The current JEP 401, authored by Dan Smith (reviewer: Brian Goetz), puts it simply. There’s one new thing: a value class, declared with the value modifier. Its instances are value objects: objects without identity. And (this is key) a value class is still a reference type. The whole tricky business of non-nullability has been split off into a separate, optional JEP (Null-Restricted Value Class Types), which we’ll get to. So instead of one complicated concept we have two simple, orthogonal ones: “does it have identity?” and, separately, for later, “does it allow null?”
Worth remembering, because if you come across an older article (or Baeldung describing “primitive classes” as a separate mechanism), you’re reading about an outdated model. In the OpenJDK canon, “primitive classes” in that sense no longer exist.
More things fell along the way. The original “Value Objects” JEP draft was withdrawn and replaced by JEP 401. The original “Universal Generics” draft also went back for rework. JEP 401 is accompanied by JEP 402: Enhanced Primitive Boxing (also preview), plus a whole series of early-access builds (LW1, LW2, LW3…) and talks from the JVM Language Summit, among them Frédéric Parain on heap flattening and Daniel Smith on the new object-initialization model.
The moral of this section is this: twelve years wasn’t twelve years of “writing code.” It was twelve years of rejecting ideas, until the one that can actually be maintained was left.
Let’s get to specifics. Here’s exactly what we get.
Declaration. You create a value class by adding the value modifier:
value class USDCurrency implements Comparable<USDCurrency> {
private int cents; // implicitly final
public USDCurrency(int dollars, int cents) {
this.cents = dollars * 100 + cents;
}
public USDCurrency plus(USDCurrency that) {
return new USDCurrency(0, this.cents + that.cents);
}
// dollars(), cents(), compareTo(), toString()...
}
It can also be a value record. The rules: all instance fields are implicitly final, methods may not be synchronized, the class is final by default (or it can form a hierarchy composed of value classes and abstract value classes), it can’t inherit from a class with identity, but it happily implements interfaces. Beyond these constraints, it’s an ordinary class.
The defining trait: no identity. This is the crux. An ordinary object has identity: two separately created new Point(1,2) are two different objects, even if they have identical contents. A value object has no identity, just as there aren’t two “different” fours of type int. From this flow all the consequences:
== changes meaning. Until now == compared identity (whether it’s the same address). For value objects, == checks substitutability: whether both values are the same class with the same fields, compared recursively (primitive fields bit by bit, object fields again via ==). That’s why new USDCurrency(3,95) == new USDCurrency(3,95) returns true. That’s good news: it ends the famous confusion with == on Integers. But careful: == looks at internal state, which isn’t always what the object represents, so for “is this the same data” comparisons keep using equals.
synchronized throws. There’s nothing to synchronize on. An attempt ends in IdentityException. When you need to force identity, you have the new helpers Objects.requireIdentity and Objects.hasIdentity.
And now the most important conceptual trap: value objects can STILL be null. This surprises everyone who thinks “value = like a primitive = never null.” In the JDK 28 model, value class is a reference type, so USDCurrency d = null; is perfectly legal. Non-nullable types (with a null restriction) are a separate, future JEP. They’re not in JDK 28. We’ll come back to this, because it’s not a detail: it’s the key to full performance.
JEP 401 gives the JVM freedom thanks to which value objects can be optimized in two main ways.
Scalarization is a JIT compiler technique. A reference to a value object gets “broken down into its prime factors,” reduced to its essence, the set of fields, with no wrapping. Instead of passing a pointer to Color, the JIT simply passes three bytes r, g, b (plus one flag bit indicating whether the reference isn’t null). Such an object is in practice free: no allocation, no work for the GC. It’s a bit like escape analysis, but much more predictable and far-reaching: it works even across the boundaries of method calls the JIT didn’t inline. The limitation: scalarization usually won’t work when a variable has a type that is a supertype of the value class (e.g. Object or, importantly, an erased generic parameter). Then the object has to be materialized on the heap.
Heap flattening is the second mechanism. The object’s essence gets encoded as a compact bit vector and written directly into a field or an array cell, without a pointer to another place in memory. This is exactly where density and locality are born.
There’s a catch worth knowing about here, though: flattened data has to be readable and writable atomically (otherwise it risks “tearing” under concurrent access). On typical platforms, “small enough” today means as little as 64 bits, including the null flag. That’s why many small value classes will flatten beautifully, but a class with, say, two int fields or one double may not fit in an atomic write and end up as an ordinary object on the heap anyway. In the future, 128-bit encodings will arrive, and the aforementioned JEP about null-restricted types will allow flattening larger classes in exchange for giving up the atomicity guarantee. This is exactly the moment where non-nullability stops being cosmetics and becomes a lever for performance.
Remember the age-old cost of boxing, wrapping int in Integer? In the new model, the wrapper classes themselves become value classes (when preview is on, Integer, Long, Double, and company lose their identity). Since the box no longer has identity, the JVM can scalarize and flatten it. The effect: Integer[] starts approaching the efficiency of int[], and the boxing overhead, to quote JEP 401, shrinks dramatically. The accompanying JEP 402 (Enhanced Primitive Boxing) goes further and smooths out conversions between primitives and their boxes, opening the road to writing things like List. But that’s a separate, still-maturing piece, so don’t assume it’ll roll in complete alongside 401.
This is where the effect shows best. Instead of holding a million pointers to a million scattered objects, a Color[] array can store directly flattened, 32-bit encodings of successive colors (again: plus a null flag). From a memory standpoint, such an array starts to look and act like a plain int[]: a contiguous block of data the processor sweeps through sequentially, cache line by cache line.
For all of this to work, some really deep foundations were moved: the new value modifier; strict construction rules (all fields must be set before anything gets to see the new object, in practice before the super() call, so that a “mutation” of final fields can never be observed); the redefinition of == as a substitutability test; adding a value-object check to the reference-comparison bytecode (acmp); the scalarization and flattening machinery; IdentityException; and the migration of existing “value-based” classes. In short, this isn’t syntactic sugar. It’s a rebuild of an assumption that had been true in Java since 1995: that every object has identity.
Let’s take the simplest possible case and trace it so it’s clear even without knowing the JVM’s internals.
Before Valhalla:
final class Point { // an ordinary class with identity
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
What’s happening here in memory? The points array is a million pointers. Each pointer leads to a separate Point object lying somewhere on the heap. And each such object is not just its two ints (8 bytes), but also a header (another dozen-or-so bytes of metadata). These objects are scattered: the allocator created them at different moments, in different places. When you iterate over the array and sum the coordinates, for each point the processor has to: read the pointer from the array, jump to the indicated address (risk of a cache miss), read the fields. A million times. This is exactly that “fluffy” layout from section 1.
After Valhalla:
value class Point { // a value class without identity
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
The difference in the code is exactly one word: value. But the difference in memory is fundamental. The JVM can now store the values themselves in the array, laid out densely one after another: 8 bytes per point (plus a possible null flag), in a contiguous block. No headers per element. No pointers. No jumping around the heap.
[IMAGE: the same Point[] array in two variants: “before” (an array of arrows → scattered boxes with headers) and “after” (a uniform strip of number pairs)]
When you now iterate over the array, the processor reads the data sequentially. Each 64-byte cache line immediately brings in several complete points. Summing a million coordinates running at memory-bandwidth speed, instead of choking on misses. On data-intensive code that can be a difference of multiples, not percentages.
And, most important for maintainability, you didn’t pay for it with abstraction. Point is still a class: it has a name, it has a constructor, it could have validation (if (x < 0) throw ...), it could have methods. You don’t have to, like before, split points into two raw int[] xs and int[] ys arrays and pray you never mix up the indices. You got the density of a primitive and the readability of a class. That’s the whole of Project Valhalla in a single example.
This is the second half of Valhalla, and honestly the harder one. Let’s start with the source of the problem.
Java implements generics through type erasure. In practice: List and List are, at runtime, the same ordinary List, and the type parameter T is erased to Object. This often gets mocked, but it’s worth knowing it was a deliberate, defensible decision, not laziness. Erasure gave Java gradual migration compatibility: you could take an existing, non-generic class and make it generic without breaking a single existing source file or compiled class, and clients could migrate right away, later, or never. In 2004, when Java already had a huge codebase, the alternative (”here are generics, but throw out all your libraries”) would have been a terrible deal. Today it would be even worse.
The trouble is that erasure clashes with Valhalla exactly where we’d care most about performance. Since T erases to Object, a value object put into a List has to be materialized as an ordinary object on the heap. In other words: your beautiful, flattenable Point in a generic collection loses its flattening: the container holds references, not flat values. All the density you gained in Point[] evaporates in ArrayList.
The repair plan is, like all of Valhalla, two-phase:
Phase 1: Universal Generics. This is a change at the language level: it lets type variables also cover value types, that is, so you can even express something like ArrayList or List. For now still through erasure. The programmer will feel it mainly as new compiler warnings about “null pollution,” because a field of type T starts out as null by default, even if T is a value type. Addressing these warnings makes APIs “specialization-ready.”
Phase 2: Specialized Generics. These are the future JVM extensions that will generate heterogeneous, specialized class layouts for concrete type arguments (in the project’s jargon: species and type restrictions). Only then will ArrayList really be backed by flat memory. This is the piece that’s still largely research work.
The consequences for libraries and frameworks are enormous, and that’s exactly why it’s happening gradually. Ultimately, collections, streams, and entire APIs can become flat and allocation-free over value types. But library authors will have to address the new warnings and design with specialization in mind. Let’s be honest: the original Universal Generics draft went through rework, and the full reward from specialization is a matter of future releases. JDK 28 doesn’t bring it.
Let’s gather this in one place, because it’s easy to get lost between “it’s here already!” and “it’s not here yet.”
What got accepted: JEP 401 (Value Classes and Objects) as a preview feature, targeting JDK 28 (release in March 2027), with integration into mainline planned for roughly July 2026. 197 thousand lines, 1,816 files, coordination on Lois Foltan’s side, the request to other committers to hold off on large changes. Disabled by default: to play with the syntax, you have to flip on --enable-preview.
What actually reaches users: the ability to declare value class and value record; migration of existing “value-based” classes in the JDK (among them the primitive wrappers like Integer) to value classes under preview; scalarization and flattening for qualifying classes; cheaper boxing.
What can still evolve, and what’s NOT in 28: null-restricted types (non-nullable); full specialized generics; 128-bit encodings; a fully mature JEP 402. And the syntax itself, because this is preview, and that’s exactly what’s expected of it: that it can change from release to release in response to feedback. Hence Goetz’s quote about “only the first part.”
How it might affect the ecosystem: for high-performance Java (data, vector computation, ML, gamedev, finance, codecs) this is the path to dense data without giving up abstraction, which is exactly what some of these domains have been waiting years for. Frameworks and libraries will start migrating their value-based classes. You’ll also have to watch out for a long tail of behavioral surprises around == and synchronized in code that (knowingly or not) relied on identity. And one more thing worth keeping in mind when planning: JDK 28 is not an “LTS” release: the next LTS will probably be JDK 29 in September 2027. So most companies will meet a stabilized Valhalla only at the LTS, but it’s precisely the preview in 28 that kicks off the real feedback loop with actual code. If you’re working on something that could benefit from this, now is the moment to start experimenting and submitting feedback.
Why do I call this one of the biggest changes in the platform’s history? Because Valhalla doesn’t bolt yet another feature onto the language; it moves its deepest assumption. “Every object has identity” had been true in Java since 1995; it’s the foundation everything else stood on. Letting the programmer opt out of that assumption (choose which objects need identity and which don’t) isn’t a refactor, it’s a shift of the foundation. And that’s exactly why it unlocks a whole decade of further work: unifying primitives and objects, specializing generics, denser collections, faster numerics.
At the same time, and this is the honest version of the headline, “Valhalla rolls into JDK 28“ is a half-truth. It’s the first, preview step of a multi-stage rollout. But it’s precisely this team’s discipline (simplify the model for the human, do the hard performance things as optional) that’s the reason it took twelve years, and the reason it can be shipped at all now.
For us, as programmers, one thing to take away matters more than the syntax: internalize the distinction identity versus value. The rest (==, flattening, generics) are consequences of that one distinction. And the early-access builds are already here: you can touch this on your own code before your competitor does.
1. Is value class just a record? No, they’re two orthogonal decisions. record means “I give up separate internal state” (content = components). value means “I give up identity.” You can have any combination: an ordinary class, a record, a value class, and a value record.
2. Can I compare value objects with ==? Yes, but == now means something different: substitutability, i.e. a comparison of all fields (recursively), not the address in memory. For the question “do they represent the same data,” it’s still usually better to use equals, because == looks at internal state, which isn’t always equal to the represented state.
3. Can a value class be null? In the JDK 28 model, yes. value class is still a reference type. Non-nullable types (with a null restriction) are a separate, future JEP, and they’re the ones that will unlock flattening of larger value classes. They’re not in JDK 28.
4. Integer becomes a value class, won’t that break my code? In most cases, no. Binaries still link, and the only new compilation errors are attempts to synchronize on such a type. The changes you might notice concern code that depends on identity: == on Integers will start comparing by value, and synchronized (someInteger) will stop working. If you relied on either of those, it was fragile code anyway.
5. Will I get a fast, flat ArrayList? Not yet. Because of type erasure, objects in a generic collection are materialized on the heap. Flat generic collections require universal and specialized generics: that’s the future. In JDK 28, flattening works directly for fields and arrays of a value type, e.g. for Point[].
6. How is this different from struct in C#? A struct in C# has identity and mutation, so the semantics of copying on assignment or passing have to be precisely defined, which gives a heavier model for the programmer and less freedom for the runtime. Value objects in Valhalla have no identity, and the way they’re laid out in memory is left to the JVM’s discretion. A simpler model for the human, more freedom for the machine.
7. Wasn’t escape analysis doing all of this already? As I already mentioned, partly. Escape analysis can avoid allocating an object when it proves the object doesn’t depend on identity, but it’s unpredictable and doesn’t help when the object lands in a field, in an array, or “escapes” beyond the optimization’s reach. Scalarization of value objects is predictable and reaches much further, including across method-call boundaries.
8. Do I have to rewrite code to benefit? For your own classes, it’s usually enough to add the value modifier to those that represent “simple domain values” and don’t rely on identity; the migration is mostly compatible. Some of the gains you’ll even get for free, because it’s the JDK migrating its own classes (like the primitive wrappers).
10. When will I see full Valhalla, with generics, non-null types, and the whole rest? In future releases. The team ships it incrementally: JDK 28 is the first preview of value classes. The full story (specialized generics, null-restricted types, 128-bit encodings) will spread across many releases and will most likely stabilize only around the next LTS.
PS: You’ll find the early-access builds at jdk.java.net/valhalla, and that’s probably the best way to form your own opinion faster than I can write another issue on the subject.
No posts
In the current setup will a Pair Value Type be a compiler error, or will it silently just have bad perf?
Curious if you think fibers vs async/await is still in this zone (amongst experts). It seems fibers are objectively better. But I'm no expert*
No they haven't. E.g. they added a class that superficially looks like Option but subtly breaks the rules that Option is meant to follow, ensuring that no-one can ever manage to migrate existing codebases away from using `null`.
Yeah. Even when they add new grammar nowadays, it's always just something that trivially sugars away into previous grammar (see: records, `with` clones, extension properties, required, etc).
The moment they need something that it's slightly more complex... Out of scope. Even when it's completely necessary for the thing to be useful in practice.
For example, they added `required`, `record`s and property initializers, giving us good reasons to write `new Foo { A = a, B = b }` instead of `new Foo(a, b)`. A and B must be positive, so you'd write:
public required int A { get; init => field = value > 0 ? value : throw ... ; }
public required int B { get; init => field = value > 0 ? value : throw ... ; }
This is pretty standard C# code that you might see in an example for records.But then the requirements change: A and B must be positive, or they must both be zero at the same time.
This cannot be expressed at all with initializers. You simply cannot add code that runs after all initializers are called. You're stuck chasing every single initialization of Foo and using a constructor or factory method instead. Shipped it as a public API? Too bad. Should have seen it coming!
The new features are filled with this sort of thing. As if Microsoft never used them beyond the most basic examples. Or maybe they did, and explicitly chose not to fix it and solve later.
Part of the reason for that is that Java is older. https://en.wikipedia.org/wiki/C_Sharp_(programming_language)...:
“In interviews and technical papers, he has stated that flaws in most major programming languages (e.g. C++, Java, Delphi, and Smalltalk) drove the fundamentals of the Common Language Runtime (CLR), which, in turn, drove the design of the C# language.”
Also, some of Java’s design warts may be there because Java was initially envisioned for much smaller devices.
Second mover advantage.
-Java always has an API, .NET is about extending an existing application (Servlet API vs IIS)
-Java has a nicer IO as .NET has bidirectional streams (You can't wrap streams in .NET).
-Linq is nice but has a huge caveat: if a Linq provider does not implement it fully to falls back to the .NET collections. So trying to 'Skip' and 'Take' on a ActiveDirectory will fall back to collections in memory and cause a crash on a huge AD in production (Yes had the pleasure).
-Java's Eco-system is way bigger.As a proportion of all easily crawled text on the internet, a lot of it will be random marketing copy. That influenced the writing style of early AIs, and since then everyone has trained at least partially on transcripts from every other AI chatbot
Now, one can argue that this is just smoke and mirrors with type erasure and it is but you can already put a Date into a List<Point> if you're so inclined because the JVM doesn't know the difference, hence type erasure. So this is no different.
I'm no JVM expert but from reading the article it seems like the chosen solution for value classes is to treat them all as a single L-type in the JVM where each primitive type is its own L-type. If I read the correctly, it means that if you have a Point value class then on the JVM level you'll be able to stuff any value class into there if you're so incline, just like with List<Point>.
Obviously we need to be concerned with fuzzing (moreso in C++) but here really we're just trying to have sensible defaults that aren't guaranteed because we can't design the language how we want from the ground up without making a new language.
Oh and there is a prosopal for this [2]. Personally, I prefer the Hack version.
People care about provenance a lot.
Whether it’s a drawing my daughter did of her mother, a Picasso napkin sketch, a worn 1960s Stratocaster, or an blog essay, the provenance is value on top of the correctness of the item.
« The pull request alone adds over 197 thousand lines of code across 1,816 files. »
I noticed that both Claude and GPT are fond of those kind of stupid accounting statements that don’t mean a lot in and of themselves, but look impressive in a « wow numbers » way. Which is kind of ironic since counting remains one of their weak points
To see why, consider that to do any useful work, data from different objects (also from different types) has to be combined. To be able to do that in the OOP framework, the encapsulation has to be unwrapped. That's why Java code is littered with getters and setters that don't do any useful work at all, they just make it too painful to get any real work done.
Again, there is a place for objects and implementation hiding, but it's at the highest levels of an architecture where different components get integrated.
Serious question: I remember the old installer, six billion devices or whatever. I’ve heard about Java ME, old set-top boxes and DVD players, etc.
But how much of that is active today. I can’t say I’ve ever seen a job listing for an embedded Java developer or even Java ME in my entire career. Are people actually still using it?
> By default, Java maintains a cache of Integer objects for values between -128 and +127.
[1]: https://stackoverflow.com/questions/3130311/weird-integer-bo...
[2]: https://dev.to/marzuk16/understanding-integer-caching-in-jav...
"All differences are opinions....except what .net did. Those are wrong!"
I personally think Java continues to to waste a lot of time and come to a slightly more verbose and worse solution again and again.
Structured concurrency is like c# async/await with less sugar. Streams are just LINQ but worse.
It's probably exactly because of the "not like .NET! We must be different to explain being late" mentality.
legacy .NET to .NET Core (which was renamed once again to .NET)
It was always .NET, only that new one had 1 till 4 had additional "Core" to clarify any confusion that could come from having same numbers as old. here's .NET's simplistic garbage collector ... it tries to be a one-fit-all solution that basically cannot be tweaked at all
Definitely tweaking GC is not a thing in .NET land but it is far from "cannot be tweaked at all".The reality, and I can see this on my bubble, is that the .NET shops are mostly former Microsoft shops now saving Windows licenses by deploying on Linux.
Stuff like MAUI remains pretty much constrained to former Xamarin customers.
Thus minimal APIs, aspire, Blazor, and whatever comes up to support those use cases first.
There are some podcast interviews from David Fowler and Maddy Montaquila where they touch the adoption issue among newer generations.
How do you expect this to work then? If the provider is bad, blaming LINQ for it makes no sense...
You either have a high level of abstraction and possible performance pitfalls - or a low level of abstraction, and also performance pitfalls since the code is less modular, more coupled and harder to read.
LINQ can in many cases improve performance significantly in large applications when used properly, since it avoids N+1 query problems due to implementation hiding/modularity, and allows composing parts of queries across different vertical subsystems of the application (vs. each subsystem doing its own query and then joining them with more boilerplate).
Nothing in Java compares to this. jOOQ and Hibernate (and the rest in the ORM ecosystem) are pale shadows, exactly due to lacking language features (such as reified expression trees), and even then, they only work with databases.
Second, working in C# felt clunky, as if every other thing was done to check the checkbox "done" and the author called it the day once it sorta kinda worked. There was some additional syntactic sugar in that language that was nice, but it did not made that much difference in practice and I don't miss it after coming back to java.
Third, I found the obsession with bashing java by people who have no idea how java projects look like and which problems they have annoying.
I don't think this is true anymore since ASP.NET Core. While you can still run under IIS but it's a more typical reverse proxy setup instead of running inside IIS.
> You can't wrap streams in .NET
You've always been able to wrap streams in .NET so I'm not sure what you mean by this
At what cost? A key benefit of value types is improved performance but AFAIK Valhalla doesn't even let you pass them by reference. Efficiently passing them through registers is great but won't help you out with larger value types.
These people may point out that languages become more or less successful not because of the things these people care about but because of other factors. And they're right, but then the question is, shouldn't a smart product team focus more on the things that actually matter more to more people?
Programming languages are tools, and so their value is not intrinsic, but comes from the value of the software they're used to create. Now, some people claim that Java's success is largely the result of it being one of the most hyped languages of the late 90s and early 00s, alongside VB, Delphi, FoxPro, and C#. But this claim doesn't stand up to even the slightest scrutiny.
It felt like magic, maybe too much magic, but so useful nevertheless.
No, it was not. What's called .NET now used to be .NET Core. And then there's .NET Framework which was commonly known as .NET.
> "cannot be tweaked at all"
Are you serious? Not only does the JDK have multiple GCs for different use cases (Serial, Parallel, G1, ZGC, Shenandoah), they have very refined tuning settings (https://wiki.openjdk.org/spaces/zgc/pages/34668579/Main#Main... / https://docs.oracle.com/en/java/javase/25/gctuning/garbage-f...). What does .NET let you do with the GC? Set the hard limit? Maybe turn on/off concurrent collection? That's not tuning, that's triviality.
As to value types and null, I'm not sure about the current picture, but the general idea is that you declare what semantic properties you want - identity or not, nullable or not, tearable or not - and then the compiler picks the best technical in-memory representation for each use. For example, the compiler could choose not to flatten variables that could be null in the heap but to flatten them in the stack. That's the general idea, but I'm not sure about the details, some of which may yet change.
More generally than just Java, nullability is often a property not of a type but of a variable. For example, in C, an int may not be null, but a pointer to an int may be. Now, in C, `int` and `int*` are two different types, but that's exactly a distinction that the original projection-spit design made and we wanted to avoid. But you could still end up with a variable that could hold either an integer or a null and another that may hold an integer but not a null, only this is separate from the reference/value projection, which combines both identity and nullability (in C, `int*` is not only nullable, but also has identity).
The stdlib's Option type predates this language update by a long shot, so it doesn't use sealed classes, but it is now possible to have the usual FP "Maybe" type in Java:
``` sealed class Maybe<T> permits Some, None { record Some<T>(T obj) {} record None() {} } ```
(You will probably have to write Maybe.Some and I might have messed up the generic syntax as I wrote it on my phone, but that's mostly how it looks)
An alternative solution to that of fibers to concurrency's simplicity vs. performance issue is known as async/await, and has been adopted by C# and Node.js, and will likely be adopted by standard JavaScript. Continuations and fibers dominate async/await in the sense that async/await is easily implemented with continuations (in fact, it can be implemented with a weak form of delimited continuations known as stackless continuations, that don't capture an entire call-stack but only the local context of a single subroutine), but not vice-versa.
While implementing async/await is easier than full-blown continuations and fibers, that solution falls far too short of addressing the problem. While async/await makes code simpler and gives it the appearance of normal, sequential code, like asynchronous code it still requires significant changes to existing code, explicit support in libraries, and does not interoperate well with synchronous code. In other words, it does not solve what's known as the "colored function" problem.
Regarding Swing, virtual threads are "just" threads so no reason they (and structured concurrency) can't be used.Thing was a lot of Microsoft APIs for GUIs and whatnot used the List and if you wanted to use the List<X> you had to copy the list or make a wrapper or something. You might say, "just use the List" but at that point (circa 2008) I had to also use the List<X> for some API so I always had to do some conversion.
Java is more used than C#, they can wait before delivering a new feature (given their leader position) but cannot deliver a flawed implementation that would stay in the language forever. Glad to have virtual threads and the backward compatibility that comes with it instead a Async version of sync methods + async and await keywords all over the code and Task as a return type in my interfaces methods to allow implementations to do non blocking I/O calls if they need.
I use Java and C# and appreciate them both.
Having reified generics in the CLR just lets you store more type information. There isn’t much of a trade off for CLR end-users.
Compare this to the constraints and workarounds that Kotlin and Scala have due to type-erasure on the JVM.
I'm not sure the no-tearing rule is particular helpful either. Like, this is something folks get wrong all the time in regular java. There's plenty of places where we use unsynchronized classes and expect synchronization to occur in the containing class or other explicit lock. If atomic operations are a requirement, and non-atomic value types get turned into references, then value types seem pointless.
I fear we're getting something called "Value types" with none of the actual benefits of value types. Like "we heard you want something called value types so here you are". No, we wanted a way to declare arrays of structured values without having to deref pointers, or to store structures inline as a field within an object. What I've read seems to be not that unless the structure's total size is 63 bits...
Then, when WinForms or WPF wanted a list, you could just give it a List<T> instance, and it would talk to it via IList, boxing and unboxing if necessary.
What you describe happened in the other direction - if you had, say, a generic method operating on IEnumerable<T> or IList<T>, and wanted to pass it a WinForms collection. WinForms generally defined strongly typed collection classes on a case by case basis, but none of them implemented the new interfaces. It was there where you had to wrap things, most often using AsEnumerable<T>().
For instance I might write this "The pull request alone adds nearly 200 thousand lines of code spanning almost two thousand files", or even better just "The pull request alone adds nearly 200 thousand lines of code" because really who cares how those are broken up into files.
By necessity a lot of people write very similarly to how llm do
Weird but true, and quite obscure.
Years before the autoboxing/Integer.valueOf() caching stuff (and before generics), (I) used to have IntegerProvider that did similar stuff to higher ranges. Personally, I have considered autoboxing on integers net-negative for Java
The creator of Scala disagrees: https://youtu.be/Xn_YpUtXWT4?t=850
People really misuse/misunderstand this term: Java objects are passed by their pointers ("references") being copied.
The alternative is pass by reference, which is done by e.g. c++, rust, who actually have references (Java doesn't). A good litmus test is whether you can write a swap method that actually changes your local variables.
I do not know how this is called.
That's materially distinct from Java's model of basically dynamic loading already compiled class files. Though class files do have "editions", and there are extra code to deal with different versions. But still, it should be possible to e.g. send a new value class to an old library's class that has never heard of them, and that should just work.
Coming from someone claiming:
"language wars" are silly and pointless.
You are just so much in language wars I am just definitely ending the conversation.I'm going to hard disagree here. And the syntax proposed in the Null-Restricted Value Class Types JEP is a major step backwards.
I want to banish nulls from my codebase, completely. I can currently do this with a variety of annotations (at the package-info.java level) and tooling, though it's not integrated well with the language.
Forcing exclamation marks into every variable and parameter is a lot of annoying noise that quite simply nobody will do. The default should be non-nullable, especially for value types.
Declaring whole types as non-nullable is less noisy and errorprone than annotating every variable declaration. If you aren't going to give me "declare the whole codebase as non-nullable" then at least give me something coarse-grained.
First, a record can't extend anything, it's not even valid syntax, so a sealed class can't permit record subclasses. So no, it's not possible to create a Maybe<T> class in Java that can only represent a Some<T> or a None<T> record. You could do it with regular classes, or if it's ok for Maybe<T> to be an interface.
Secondly, regardless of the sealing, nothing in any current or near future of Java prevents you from assigning `null` to any class of any kind you might create. So you can always have `Maybe<T> x = null`, or even `Some<T> x = null`.
None of this will change with the adoption of value classes either. So no, there is absolutely no way in Java to create a real Optional/Maybe type that would guarantee that a variable is either an object of a given type or None. There is probably some way to do it for your specific project using annotation processors, of course, but that is very different from having built-in support.
That is an eloquent way of re-writing the history of Microsoft stealing Java and not being allowed to get away with it.
There's a whole bunch of specification language describing how constants aren't actually constant in specific situations.
I don't know Kotlin but I assume it does the same thing: until the non-nullable field gets initialized, it holds null and violates the type system.
And that's a massive problem that they're planing to solve with specialized generics.
> If I read the correctly, it means that if you have a Point value class then on the JVM level you'll be able to stuff any value class into there if you're so incline, just like with List<Point>.
I'm not sure what you mean. An L-type is an object reference. E.g. Ljava/lang/String, Lcom/org/CustomObject. The issue you're conflating is the erasure of List<T> to List<Object> and it's L-type Ljava/util/List.
> I prefer the Hack version
Hack/HHVM didn't have to worry about backwards compatibility.
---
The blog post does a pretty high level overview of the implementation, but my understanding of it is the following:
1. They are adding a new bytecode class flag for value classes. Bytecode descriptor for value classes are exactly the same (L-type).
2. Primitive wrapper classes will become value classes.
The difference from the CLR is that the JVM implementation is backward compatible with linked legacy bytecode. As they can accept a value class instance into their methods due to the same L-type. It's just additional metadata added to allow the JVM to stack allocate the class.
---
Separately, to handle generics:
1. Parameterized container classes will be flagged with a new bytecode to enable parametric attributes on initialization with the additional data of the parameters in the Constant Pool. Bytecode descriptor for classes are exactly the same with type-erasure, e.g. List<SomeType> erased to Ljava/util/List.
2. Initialization of parameterized classes are done with the additional metadata of the type argument stored in the Constant Pool.
3. The runtime does monomorphization of the parameterized class.
It doesn't seem too different to what the CLR does, i.e. runtime monomorphization. The difference is the JVM implementation is backward compatible with older type-erased code, i.e. the restrictions in Java due to type-erasure are exactly the same as before and the L-types don't change. It's just additional metadata added to allow the JVM to monomorphize the classes for performance.
---
In summary, value objects and specialized generics are backwards compatible with legacy bytecode. The JVM handles the compatibility. Of course, the newer bytecode is not forward compatible with older JVMs.
This also has huge implications in a language that emphasises dynamic loading like Java. And it also flies in the face of all of the pretenses that ABI compatibility is sacrosanct and no feature that breaka it can be considered, that the design team often touts.
iconst_1
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/IntegerI think (but may be wrong) their concerns are about the insert part. C# always had structs, Java wants to add them in a backward-compatible way. They want, for example, existing generic container classes pulled in from a .jar (i.e. already compiled) to support Java value types.
Yes and no, because in Java we have runtime types and compile-time types. The frontend compiler will treat these types as having different defaults on nullability, but they'll compile down to the same representation (when appropriate). I.e. if the compiler sees that some Integer variable is never null, it will compile down to the same thing it would if it were declared an int.
You're right, however, that on the heap, until the language adds nullability information, the compiler cannot generally know that an Integer will never be null (unless it's a final field), so it's likely that, unlike on the stack, you'll get a different representation.
Kotlin still has a hole where you can run code in "init" blocks which are executed sequentially on object construction; in one, you can call a function that is defined after an unmodifiable property, and it will see the uninitiailized value.
Regarding the 0 value choice in Go, I don't agree that this is worse than null. It simply applies a design constraint that is not usually very hard to satisfy - that the 0 value of your type must have well defined semantics.
Like Caesar's supposed "Veni Vidi Vici" saying, people seem to prefer and remember items when grouped in three.
I recall a public speaking film shown to my management science class starring John Cleese mentioning this rule of 3.
Nope! That is - training on lowest-common-denominator, low-signal high-noise "idiotspeak" was not at all inadvertent.
4chan
Call of Duty chat logs
Every public marketing site
SlashDot
UseNet
...
Verdict: Yes idiotspeak was part of the training set, but no, it was not inadvertent. There's a smattering of Shakespeare in there, at least.
But I'd say that GP's complaint about inequality leaking makes no sense anyways, because what could be more unequal than different implementation, or different internal state implying different behavior down the line? The public subset isn't some arbitrary interface that could have different implementations. And even then, "equals under interface I1" would have to be considered a very special type of "equality", not the general case.
That being said, it is easier to write a language on top of the JVM with good interop, since there are less ways to implement features. Essentially, your language has to interop with Java.
And it is harder to have good interop between CLR languages because there are more ways to implement features. Essentially, your language has to interop with C#.
The monomorphization of CLR generics is what NativeAOT does, though it doesn't support some C# features.
TypeScript is essentially C#, but with type-erasure and lacking the low-level struct & pass-by-reference features.
I do think the C#/CLR struct implementation is better though.
Then you didn't read the JEP draft (it's not an accepted JEP) carefully. It says, under "future work":
Providing a mechanism in the language to assert that all types in a certain context are implicitly null-restricted, without requiring the programmer to use explicit ! symbols.
In other words, the draft already incorporates your point, but JEPs (both drafts and actual JEPs) follow the pattern we've found to work so well, that features are best delivered piecemeal rather than in a big bang.
Having said that, I don't know the current plans for this matter, as that document is only in Draft status, so saying it's good or bad is pointless, as it's not even a proposal yet, just something being explored.
In Java, you can ask, `x instanceof T` (and this is a runtime test), which means, is x one of the values in the set of values allowed by T. `null instanceof Integer` is false, even though a variable of type `Integer` can be assigned a null. So you can think of `Integer x` as being `Integer|null x`, i.e. x can hold a null, even though `null instancof Integer` is false.
This is true in some languages but not in Java. The limitations (and performance cost) are not from the nature of continuations/stackful coroutines/"colourless functions", but from their interaction with other constraints and existing designs in the language. E.g. in Java, virtual threads have zero impact on FFI.
In general, the costs and limitations associated with a feature in language X don't extrapolate to language Y, because they often stem from interaction with existing constraints in language X.
The design of FFI is a very common source of problems for various features. For example, if the FFI is designed such that you frequently pass pointers to to objects to C, that can have a big impact on other features. In Java, you nearly always only pass pointers to "off heap" memory, i.e. memory that's not managed directly by the JVM. While this has no performance cost, you could say that this, in itself, has some convenience cost, except Java programs need to rely on FFI much less than other languages, so the overall cost to convenience is low.
And as proven in the recent announcement, they had to rewrite parcel from C++ into Go, as they didn't found a comparable library in Go ecosystem.
There is also another interview, where again they mention having used AI as tool for code rewriting as well.
Also to note that it was pointed out that Native AOT wasn't up to the job, again something that both Java and C# failed not having done it properly from day one.
type Foo = { x: number; }
type Bar = { x: number; y: number }
type FooBar = Foo | Bar;
function baz(x: FooBar) {
if ('y' in x) {
// compiler now knows x is a Bar
}
}
In this case, the variable `x` has a property that is determined by the compiler based on control flow. i.e. it isn't explicitly carried by the type of `x`.I don't think this syntax is desirable as currently proposed, and that one line under "future work" is doing far too much lifting. My sincere hope is that there are people closer to the process that also feel this way, they will provide similar feedback, and the next draft will be something completely different.
This also has significant impact for serialization/de serialization - a classic place where you get unexpected nulls, that Java Optional/Maybe don't help with at all.
> Also to note that it was pointed out that Native AOT wasn't up to the job, again something that both Java and C# failed not having done it properly from day one.
It's been working fine for a few years now. The only problem I know is there is little to no reflection allowed (by design) so a lot of code out there is not compatible with it yet. Not sure if that's what turned the TypeScript team away from it.
Types are both for the compiler, as well as for the developer. Maybe types are implicit documentation telling the developer that it is meaningful in the application that this field can have a None state.
That's a huge code smell to ever set null to an Optional/Maybe and code reviews, linters, nullness analyzers all should/will flag such.
Like I have never ever had an NPE from an Optional being null. Sure, complete null safety would be better of course, but in this very instance it ain't buying you much.
So consider that draft as an idea for how the site-specific nullability annotations could work rather than an idea for how nullability could work in the language in general.
https://youtu.be/UJfF3-13aFo?t=1453
As for the AOT part, one would expect that being all Microsoft, they could work together to fix whatever were the issues with Native AOT.
All the types that are value types in semantics, e.g. Optional, should be proper value types on Valhalla.
Additionally, they should be compatible with existing code that expects them as parameters, fields,.... without being recompiled from source.
If it is a complete new type without backwards compatibility, no one is going to adopt it, other than a few niche cases.
Well wasn't that the argument above, that the stuff they added so far isn't proper at least in part because they didn't fix that problem yet?