Null-Restricted and Nullable Types
bugs.openjdk.org
bugs.openjdk.org
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.
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.
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”
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.
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.
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?
> 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.
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.
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).
In addition, the existing JSR-305 @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 the indication should be in the source, at least during the transitional years. 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. The compiler could also warn or even error out when the code is obviously being compiled with the wrong semantics, that is, if ? or ! are present but all redundant.
Regarding the class syntax, you can have multiple (package-private) classes in the same file, so I wouldn’t do that. A simple solution would be to have an optional ! or ? at the very start of the source file (ignoring comments and whitespace), which means a regex could handle it (useful for external tooling), and it would also work in the absence of a package declaration (i.e. in the default package). Another reason to not put it on the package declaration is that it isn’t meant to apply to the whole package.
null-marked module accounting {
}
Could very well be the sort of thing that comes next.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.
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.
"[...] 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.
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.
String? id(String! arg) { return arg; }
String s = null;
Object! o1 = s; // NPE
Object o2 = id(s); // NPE
Object o3 = (String!) s; // NPE
Surely at least the two first cases should yield a compiler error. The last case you're explicit so it's borderline but I'd rather see something like : String s = ...
if (s != null) {
# Here the compiler knows that the effective type is String!
String! ss = s; # OK
}
Like this no possibility of error.Java currently doesn't provide any decent (general) solution for the problem of nullability - JSR-305 is a failed spec, Optional is very verbose, doesn't work for many use cases (e.g. isn't Serializable) and funnily enough there's no guarantee the Optional instance is non-null, value types (primitive and the preview support for complex ones) obviously covers only very specific use cases.
For example they explicitly saying that standard library will not migrate to null types, at least for now.
Unfortunately this will only be checked at the runtime.
Worse, permitting this conversion at compile time means developers will ignore the warning, so we'll have actual codebases which include these conversions. Any later change to enforce nullability checking at compile time will then have a significant backwards compatibility cost.
In fact, I'd even prefer Objects.requireNonNull(s) be used instead since that's even more explicit than the cast in the last case. However, I'd also like for there to be an Objects.unsafeForceNonNull(s) that just bypasses any explicit check unless there's some sort of optimization that would otherwise prevent. The unsafe method lets you implement your own requireNonNull without adding a bunch of complicated static analysis.
this is only an issue at the boundaries of our code where we interact with libraries. i imagine that will be the case with this new syntax as well
With good tooling compiler error vs lint error is a distinction that matters about as much in practice as parse error vs type error—your editor and CI pipelines will yell at you either way.
> 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.
Speaking from experience being forced to use php, it's a hassle to have ahead-of-time guarantees on your data that you need to strip out or build back up everytime you interact with the (massive) standard library
They should be a bit more enthused to add that kind of expressiveness to their STL and make it a first class citizen in the STL
It took two major versions to add types to both function arguments and return values and there's still no type-restricted data structures. Think "array of foo".
Just overall, other than with bash, I've never shot myself in the foot more often than with php
I despise that thing with a passion
I don't generally like duck-typing languages, but PHP is no worse than its competitors.
I find the biggest argument for or against PHP in the context of these languages to be "I find dollar signs aestethically (dis)pleasing". It's a perfectly valid preference to have, but people talk about PHP like they're still porting Wordpress plugins to PHP 4.
Additionally, separating it into two steps allows them to more easily launch this as a preview feature, get feedback, and then commit to a design before trying to overhaul the whole standard library. If they try and do it all at once, that gives them little room to actually iterate on the feature.
Language-level support is leagues better than Optional/Maybe in my experience too because it keeps the code focused on the actual logic instead of putting everything in a map/flatMap railway.
Why don't just use any JVM languages?
This is a bad decision. Java is mostly statically typed language, why introduce another dynamic behaviour? Hope there will be easy way to promote such warnings to errors.
And yes, languages where the type systems force handling null/nil in all cases is a lot nicer. But that is not what the current Java is. This will still be a big improvement.
It seems we need some annotation akin to @Deprecated(forRemoval = true). Something like @NotNull(willBeEnforced = true) that would only emit a warning but informs the user that soon there will be a breaking change related to nullability.
* things should be non-nullable by default
* things should be immutable by default
* things should be of the narrowest scope by default
So many design decisions for new things make the trade-off of immediate convenience instead of "falling towards the safe path" (which requires much more careful design and UX). And we have sooooo many footguns as a result in almost every language, platform, and technology thanks to this cowboy mentality.
Civil and electrical engineering have codes. We have lessons-learned, which we re-learn every 30 years or so with the next batch of languages and techs.
"We" and "Java" are not the same.
Java is like a third world village learning about boiling water killing germs.
IntelliJ will issue you a warning for using an Optional either as a field, or a method parameter.
It's about making our systems human-tolerant. Humans are absolutely terrible when it comes to consistent (or even correct) behavior, so we must design our systems such that the path of lowest cognitive load is also the path of highest safety and least astonishment.
For example, C++ has the concept of a const method. And that's great, because you can label which methods won't mutate an object's data. But it's actually backwards. It should be that mutator methods must be marked as such (because they're the ones with a higher likelihood of causing problems), whereas any non-marked method is assumed to be non-mutating.
In Rust, you must specially mark mutable objects. In Kotlin, you must specially mark nullable things. Each of these newer languages is taking on some of the lessons learned, but nobody is taking a serious holistic look at human psychology and how it contributes to system failures - it's always just a few pieces here and there that the designers had learned along the way. And then someone comes along and points out a lesson they didn't implement and everyone goes "Oh. Oops."
You don't get that in civil engineering because the lessons have been codified (and people who skimp out on them are liable).
Psychology really needs to have more of a center focus when designing software systems, because we can't take the human fully out of the works.
I don't think that's the case here. It's not "Oh. Oops". It's "Yes, we like it that way".
I think that readers of Java source code have the right to see a BufferedReader, and think "That's a BufferedReader".
Writers of Java source code have decided that that's too onerous. They would like the 'opposite' right - of taking a 0x0000, and declaring that to be a BufferedReader. That's really the core of it.
The debates always involve adding extra types, annotations, IDEs or external tooling into the mix, as if nulls naturally arise in nature, and some genius-level engineering is required to mitigate against them. But people routinely include them in their source code. I don't think you can win the war on accidental nulls if you can't swing opinion on deliberate nulls.
Then of course Rust came along and I'm questioning even that level of tolerance for them.
Java was an early 90s attempt to get C and C++ programmers to write safer code with the technology of the day. The design goal of the language was to get half way to LISP or ML for those C and C++ programmers. In an alternate universe where they had just made Common LISP, does it succeed? Perhaps not. Also consider that Java is a cross platform language, that must be backwards compatible, and work the same via a vm across architectures and operating systems, and basic things like just make variables non-nullable by default are not as simple.
* Haskell
* Rust
* C# (barely) with other .NET languages
Any others?
If "Java is like a third world village learning about boiling water killing germs" then how do you call other main programming languages - C, C++, JS, Python?
Everything is riddled with foot guns and escape hatches but references can't be null.
And also - you can get nullable references. The following compiles (and often runs) without a problem:
int functionWithReference(int &ref) {
return ref;
}
int main() {
int* pointer = NULL;
return functionWithReference(*pointer);
}Also the above will not compile. Because compilers can actually detect if you set a pointer to nullptr and then use it. Here, you didn't use it so maybe the compiler won't complain. But in the general case yes it will complain.
You can also get Null references in C# and Java in this same method. You call some unsafe code that directly interacts with memory. That's allowed, and yes it breaks the type invariants.
Point being, if you truly want to argue C++ is weakly-typed then you'd also have to argue C# and Java are weakly-typed. Most people don't do that though, so it's just a matter of bias or familiarity.
I love C++ to death, but calling C++ strongly typed when the only type enforcement that is actually done is either static typing at compile time (which a lot of casts can circumvent) or dynamic_cast at runtime is actually laughable. Like, there are literally 2 casts almost entirely ignore the static type system in reinterpret_cast and const_cast. This isn't even talking about the C style cast that still is possible, which can be any of static_cast, const_cast and reinterpret_cast depending on the context.
C++ will happily run with an invalid type assumption you introduced at compile time, while in both C# and Java you will have a type cast exception. C# and Java do runtime type checks at basically every cast.
Weakness consists of all that the type system doesn't catch.
Like oh, for instance: C++ local variables become pixie dust with terminates. Yet an address of these variables can escape from the scope. The type system does nothing to catch this.
Or: the pointer to a Derived can be converted to a pointer to a Base implicitly. But then pointer arithmetic can be applied. Even if there is an array of Derived there, the displacements are wrong.
Most of the unsafe features from C are present, like argument handling in variadic functions.
Explicit memory management. Code would work with an object may be perfectly well typed as far as the data representation goes except oops something already invoked the delete op on the object.
You can have code that's perfectly fine except oops when exceptions happen.
If C++ were type safe, they wouldn't be all these rules on how to write safe C++. Like do use this kind of smart pointer but don't use this other obsolete one that has issues.
And, to be clear, that behavior ALSO exists in C# and Java. You can absolutely 100% ignore the type system in Java and C# and its supported by the standard. Just like C++.
So if that's your own standard for what is weak, then you must also admit that Java and C# are ALSO weakly typed. But clearly you don't believe that, so you have some sort of logic problem here.
In my opinion, people see C++ differently because C++ code in the wild is fundamentally different. It's high performance and used in domains where that matters, so escape hatches are used more often than other langs.
However IMO the strictness/weakness of a language comes from the language itself. And both C++ and Java/C# provide static checks + runtime type checks with escape hatches.
The difference is that C++ code with raw pointers is (fairly) common.
It compiled for me (after changing NULL to nullptr).
> Because compilers can actually detect if you set a pointer to nullptr and then use it.
Apparently it didn't.
> Here, you didn't use it so maybe the compiler won't complain. But in the general case yes it will complain.
That's incorrect, there's a use of the nullptr.
> You call some unsafe code that directly interacts with memory.
I don't see something explicitly declared as unsafe from the source code alone. It looks like a normal pointer dereference to me.
No, there isn't.
> declared unsafe
Please, I have no patience for the purposefully obtuse. It's clear I was referring to other languages there.
Which, you failed to address. Probably because it was difficult, so you figured if you just said "I'm right" and elaborated nothing nobody would notice. Sorry, that doesn't work on people who are awake.
As I've said, you can argue that C++ is weakly typed but you MUST also address languages like C# and Java to make that argument. You haven't, so nobody will believe you when you say C++ is weakly typed.
fn function_with_reference(reference: &i64) -> i64 {
reference.clone()
}
pub fn main() {
unsafe {
let x = 0 as *mut i64;
function_with_reference(&*x);
}
}
This also compiles, runs, and segfaults. And it doesn't mean that references in Rust can be null - they can't.Your problem is with undefined behaviour or perhaps raw pointers, but not references. The reference itself isn't what's messing this up!
That way of obtaining a null reference is actually quite portable.
Where you going to run aground is when you have some null reference in some scope, and you have some code conditional on it being null. Yet the optimizing compiler assumes that since a reference is being checked for having a null address that comparison is always false.
From the TIOBE index list[1] (pick any other list if you want):
- Only 3 languages from top 20 (Rust, Swift and Kotlin) started with null safety type features, some others developed this over time (JS with TS, Python type hints)
- Only 2 (Rust, and Kotlin) languages AFAIK are immmutable by default
It is no coincidence that all of these languages are modern and created after the 2000sF# also is immutable by default. Maybe it's not on that "top 20" but 20 is an arbitrary cutoff. There are many so many languages being used in production that it's hard to imagine "20" is a defensible sample.
It's also hard to argue Python has any type safety with "type hints". The interpreter doesn't do anything with those hints. Linters and safety don't belong in the same sentence.
All in all, that seems like a bad source.
But it's probably more important than all of these languages have been heavily influenced by the ML/Haskell family of languages. Most ML-influenced languages forbid null references, discourage them or make nullability a part of the type signature. I think the ML-heritage is the key factor, together with Tony Hoare's influence.
Languages released shortly after 2009 that have ignored the ML tradition like Go and Dart did not have null-safety. I believe ML-influenced language designers were more attuned to Tony Hoare's call for action and had a good idea about how to make null-safety work smoothly an industrial language.
Fortunately for us, nowadays the language designer doesn't have to be a Functional Programming aficionado anymore: null-safety has proven itself as relatively easy to implement and too useful to ignore.
---
[1] Rust is the odd one out here: Graydon has started working on Rust as a personal project back in 2006, but that language is bears little resemblence to what we call Rust today. Rust only became more than a personal project around 2009 and the first release (0.1) only came in 2012. Looking around, I find no reference for avoiding null reference in Rust before 2010.
Null is a perfectly valid and very useful feature, if modeled in the type system. A nullable reference of type, say, "T?", would accept values accepted by T or the special value "null", which is equivalent to a union type of "T | Void" (or whatever type is assigned to "null" in your language).
The billion-dollar-mistake is not the existence of null references, but the inability to model nullability in the type system, thus making references implicitly nullable. You can ban the concept of null from your language, but you're inevitably going to add lots of syntax sugar or helper functions to unwrap "Optional<T>" or "Maybe t" values, so making nullability a "type modifier" has equivalent behavior and, IMO, better ergonomics.
No it isn't, because union types leak through generics in surprising ways. For example:
// value is null if not loaded yet
struct AsyncSnapshot<T> { value: T? }
// returns null if user is not found
fn getUser(id: Id<User>): User?
fn runInBackground<T>(f: () -> T): Stream<AsyncSnapshot<T>>
If you run `runInBackground({ getUser(id) })`, `AsyncSnapshot<T?>`'s `value` gets the type `T??` which is typically coalesced to `T?` and you can't tell whether it's `null` because there is no user, or because it's still loading.Sum types like `Option<T>` do not coalesce, `Option<Option<T>>` is distinct from `Option<T>`. Better yet, you can declare your own clearer sum types, so you can have `MaybeLoaded<MaybeExists<User>>` rather than have to remember which layer is which.
As they clearly write at the end, they will think of additional proposals, like making non-nullable the default on a per-class/package level. But they don’t have the luxury for that. It’s like, possibly the best language team on earth did think of this tiny little obvious detail.
Some questions though.
First, what about nullable arrays? There are examples of String![] where the objects can be null but what about the array itself? As a reminder this is entirely legal in Java:
String labels[] = null;
Does that mean you have to declare it: String![]! labels;
?In Hack, this is easy:
vec<String> $foo; // neither foo nor the elements are null
?vec<string> $foo; // the elements are non-null but foo can be null
vec<?string? $foo; // foo cannot be null, elements can be
?vec<?string> $foo; // either can be null
In practice, there's really little reason to ever use a null array so the default really should be that something isn't nullable. I understand the issue with Java is that all legacy code assumes nullability so that's an issue. String? id(String! arg) { return arg; }
String s = null;
Object! o1 = s; // NPE
Object o2 = id(s); // NPE
Object o3 = (String!) s; // NPE
As for this example, shouldn't (2) and (3) be compiler errors?As for the last, I really like Hack's as enforcement operators here rather than Java's casting eg:
class A {}
class extends B {}
void foo(B $b) {}
?B $b = new B();
foo($b); // compiler error
A $a = $b as A; // runtime error if $b is null
foo($a as B); // runtime error if $a is not a B
?B $b2 = $a as ?B; // $b2 is cast to B if it is one, otherwise null is returned
Anyway, the question becomes can you retrofit this to the Java SDK? What does it look like for legacy code? String![]? labels = null;I will still prefer to work in Kotlin though as I don't have to deal with things like lombok though Java records are nice.
I think this question needs to be answered now. The addition of ? is useless and confusing unless this is planned to eventually happen.
I am inclined not to introduce breaking changes and to only have !.
The lack of a mark means you have to consider it nullable but no one has told you that setting it to null is the right thing. Things may still go wrong if you set it null though. It’s just the contract isn’t explicit in the code.
String? a = null;
String b = null;
String? c = "";
String d = "";
String! aa = a; // Compilation error
String! bb = b; // Runtime error
String! cc = c; // Compilation error
String! dd = d; // Works!Java should take a look at its own history and how it was able to retrofit generics into the language. It's nearly all type safe and the unfortunate unsafe parts generate compiler warnings.
I was wondering how "enforceable" these nullness check will be and how they will affect generics (will a "List!<String?>" be treated like a non-nullable list of nullable strings? and a List<String!> ? is the "unknown nullables" forwarded to the elements?)
Probably in some years from now once codebases exclusively use this feature, there would be a way to tell the compiler that the default type (without marker) it is a non-nullable type.
[Source](https://blogs.oracle.com/javamagazine/post/what-are-they-bui...
// Java
Baz bar() { return new Baz(); }
// Kotlin
val foo = someJavaObject.bar()
foo now has type Baz! but the moment you declare the type of foo explicitly you're forced to pick, and an NPE check is done at that moment.It seems to me that something somehow being assigned a null is a bug, but one that would stick out like a sore thumb. At some point, you're returning a Null, right? Any code calling a function that can return a Null should know that being handed a Null is a possibility and handle it, right?
I'd think that eliminating Nulls is a bandaid over the wrong problem. Or is eliminating Nulls really meant to catch any potential NPE's at compile time as a way for force better error checking?
Well right now, the only way to know is to read comments/docs. The problem is that for many older languages, the signature cannot make it clear that null is a possibility (unless you count "anywhere can be null regardless of the function", which isn't helpful).
The goal is to have a real distinction between can be null and wont be null so that things can be made explicit and the compiler can actually highlight places where the handling is missing.
It's a tool to help do exactly what you describe in a less error-prone way.
Everyone everywhere always being more careful is the bandaid solution, if you lift the extra non-optional sum type into the type system you solve the problem permanently.
That's not what is happening here. The issue with the current iteration of Java is that all type declarations implicitly include `null`. So you can do for example:
// some method
public String getSomeValue();
// client code
String someVal = obj.getSomeValue();
At that point, the caller of the getSomeValue() can't depend on someVal being non-null. You say "Any code calling a function that can return a Null should know that being handed a Null is a possibility and handle it, right?" and that's really the problem. ANY method that returns an object type can return null, so, in theory, it would force all callers to do stuff like `if (someVal != null) ...`, which is really annoying if it has to be all over the place.This proposal makes it clear to callers whether a method can return null, or if it is guaranteed by the type system not to return null. That way, it is now possible for a method to state "I guarantee that I will only return a String, never null", and previously that was not possible (directly in the language at least - there were some kludgey workarounds like the Optional generic type). So this means that clients don't need to do tons of unnecessary null checks.
This is definitely not an issue specific to Java. For example, TypeScript is clear that its types do NOT automatically include null, and it makes coding SO much clearer. For a type to include null (or undefined, but that's a whole other JS-specific ball of wax...) you must explicitly write e.g.:
const foo: string | null;Which is easier to understand? I don't mind a few extra keystrokes if it improves readability.
private String? foo;
private String! bar;
// or
private nullable String foo;
private nonnull String bar;understandable in a way but I'm more fond of types vs syntax
private String? foo;
private String! bar;
// or
private String foo;
private nonnull String bar;Explicit nullability fixes the missing information in older code. There could be other ways to mark opting into strict nullability through metadata at the class, module, package or VM level; but fine grained markers are the less disruptive for gradual migration. In particular this keeps the information local instead of relying on an ambient context.
This reminds me of some discussions about the "weirdness budget". When you introduce some new feature, you want it to stand out and be noticed. But as people get used to it, a terser syntax is fine. Some example are callback syntax in various languages introducing a short-hand form when it got popular, or the move from the `try!` macro to the `?` try operator in Rust. You have to consider a longer time horizon for Java's nullability markers. They may be weird at the start, but I feel that the lighter syntax is a better trade-off when you consider their usage 10 years in the future.
Overall, I feel pretty happy with this proposal given their backwards compatibility requirements. I just wish that it had come earlier so `Optional` could enforce non-nullability of its inner type.
That's exactly why I'm not fond of the question mark, that already has an overloaded meaning in generics. Foo<Bar>, Foo<?>, Foo<Bar?>, Foo<? extends Bar>, Foo<? extends Bar?> look pretty confusingly similar. One could argue that ? and * were terrible symbols for use in generics in the first place, but that ship has sailed.
I'm sure people (myself included) could easily get used to the syntax, but habituation is not going to stop me from being grumpy :-)
I've always found the "? extends" syntax to be confusing, and I feel like the question mark doesn't even need to be there on a syntax level. I also feel like on a language level, Java shouldn't even need a "? extends Bar", but unfortunately Java's generics system isn't strong enough to work without it.
And then it gets worse, with Foo<?> and Foo<? extends Object> being slightly different, even though it makes no sense at all.
Golang's use of capital first letter to denote public is the most egregious use of this, for me. Completely non-obvious to newcomers and hard to search through code.
As others have said, this is a very common convention in many languages now. You get used to it in a day. It also opens this door for nice sugar like the ?? operator.
For new features, people insist on LOUD explicit syntax.
For established features, people want terse notation.I find the question mark to be quite clear ("its a string, but is it? Could be nothing!"), and I find the exclamation mark to be a clear opposite of the question mark.
This annotation is also very common in other languages. I don't think it makes sense for Java to invent its own notation here.
After a ten-second explainer? The one with the punctuation marks. This is base vocabulary intended to be used very frequently, so it should be compact and unintrusive.
> Equal signs make sense for assignment and equality because of mathy history. But question marks and exclamation marks don't make much sense for nullability.
I think they do. The question mark expresses uncertainty ("does this contain an actual String?") and the exclamation mark expresses certainty ("this definitely contains a String!").
int a = b plus c;
It's easy to misguide people with regional words, symbols are universalPlus, Java libraries used @Nullable and @NonNull annotations for >10 years, I doubt they would confuse anyone.
[0] https://en.wikipedia.org/wiki/Division_sign
[1] https://en.wikipedia.org/wiki/Assignment_(computer_science)#...
Furthermore, Java is not correctly reinventing the wheel, both `?` and `!` have a lot of prior art as nullability type modifiers e.g. TypeScript, Swift, C#, Zig, … not to mention null-safe operators for which they are absolutely ubiquitous.
I prefer using long options in shell scripts for similar reasons. Easier for future maintainers to search for.
Personally I've never found nulls unintuitive or hard to use. Changing the core of the language to align itself with another fleeting in vogue trend is going to leave us with a bunch of garbage and tech debt when the trend inevitably falls out of style.
I don't think wanting not-null is going to be fleeting, but I'm sure with Java's long loooong 30 year history there have to be other "in vogue trends" that have left tech debt... Can you point to any?
There's something to be said about the difference in philosophy here though. Some languages are much more focused on facilitating particular paradigms which is also completely reasonable. Java and C# are well-positioned in the industry to support many optional features and have that be a selling point.
To your second point, I mean it's great that nulls are easy for you to use, I'm happy for you. But there are bad and good developers the world over that are contributing to bad practice with null handling when there aren't any guard rails. And not even necessarily because they don't understand null, but in some cases just because they don't think about it. Warnings are a reasonable solution to that IMO.
String? y = null;
if (x != null) {
y = x.foo;
}
might trigger a null pointer exception. I don't mind the static error, if say `y` is not nullable but `x.foo` is nullable. But that's not at play in this code. Rather, these exceptions in the JEP come when `x.foo` is a `String!` but for various reasons either it hasn't been initialized yet and perhaps this code is occurring in a parallel thread, or due to various chicanery some null somehow was stored in it. (In the JEP the big case would appear to be that `x` is in this case a class with a static attribute `foo`? "Note, however, that this rule does not prevent some other class from trying to read the field during the class initialization process; in that case, a run time check detects the early read attempt and throws an exception.")To me, this is a minor change to "the core of the language" that betrays some bigger, more important change that has yet to be made to "the core of the language." Something like null tracking at the JVM level, or forbidding concurrent access to things that are being initialized... or maybe just that classes need new rules around what you can/can't do in their static initializations... some big semantic shift.
Also, one of the more intricate parts of TypeScript has to do with types-as-unions, you want to be able to analyze code that says something like,
if (x.foo == null) {
return;
}
String! y = x.foo;
and make the analysis step that this is legal and type-safe. But TypeScript can only do this because JS multi-threads on a message-passing model. Java doesn't do this, there is a race condition where `x.foo` was not null when the check was made, but then this thread slept for a millisecond while another thread with visibility of `x` modified `x.foo`. So in Java I think you need to correctly mark the above typecheck as "does not pass" while the closely related version, String? z = x.foo;
if (z == null) {
return;
}
String! y = z;
would presumably need to pass static analysis because you can determine that `String? z` is not reachable from any other thread. So your static type-system now needs to have some heuristic "reachability system" that it appeals to, and it has to be a heuristic because you're not going to reimplement something as sophisticated as borrow-checking in Java.Every OOP language can behave badly at the data constructors. Every single one has some lightly-communicated rules that you must not break and are hard to verify.
> Personally I've never found nulls unintuitive or hard to use.
That's not the argument. The argument is that it's easy to mess up their use without realizing it, leading to bugs that only show up intermittently at runtime.
I'm just disappointed that this JEP doesn't go far enough; the compiler/runtime will automatically narrow types (e.g. auto-cast from String! to String), without warning, and possibly throw NPEs. That should be a compile-time error.
Casually just describes one of the largest sources of bugs in the history of programming.
JSpecify takes advantage of Java 8 features (e.g. annotations on generic type parameters), so it's strictly better, and I believe at least the IntelliJ IDEs support it.
[1]: https://jcp.org/en/jsr/detail?id=305
[2]: https://central.sonatype.com/artifact/com.google.code.findbu...
But they never made it into the language, so it’s only ‘enforced’ by the IDE and it’s a lot more visual noise than ? or ! which I’m already used to from other languages.
I can’t wait, I hope it gets in.
There's a huge inertia to "strictly wrong, but already existing", so there is a bias to keep maintaining those things.
(And there are - at least - hundreds of thousands of programmers with huge status quo biases. We saw this mostly as C started to lose a lot of ground, finally, to safer languages with infinitely better tooling. It's that too many people put up with bad bad tools, mostly because they prefer very very incremental changes, and mostly those that give them results in "functional requirements".)
Most of the innovation when it comes to concurrency, garbage collection, JIT did come from Java. Syntax - that's more of an opinion/preference.
My gripe with yet another "improvement" of the language is still the lack of headerless objects (aka value classes) - JEP 401[0]
Wild statement. Java certainly was incredibly innovative upon release. It didn’t explode in popularity by accident.
Core Java was staunchly against incorporating any functional features while the rest of the ecosystem was innovating
I get the feeling Java seems to have suffered from its early success, pioneering things like generics in GC languages, which competitors like C# were able to do better, but only because Microsoft decided to break compatibility between .NET 1 and .NET 2 or because there was no compatibility to break. After that, the language seems to have shyed away from innovation out of fear of breaking things for a long time.
In recent years, this has changed, but Java has a lot of language features to catch up on. Project Loom (green threading without coloured functions) is the first innovative thing I've seen Java do in recent history.
Here is a list of GC languages with generics that predated Java:
CLU, ML, Standard ML, Caml Light, OCaml, Miranda, Haskell, Sather, Eiffel, Modula-2+, Modula-3.
> Java is not an innovative language and the fact it is evolving towards something better...
So basically C#?• The feature is compile-time only (unsurprising considering the type narrowing approach is very similar to how TypeScript works)
• It interacts weirdly with Nullable<T> for structs since that already came before, uses some of the same syntax, but works differently because it's actually a type at runtime.
Java seems to have the nice position here to make it consistent across value types and reference types (assuming Project Valhalla is ever done) and they seem to have opted to retain the types at runtime as well which will cause runtime checks. This may cause gripes about performance, but from a correctness standpoint it's definitely much nicer than just having APIs that tell you that null won't ever occur, only to still have that problem in certain cases.
Just like 60% of Azure runs Linux workloads, as per official numbers.
Interesting the rounds that the world makes.