So in that sense having the three options would be quite desirable. Especially since a few hundred thousand lines of code aren't that quick or easy to migrate.
In another internal project we've been doing this in a different way by sprinkling #nullable enable around the code where we touch it and thus gradually improve nullability coverage (and keep in mind that new code must be in a nullable context). This works as well to make it explicit what is annotated already, but it's a much smaller codebase and team.
At least new code is forced to deal with those warnings for now, which is much easier than fixing legacy code.
In addition, the existing @Nonnull annotation could have been leveraged as being the way how nonnullable type occurrences are indicated in class files, providing two-way compatibility for old JDKs.
The current proposal doesn't exclude this possibility in the future, does it. Maybe one day you will be able to declare a class
public class! MyClass {
or have a package-info file which says package! com.initech.accounting;Otherwise, I agree that it should be in the source. In practice, looking for the presence or absence for T? vs. T! will be a good heuristic for whether the code assumes the new or the old semantics.
null-marked module accounting {
}
Could very well be the sort of thing that comes next.> 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.
Could be nice, even if just as a compiler flag or a module tag.
Still, I also suppose there's going to be a compiler flag that auto-assumes non-nullability unless specified otherwise.
Project Loom also breaks tons of code because of locking and other threaded side effects that weren't even a consideration before, a trade-off between coloured functions and risking old code breaking.
It seems to me that Java is very conservative when it comes to the language itself, but actually compiling and running that language seems less of a concern. In this case, the "specify a flag to make all non-annotated variables non-null" approach would only add a single glyph to the language (the ? for denoting nullability), which I would've expected based on the way Java has evolved over time.
As far as I understand, some IDE's and built automation tools did not rollout support for module system immediately, yes?
The problem isn't necessarily that the compiler isn't called right, but rather that a lot of existing library code suddenly didn't work anymore. By compartmentalising code (a welcome improvement!), projects that weren't designed for this compartmentalisation need a lot of "basically turn off Java module restrictions" command line flags to the JVM.
That’s not true at all. Besides the package name change with jakarta, all it did is just expose a couple, popular libraries that were depending on runtime internals. Most of this could be fixed by bumping said libraries version. And the whole point of the module system is that it will never happen again, due to stricter encapsulation.
> Project Loom
How would it break any code? You have to explicitly use virtual threads, normal threads are not touched at all and will work as they have always did.
And yeah, java has always been “conservative on the language front, state-of-the-art on the runtime”
After 17, legacy systems needed to start adding runtime options to work correctly. And, as I recall, this was mostly constrained to the dynamic, reflective, and introspective properties. Generic jars and code can still run without the module system.
Unfortunately, a lot of the magic in modern Java is through those reflection aspects. So it made moving to 17+ an effort for many legacy code bases.
And it should be clear, in general, all of the code was fine. It was all runtime options setting up package permissions that was the problem. Doesn’t make it less frustrating.
(And I’m pretty sure 17 was the rubicon for this, but it may have been another version.)
I don't remember the exact details, but I worked on a project that was stuck on java 8 for a long time because some critical dependencies broke in java 9, and it took a long time before they released versions that were compatible with java 9.
You often have no control over what kind of thread the caller of your code is using. Code written for the old threading model can and will end up being called by these newfangled virtual threads. If it does something the virtual threads don't like (which, as far as I have heard, includes things like some uses of synchronized blocks), you can have unexpected breakage.
And many of these extreme cases are getting “solved” to not pin the thread.
It is made worse, by being a C# 8 feature and not usable in libraries that need to stay compatible with .NET Framework (yep still plenty of stuff going on there).
[−3.7]: https://sharplab.io/#v2:EYLgHgbALAPgAgZgARwExIMJIN4FgBQSRKyc...
One approach to adding nullable annotations to a large code-base is to go "bottom-up" on DLL at a time. The annotations are somewhat viral; if you were to go top-down then as you get to more of the "shared" DLLs and add annotations you'll find yourself having to go back and revisit the DLLs you already migrated. In this light, the .NET standard library situation is problematic.
Imagine implementing IEnumerable<T> by wrapping another IEnumerable<T>; in .NET framework there are no nullable annotations, so you can get into a situation where you don't add a nullable annotation where you will eventually need one upgrading to newer .NET. This can bubble up throughout your code base, making that eventual .NET upgrade more challenging.
I'm not saying its not worth it to do this in .NET framework, but you can very easily add extra work to the inevitable(?) update to .NET 8+. When you get there you could of course temporarily regress and turn nullable stuff back to warnings until you work through that surprise backlog, but nullable is really nice so you might be strongly inclined to not do that... not a huge problem, just something to be aware of!
This shouldn't be a problem when multi-targeting, but we've opted to not use that for now for other reasons (performance in Rider, mostly).
All existing code is "we don't know if it is nullable until someone reviews it", that is different than explicitly allowing nulls. To add to confusion all Optional<> variables should be not-null or using Optional makes no sense at all.
I use Optional<> but only to indicate at the API level that something can return null. I hope the chaining ability (?. in many languages, implemented as Optional.map()) will also one day make it to Java.
https://stackoverflow.com/questions/26327957/should-java-8-g...
What's even worse is that APIs that return Optional<T> can just return null for the Optional value itself! This a pretty evil thing to do on purpose, but this could still easily happen by mistake. If you want to make code using optionals null-safe you have to go through quite some hoops and check that both that Optional<T> is neither null nor empty.
I hope Java can now change the API of Optional and make it safer. For instance, Optional<T>.of() should require a `T!` argument, while Optional.flatMap() should require that the mapper returns a non-nullable Optional value. Likewise, linters should reject any definition of an Optional that is nullable or has unspecified nullability.
I have yet to see this ever happen, despite working in Java shops using Optional heavily since it was included in the JDK. I would guess that this is due to developers using better tooling. IDEs commonly provide warnings when returning null, or when violating a "soft" nullability assertion marked by an annotation. NullPointerExceptions seem fairly rare.
Yes, this is not likely to happen with a good IDE with warnings enabled, and developers not ignoring said warnings. It is even less likely with a strict lint step in your CI/CD which treats all non-suppressed warnings as errors. This should be the standard for every software project in the world.
Unfortunately, for many enterprise scenarios, developers are not encouraged (or even discouraged) to apply these best practices. I'm talking about your typical setting with non-technical management, low-salaried or outsourced developers, tech debt rarely being fixed and the only best practices which exist date back to the 1990s or early 2000s.
Doubly unfortunate is the fact that shops of this type _overwhelmingly_ favor Java. For better or worse, Java is the #1 enterprise language and the COBOL of the 21st century. As such, something that would be a non-issue in Rust (where few developers would dare push to production code that didn't pass cargo clippy with flying colors), becomes a rather big issue in many shops.
If a company policy mandated that it must be on, it would probably not meaningfully reduce poor code because the team was so poor in the first place, they probably don’t understand the problem it solves, so will just work around the now “error” using the least amount of effort to appease the CI server, while still leaving problems in their wake.
Strong teams regularly and consistently reflect on their processes towards continuously improving, such teams will naturally move to applying such a practice as it will add value.
Weak teams tend to just do what they always did until forced to do something new and even then they do it more cargo cult style than in a way that actually improves the bottom line.
x?.let{f(it.y)}
via Optional.ofNullable(x).map(it->f(it.y)).orElse(null);
Will it at least get taken care of by escape analysis?These days I'd not even trade the simple but effective approach taken by Kotlin for the full glory of Scala's Option (which is so far beyond the Java Optional).
If I have v = Optional<A>, if v.hasValue() does it mean v.value is not null? How does it interact with the nullability control? If v? has type Optional<A>, how do I write Optional<Optional<A>>? (And if you think that type doesn't make sense, you haven't really internalized the "parse, don't verify" rule.) If v? doesn't have type Optional<A>, why the fuck both types exist and when should I use each?
It can more clearly show intent. Is is similar to the argument for using const in javascript: you should use const as much as possible so that when you see a let you know that it is going to be mutated at some point.
Both the term "platform types" and the exclamation mark symbol are confusing, but other than that Kotlin's approach works quite well. Since Kotlin has nullability from the very beginning, the programmer can't specify platform types directly, but Kotlin still needs to support them to maintain compatibility with underlying platforms that support unspecified nullability, such as the JVM and JavaScript.
With this approach, the defaults are still sane[2] (always non-nullable by default — everybody who thinks otherwise has learned nothing from Tony Hoare[2]), but the backward compatibility is maintained. But Kotlin got it easy, Java and C# also need to maintain compatibility with existing source code.
Neither Java's approach or C#'s approach seems ideal. The C# approach drastically changes the behavior of the code based on a compiler flag, while the Java approach makes the default into the worst possible choice.
I still lean towards the C# solution, because I think making "maybe-nullable" the easiest choice, means most programmers will choose it by default. Especially in an enterprise-friendly language like Java. Linters and compiler warnings would help in the long-term, but I think it will be many years before most of the Java code will be properly annotated for nullability. C# users will see more pain in the short term, but they will probably get to the promised land of clear nullability much faster.
[1] https://kotlinlang.org/docs/java-interop.html#null-safety-an...
[2] https://www.infoq.com/presentations/Null-References-The-Bill...
As a long-time Kotlin user I can only say: Nullability is nice! Once you know it, you won't want to code without it.
"[...] but Kotlin still needs to support [platform types] to maintain compatibility with underlying platforms that support unspecified nullability, such as the JVM and JavaScript."
To be more accurate, "platform types" are not used with JavaScript, but you do get something similar in that case. I haven't worked with Kotlin/JS at all, but from my understanding if you do not have proper type definitions, you have to deal with dynamic types - which are also nullable.
When kotlin see these exist. It will convert the type to null enforced type instead. Which give you a tool to gradually convert you project to null enforced type if you can't convert it to kotlin at once.
Kotlin will yell at you if you don't initialize everything in the constructor and will yell at you if you try to use a variable in the constructor before it's declared, but if you call out to another function in the constructor you're on your own and might end up with nulls.
That's not complicated to understand, but it eliminates most types of not-yet-initialized nulls in the language.
Also, isn't how you handle not-null types in constructors totally orthogonal to which syntax is used for not-null and whether there's a third I-dunno-if-null type? And is a three-way nullability system really simpler to understand?
lets say a library was written with no nullability flag selected (legacy jar). i import that jar into my code. in my code i enable all variables as non-null by default
I've caught "null!" in C# code reviews a few times: My response is usually "You can't fix your code by yelling at it."
The reality is that C#'s approach also has some nasty footguns that require understanding the nuances of the "required" keyword, and when values are assigned.
To show the reader of your code that you explicitly want this nullable, instead of just being too lazy to type?
You can then use a linter to forbid the implicit behaviour in your code.
Kotlin, another JVM language like Java, also follows the C# approach, assuming everything is non-null unless explicitly marked as being non-null, except Kotlin doesn't have backwards compatibility to worry about. Kotlin also offers a special `lateinit var` declaration for leaving a non-nullable type unassigned until initialization in another method, throwing a special exception on uninitialized access, which is an interesting workaround for some code patterns that this approach doesn't seem to cover.
I wonder why the choice to offer three options was made. Why not keep the non-annotated variables as nullable and make annotated variables explicitly non-null instead? I can't think of a reason why you would want to declare something as nullable when not declaring anything automatically makes the type nullable anyway.
I think I like C#'s approach better, although this has the benefit of being usable in a legacy code base without having to solve any of the nullability issues. Then again, the C# approach does immediately highlight nullability issues in a code base, whereas this proposal hides them the same way they've always been hidden.
Additionally, I find this strange:
> One method may override another even if the nullness of their parameters and returns do not match.
This feels like a footgun for when a developer overrides/implements some kind of callback and returns null where the overridden method specifies non-null return values.