I appreciate the hard work that went into the things that did make it into Valhalla eventually, but:
> 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
> 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.
I 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.
First, your parent comment misunderstood what the section they were critiquing is referring to. It's not about nullability (which is orthogonal) but about reference/value projections.
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.
This is great advice and it applies to a lot more than just language features. Different architecture, deployment setups, QA approaches are all like this. It's always "approach A is no good", "but company X uses approach A and they're doing very well", "yeah but look at all of these problems they have". Maybe a fair argument but the approach B people also have their fair share of problems...
What's funny is that the only languages in the same popularity league as Java are Python and JS/TS (and possibly C and C++ if you want to extend things to more domains) yet I rarely ever hear people saying Java should be more like these languages. It's because many of the people who dislike Java also don't like any of the most popular languages (even those who like, say, Python better don't think Java should be more like Python).
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.
Both languages/runtimes are decent, stable and good choices for running production systems.
They have different philosophies though, C# is mostly "give the developer a complex toolkit so they can do whatever they want, but keep the runtime simple", while Java is more like "keep the language as simple as possible, and put new features mostly into the runtime making it more complex, but benefiting even old code".
Async vs virtual threads is a clear example of that. The former makes the language much more complex, and has a huge surface area where it may clash with other features, so a developer has to remember all of these. But it's a simple machinery under the hood.
Meanwhile virtual threads don't change the language at all, it's the same as it was with Threads, but under the hood it is a very complex feature.
And in this particular instance I think it is quite clear cut that Java made the correct decision, for the most common use case of these languages having non-blocking IO "automatically" makes for significantly better user experience.
I do think Java is a fine language but I don't really agree that it looks more simple in any way shape or form.
For things like UI programming where you need a main thread or where you do need a cross language OS thread, I think Java is going to end up more complex. Under the hood it is very complex and that _will_ show itself.
So from my perspective it's not virtual threads, it's virtual threads with structured concurrency vs async/await. When you look at it that way, the comparison of complexity is certainly not seamless or simple.
.NET do what they do because they believe many people like it, and we do what we do because we believe many people like it. Because different developers like different things, we may both be right. The only explanation to why we do what we do is that we believe it works well for us, and we know what everyone should know, which is that what we do can't be universally liked because developers have different preferences.
But that doesn't mean the preferences are equally distributed. Some people liked Betamax's better picture quality while others liked VHS's better recording times, but they didn't do it in equal numbers. And that's what's funny, because there are two languages doing as well as Java or better (one of them is much more popular), and yet what's almost universal is that when people say "Java is worse than X", X is invariably a language that's doing much worse than Java.
I'm not saying that there isn't much to learn from less popular languages, but the claim "Java is consistently making the wrong decisions compared X" doesn't make sense when X is consistently losing to Java.
And I can even give you the secret to our strategy: Make Java the language that people who choose it regret less than people who choose other languages.
I think you misunderstood my point. I'm not claiming that the most successful products necessarily have all the best features. I am saying that the claim that "team X consistently makes worse decisions than team Y" is hard to support if team X is winning. At least some of their decisions are probably better, and those ones also probably matter more.
As a person with a lot of .NET experience but has been working on other tech since 6 years, the wow effect I had from using LINQ has never been repeated with anything else.
It felt like magic, maybe too much magic, but so useful nevertheless.
LINQ comes from FP, and was introduced into C# via Haskell, and actually the first version (.NET 3.5) was quite similar to Smalltalk or Common Lisp collections.
Value types kind of definitively don't have null, right? You can have a zero int but not a null int. So nullability is not entirely orthogonal to value types, its an advantage for value types where they are practical.
I didn't say nullability is orthogonal to value types; I said it was orthogonal to the two-projections world, which is what that text in the article was about rather than nullability.
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).
> More generally than just Java, nullability is often a property not of a type but of a variable
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.
> the syntax proposed in the Null-Restricted Value Class Types JEP is a major step backwards. 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.
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.
It's a proposal for a proposal, fine. Consider this my proposed feedback.
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.
Except we like delivering features by pieces, so unless your proposal is to first deliver global flags with no instance-specific control and later add more control (and I think you're not suggesting that), I don't see any difference between what you're suggesting and what that draft is suggesting. That a one-line under "future work" is doing a lot of lifting is just how we usually do it (and it's okay, people always complain, but we've tried the big-bang approach and this one just works better for us).
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.
> how the site-specific nullability annotations could work
That's fine but it's still a bad choice. The problem is that it defaults to everything nullable and adds noise for non-nullable. This is backwards; in the overwhelming majority of software that people use Java for, non-null is the common case and nullability is the exception.
Kotlin and Typescript got this right. Nobody wants! to! write code! like! this! And they won't, so the feature won't get used and we're back to everything being nullable.
Except there's no choice here. It's a draft of an exploration of a portion of a feature.
Drafts are ideas that have not even been submitted for consideration for inclusion in the roadmap. You can find drafts over ten years old that have long since been superseded by other ones or abandoned altogether: https://openjdk.org/jeps/0#Draft-JEPs. Some JDK engineers write JEP drafts when they feel they get close to something they would like to propose, while others write drafts for pretty much any idea they have.
> The problem is that it defaults to everything nullable and adds noise for non-nullable
It doesn't, though. Even if this draft ever becomes a JEP (and I don't know if it or anything like it will), in its present form or another, it would still, like most JEPs, describe only part of the feature. It's perfectly on point for one JEP to describe the explicit nullability annotations, while another describes the defaults. Smaller features than this have been split into two or three JEPs. This is just how Java features have been described and delivered for years now (see how many JEPs patterns were split into: https://openjdk.org/jeps/0).
It's perfectly okay to dislike some design, but I find it strange to assume, based on a draft of a portion of a feature that one of the most experienced and successful programming language design teams in the history of software is likely to get it wrong. Maybe wait until there's an actual proposal and a roadmap for the complete feature before critiquing it? It's like seeing a draft of some building's foundations by a prestigious architectural firm and saying, these idiots forgot to plan a roof.
This is a strange conversation; you must be misinterpreting me.
I'm not saying that the people who wrote this are dumb, or that Java is a bad language, or that it's time to move to Kotlin, etc. I use Java every day. I pick Java for greenfield projects. I'm on Team Java.
I'm saying I don't like this proposal. I happily accept that this proposal might not become part of the Java language. That's great! I'm spending time writing this in public specifically to discourage it in some tiny way.
It would be fair to tell me "you should write this to the proposal writers directly". And I understand how the language snobbery on HN might make you feel defensive - I often feel that too. But I think my criticism here is valid.
And I'm trying to tell you that this document is not what you think it is. It's a rough sketch of a building's foundations and your critique is about the roof. Even if this were to become a proposal, it's likely the matter of defaults of this feature will be covered by a different JEP, because Java features are usually broken down into multiple JEPs. What you're complaining about may be part of the feature, but it is not covered by this document. If this becomes a JEP and another JEP talks about nullabilty defaults, then you could criticise the selection of defaults, but that particular aspect of this feature is outside the scope of this particular document. So one, this is not a proposal, and two, you haven't seen the description of the part of the feature you want to criticise.
We are well aware that splitting features over multiple JEPs can invite such misunderstandings, but that doesn't change the fact that Java features are, at least currently, split over multiple JEPs. We are also well aware that if this does become a proposal, adding ! everywhere is not what we want, but we want to cover that aspect in a different document, as we usually do. Most Java users aren't confused by this because they don't read the JEPs at all, but such splits help focus the discussion on one aspect of a feature at a time. So your desire for a non-nullable default is very understandable, it's just not relevant as a critique of this document.
For example, the virtual threads JEP described a pinning limitation. We knew it reduced the applicability of that JEP and said as much. We just wanted to address it in a different JEP, and so we did (https://openjdk.org/jeps/491). Ever since the JDK switched to time-boxed, semiannual releases, this is how we've delivered features: in multiple pieces. The same applies to Valhalla.
Yes, but it comes from Java having both runtime and compile-time types; it's harder to make the distinction in languages that don't have runtime types.
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.
I think I mostly got this, but just to test it, it would be like in Typescript where I might say:
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`.
This won't be true in Java, though - in Java, you will have null Integers at least. It seems that int will remain a different thing entirely from Integer, and will remain a JVM-only concept.
But with null-restricted types, Integer! and int has no difference semantically and representation. They plan to introduce null-restricted types in future.
I think user-mode threads are better when the language already has threads. I'm not sure they would have been better for JS. But yeah, this one is probably less controversial than things like properties and extension members, which we also said "yeah, nah" to.
Although the work being done to enable multiple threads in JS is impressive I think it will be hard to make robust in many casss without locks, or fairly strict limits what operations can be performed on objects shared between threads.
The property lookup and modification process in JS is complex enough as is, is not specified in an atomic kind of way, and has many opportunities for user code to be run as part of accessor properties. Enabling it in multithreaded implementations is tricky without opening up deadlocks when modifying property collections, and even with that could likely be broken by some suitably evil code. Ive worked on more than one implementation that offered some degree of multithreaded access and it’s generally only safe when limited to simple properties.
Async / await avoids those issues because none of the places where user code can be executed allow async code, so there is no opportunity for the world to be changed under your feet during something like property access.
What's wrong with what .NET did with threads? Having async tasks sharing the GUI thread seems like a nice feature. Will we be able to use virtual threads and structured concurrency with Swing, e.g. to wait for a background task in an event listener?
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.
So does Java provide an API for continuations or just the virtual threads hidden behind the threading API? You can't use threads to wait for something on the UI thread (which is why e.g. SwingWorker exists), but you can with await.
There is no public-facing continuations API afaik.
Structure concurrency/virtual threads seem like a good fit for Swing; just have your event handler fire off virtual thread[s] and do your work and call SwingUtilities.invokeLater to schedule the result to be applied on the event loop thread when you're done. Structured concurrency simplifies managing groups of concurrent tasks, cancelation, etc.
You still can't block in UI code, right? You don't know whether you're on a virtual thread or the UI thread (even if its virtual) by design. You still have to bifurcate your APIs into blocking and non-blocking, don't you? Virtual threads will help you keeping the CPU fed but it won't handle separating UI and non-UI work, or know when a frame deadline ends.
Have there been Swing specific updates to deal with that?
The colorless functions approach has well-known disadvantages though, including providing less control and making interop a pain. It isn't like one approach is the correct one and another is a mistake.
> including providing less control and making interop a pain
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.
How can green threads have zero impact on FFI in the presence of callbacks across the boundary? I mean, as soon as the other side uses TLS in any way, for example, you have to figure out how to handle that, and it's not free.
In every user-mode thread or stackless coroutine (async/await) implementation I know, the construct is pinned (i.e. can't be preempted) while there's a foreign frame on the stack. There is no advantage to either design on that front.
But 1. there's no speed penalty in Java for doing that, and 2. In Java, calling native functions that block (which is the only time this limitation matters) is rare, especially in high concurrency situations where you'd use virtual threads in the first place.
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.
Java stagnated for quite a while. Seemed like everyone was stuck on Java 6 for about a decade. But JDK8 was a huge step forward. Lambdas, streams, and a date/time API that is the best I've seen.
I think DotNet had a bit of benefit, in that the language was still new enough to do the hard breakage. It was only about 3.5 years between NET1.0 and 2.0 (Where generics were added.)
I was annoyed by it back when I was doing Silverlight/ASP.NET
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.
The way it worked in practice is that all the new built-in collections in System.Collections.Generic implemented the non-generic interfaces as well, so e.g. List<T> implements IList<T>, but it also implements IList (explicitly), and similarly for ICollection and IEnumerable.
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>().
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.
> They have added proper algebraic data types to the language
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`.
Sealed classes/interfaces and records are proper sum and product types.
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)
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.
> So you can always have `Maybe<T> x = null`, or even `Some<T> x = null`.
Yeah and? Practically every type system have escape hatches, like Haskell can also do side effects without the IO monad, does it make the latter useless?
The whole point of using Optional/Maybe is to prevent the possibility of accidemtally creating nulls. If you don't make mistakes, then nullability is not a problem. If you do make mistakes, then a class that only helps when you don't make mistakes is basically useless.
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.
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.
Given the mess of some .NET frameworks currently, and how bad it has taken for non nullable references to be widely adopted, I don't see those correct decisions on the last releases.
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.
>Those interceptors and inline arrays via attributes instead of proper language grammar aren't that great either.
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.
I have this idea, that since they went open source, but failed to gain the adoption on UNIX shops they were expecting, there are tons of features to try to make it cool again somehow.
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.
As someone who works with Java on a daily basis alongside a dozen other technology stacks, let me go out on a limb and say that I believe Oracle has been a stellar steward of the language. Java has been evolving quite nicely and at a reasonable pace, all without breaking the ecosystem or causing fragmentation. It certainly has its drawbacks, but doesn't everything?
I'm honestly happy with java lang's stewardship over the past decade, this particular JEP notwithstanding (it's fine, but the good parts come later.) They're conservative in adopting new features whereas I see every other language bolting on everything under the sun with reckless abandon. I prefer the "let's see what shakes out" and adopt "the good parts" which seems to be Java's approach. Sugar like "var" from kotlin, project loom event loop like nodes, etc.
not claiming kotlin invented anything, just that after kotlin adopts something and it turns out to be useful, java will come along in a few years and implement it (or avoid it, depending on how it goes)
I had the opposite experience, spent a year with each language, first Java then C#, and to me C# felt like "Java done right". (Which appeared to be the original design goal behind the language!) So I'm curious about your experience.
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 :)
Same for me. I have worked with Java since 1.2.2 and used .NET for something like 10 years (don't remember the versions). Most important differences are:
-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.
> -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).
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.
> .NET is about extending an existing application (Servlet API vs IIS)
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
Yeah, me too. Java always seemed to consider design a lot more than C# which seems to have taken more of a kitchen sink approach to language design. That stuff piles up over time (see c++)
First, huge open source ecosystem and culture. Mature open source projects, culture of writing blogs and tutorials (that one will die due to changes in search engines, but it was super nice while it lasted).
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.
How .net got so many things right where java did not is a mystery to me, but appreciated (it has its own flaws, of course). Java, in my understanding, is still of core relevance to Oracle, and tied into a lot of contracts that require very little effort from them to maintain. But you are correct in observing that they want to be a datacentre/compute business more and more these days; they may have in fact overcomitted to this due to the AI craze, since shareholders are already complaining.
“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.
This. C# was basically always meant to be "Java but done right". It came several years later, after Microsoft was legally barred from "EEE"-ing Java and required a direct competitor.
They didn't "steal" anything, iirc; they started as a legitimate licensee and then tried their usual embrace/extend/extinguish as "J++" (the EEE I mentioned). Sun sued for breach of license and won, barring Microsoft from extending Java outside of the (Sun-controlled) process. So they dropped it and built their own version, with blackjack and hookers.
But what I don’t get reading the original article is that they present how to insert struct in an object oriented language as an intractable problem, whereas a good implementation with .net (as far as I can tell) has been out there for nearly 30 years. And C# was shameless about stealing from other languages.
> how to insert struct in an object oriented language as an intractable problem, whereas a good implementation with .net (as far as I can tell) has been out there for nearly 30 years. And C# was shameless about stealing from other languages.
I 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.
Well, my old code can have a List<Object>, and I can pass my new type into it and have it do some operations. Let alone with reflection - so backwards compatibility is more complex than that.
As for structs, Java avoids e.g. tearing issues with making them immutable, while it is easy to optimize it to local modification under the hood.
Which recently decided that Go was a better option than C# for the Typescript rewrite, exactly because not all decisions were done correctly to make C# a better fit for the problem.
Go was chosen mainly because it aligned more with how the existing compiler is designed. They did not want to redesign the compiler which eliminated C# as a choice. So Go is apparently just a better fit for quickly porting JavaScript code to.
That was the original motivation yes, although they acknowledged later that the weaker type system from Go required redesigning the data structures anyway.
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.
They said the prototyped in a few languages before settling on Go. Based on what you said it sounds like they didn't do a great job at that and stuck with their decision anyway.
> 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.
The mystery of why .NET got so many things right is simply that C# was built several years later by the exact same Microsoft engineers who had previously worked on extending Java, giving them a perfect blank slate to fix the architectural flaws they had already encountered
virtual thread instead of async/await is a counter example.
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.
.NET had green threads (fibers) in its first iteration. They were abandoned because practical use of .NET was also FFI-heavy - WinForms etc - and native code generally doesn't play well with non-native threads. The benefit of async is that it desugars into callbacks with state, which is something that can be easily expressed in terms of the C ABI (which is the de facto interop standard on all mainstream platforms). Which is why you can have async C# code calling into async C++ or Rust code, or for that matter async Python calling into async C#. Given that .NET was historically supposed to be a multi-language runtime, before they went all in on C#, async made a lot of sense.
C# did not ship with async/await, and Java didn't have virtual threads back then. I am specifically referring to the initial choices made in C#'s foundation.
Java made several mistakes. It also made some questionable (yet often defensible) decisions. It's understandable. Type erasure was one I believe was a mistake. It's talked about in the article. Yes, you kept binary compatibility but you that created so many other problems such as not being able to use value types in generics. Notably, C# looked at that and said "nope". Type erasure is also hurting Valhalla here and the issue of value classes in generics is the second phase so is being pushed far into the future.
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).
> Yes, you kept binary compatibility but you that created so many other problems such as not being able to use value types in generics.
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
How would a non-nullable class field work in Java when it can be initiqlized by arbitrary imperative code that can read it while it's being initialized?
The type erasure version of this would look a lot like Hack [1]. So generic arguments would simply have a ? if they allow nulls eg List<?Point>. The list itself couldn't be null unless it was ?List<?Point>.
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.
> 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.
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.
Look at how Go did it. Any value type has a defined 0 value, and any variable of field of that type is initialized to 0 by default. So any un-initialized non-null able value type field could have the corresponding 0 value.
That's worse than null. Now every object has an invalid state. It works for Go because Go is trying to keep language complexity low. It has no good design principles behind it other than that. Also the zero value of a reference is null so you still haven't answered how a non-nullable reference field would work.
Sorry, I misinterpreted your question to be about non-nullable value classes, not non-nullable classes more generally. For reference variables, it seems that the a priori best approach would have been to generate a compiler error if the field was read before it was written; since this ship has long since sailed with final fields, it seems that the same approach would have to be taken - the "not null able" guarantee only applies after the class is fully initialized, and you can observe it as null during initialization. This is probably not a huge problem, given that initialization code always has to deal with a class breaking its invariants, since they are only established at the end of initialization.
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.
The compiler doesn't solve the halting problem so it doesn't know whether a field is read before it's written.
If you allow a nonnull field to hold null at any point then it's trivial to leak that out and have nonnull variables holding null all over the program, which is obviously something to be avoided.
Maybe you can say that if you read a nonnull field when it's null, that throws an exception. That might have its own knock-on effects since nobody expects reading a field from a valid reference to throw an exception, but it might be the best way.
You don't need to solve the halting problem to prevent such access, not even close. You simply apply the same strategy as any other property you want to enforce - you prevent any use of the variable that can't be proven safe. You want to pass this in a call from the constructor to a function defined somewhere else where it might access a final/non-null able field that has not yet been initialized? Not allowed, only member functions that don't call other non-member functions allowed from the constructor.
Of course, as I said, this ship has long sailed for general Java classes. It would be far too big of a breaking change to add such a limitation today. However, we know for sure this could have been done in Java: this is exactly how record and now value classes work: any call that could expose un initialized fields is in fact disallowed.
I think you should require the code to be written in such a way that the compiler can verify the value is never read before initialization. There might be some subtleties in how to do that but I don't think it should be insurmountable.
It's completely insurmountable. It basically means you can't call any functions during initialization. Including the one that creates the object you want to point your nonnull reference to.
...which is exactly how value classes and (after this JEP) records work. You are not allowed to call any instance methods before the call to `super()` (which happens implicitly at the end of the constructor for value classes and for records, unless you explicitly call it), and you are not allowed to call `super()` without first setting a value for all of your fields.
Probably with hacks. Did you know a final field in Java can change its value? And I'm not talking about his reflection to make it non-final. With ordinary code only, you can read a final field before it's been initialized, so it still holds its default zero value. For example "final int x = calcX();" and have calcX print the value of x, it will be zero.
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.
Kotlin works around this by not exposing the backing field directly; a "field" becomes a "property" which has a getter and setter under the hood. Attempts to read uninitialized properties are compile-time errors. You can turn these into runtime errors using the "lateinit" keyword.
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.
Nothing stops me from sticking my dick into a blender either, yet it works fine for most of the things (the blender). Both Java’s notion of final and Kotlin’s nullability work great for 99.999 use cases and you really need to go out of your way to break them.
If you can get a null into a nonnull variable, it can propagate all over the program and people will be defensively checking for it everywhere - final is not like this.
`lateinit var` was not added to Kotlin to handle nullability, but to address specific Android design where system components like Activity cannot realisticly initialize fields in their constructors. Outside of Android it shouldn't be too commonly used.
"lateinit var" predates Android's adoption of Kotlin by years and was added because fields initialized after construction by frameworks are common in many contexts, but having nullability in the language makes that annoying.
I agree, but I have seen the previous proposals/jeps and the discourse around them is rather discouraging. I hope this one can find it's way out of Draft, but I'll only believe it when I see it.
> 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.
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".
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.
That's not quite how it works in Valhalla. Because Integer and int already exists, your declarations above will be interpreted with those meanings, but (assuming some TBD nullability annotation), they will be equivalent to `int? x` and `Integer! x` respectively. In other words, the nullability of a variable is a separate concern from the data type, and other than the different defaults on variable declarations (as these types already exist), Integer and int become the same type.
This may be true, hopefully, in a future version of Java, if the article isn't wrong. In JDK 28 with the Preview feature enabled, int is not nullable and Integer is nullable, and they are thus different types under the hood. Which also means that on most CPU architectures, Long[] will be just as inefficient compared to long[] as it was in any previous version of Java.
> int is not nullable and Integer is nullable, and they are thus different types under the hood
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.
It's not that weird. The goal is to enable existing types to be turned into value types without porting the users, so stdlibs and other libraries can mark types as value types without an API break.
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.
Nullable is just a different loadout state in Railway Orientated Programming. So, no reason to put different flavours of state into the language directly, when its a solved thing since (checks slides) 2012. There is just rails - going to A or going to B, depending on the trains loadout.
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.
They just decided to tackle non-nullable value types in a follow-on JEP. I don't think they're saying it's untenable. You don't eat the elephant in one bite and all that.
That said, we've been gnawing on this limb for a while...
> 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.
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.
I mean, I appreciate the huge engineering and design challenges here, but C# had non-null value types in 2002. It had generic value types in 2005 and it gained not-null “types with identity” in 2019 and no-one has batted an eyelid. (Indeed the type system support for value types still includes stuff Java isn’t even considering yet.)
Saying the mental model is too hard is basically saying your userbase is stupid. This stuff is not tricky.
> 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