Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Null-Restricted and Nullable Types (openjdk.org)
224 points by lichtenberger on Aug 2, 2024 | hide | past | favorite | 227 comments


The difference between this approach and the approach C# took when they implemented this a few years ago is intereting. With C#, you enable nullability in a project, and every variable is declared as non-null unless you explicitly mark it as nullable (or use compiler directives to disable nullability for a block of code). With this proposal, all existing variables will be effectively nullable, explicitly nullable, or explicitly non-nullable.

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.


I'm currently involved at work with migrating a legacy C# code base to nullable reference types. One thing I'd really like is the explicit marker for non-nullable since it makes it explicit which parts of the code have already been looked at and annotated. For time reasons we've opted to just enable the annotations for now except in a few core places. But we still retain the JetBrains NotNullAttribute and CanBeNullAttribute as markers for now to immediately see where we've made a conscious decision (granted, the latter can be removed since nullable has an explicit marker, but the former has a name conflict with C#'s own feature).

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.


What we did was to turn on nullability as warnings everywhere. We then went through them project by project, and fixed them. After we were done with a project, we'd turn on WarningsAsErrors for nullable warnings, and know the project didn't need additional fixes any more.


Would have loved to do the same and that's my plan eventually, too. Alas, no time for fixing right now. And as I've already noticed when migrating from JS to TS or non-strict TS to strict TS, there's a whole lot of issues that require thought and investigation whether everything is fine from a nullable perspective. I have found quite a few issues, even outright bugs, but when there are more pressing issues than a few lingering NullReferenceExceptions that no customer has stumbled over in the past decade, then those get prioritized.

At least new code is forced to deal with those warnings for now, which is much easier than fixing legacy code.


Kotlin does have types with unspecified nullability, although you cannot define it directly. Kotlin calls them "platform types"[1], and they're annotated with an exclamation mark (e.g. "String!" for a possibly-nullable string) in errors and diagnostics.

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...


you failed to mention that Kotlin has platform types only if it's interoperates with Java. Platform types are for objects that come over from Java code (and therefore have unspecified nullability (assuming the Java code doesn't use nullability annotations)). Admittedly you provided a link that explains this, but how many people will follow the link? And yes, this detail feels kind of important. And yes, future Java (and Kotlin) will only have their version of platform types to interop with old Java code.

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.


The type kotlin generated from java code also inserts nullable specifier if java code have @NonNull or @Nullable specified.

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.


I did mention all of this:

"[...] 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.


I agree that it is strange. What I would have expected is (1) that on the source-code level T? means nullable (the existing semantics) and T! means non-nullable, and that the default for plain T in newly compiled source code can be specified using a pragma-like declaration per source file, per package-info file, or globally by a compiler switch, and (2) that a subsequent LTS Java release would switch the global default. This would allow easy switching of projects (you can insert/update the pragma in an automated fashion), and also allow keeping the old default when needed.

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.


Sounds pretty good. But i think the mode selector has to be in the source code where a reader can see it, which rules out a compiler switch.

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;


The compiler switch would be for emulating (or for disabling) the global switchover that we want to take place at some point. We currently have the wrong default, so to speak, and we’d want to change that at some point. Then it is useful to already be able to do that in earlier releases, and also to be able to revert to the old default in later releases.

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.


To use the terminology from JSpecify:

    null-marked module accounting {
    
    }

Could very well be the sort of thing that comes next.


At the end, there is this section:

> 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.


I suppose the reason is backwards compatibility of code, something that Java values a lot more than other languages.

Still, I also suppose there's going to be a compiler flag that auto-assumes non-nullability unless specified otherwise.


On the one hand Java is very strict about backwards compatibility (still reserving keywords like const and goto even though they're not used (anymore)), but on the other hand Java 9's module system broke just about every Java <=8 project I've ever seen.

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.


Another problem with (especially older versions of) Java is that the language was so restrictive that many of the popular frameworks rely on reflection and/or bytecode manipulation and these things can and do break frequently across JDK or JRE updates.


> Java 9's module system broke just about every Java <=8 project I've ever seen.

As far as I understand, some IDE's and built automation tools did not rollout support for module system immediately, yes?


It took a while for IDEs and tools to catch up, but I still encounter issues with old codebases to this day, using the latest and greatest tools.

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.


Mixing a Java >= 9 project with Maven libraries made for <= 8 is one hell of a headache.


> but on the other hand Java 9's module system broke just about every Java <=8 project I've ever seen

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”


JDK 9 was the impetus, but the real nail was, I think, 17 when a lot of the constraints imposed by the module system became required. Before that folks could pretty much continue as usual with little regard to the module system.

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.)


> 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.

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.


That’s not backwards compatibility, but forwards compatibility, though. And you would really have to go out of your way to introduce such an issue. Certain extreme (e.g. native calls) cases are handled by simply pinning a virtual thread to a real thread. So it would only cause an issue if it was already causing an issue at much smaller thread numbers.

And many of these extreme cases are getting “solved” to not pin the thread.


You're confusing the thread pinning problem with "breakage". The problem with some synchronized blocks is that they lead to thread pinning for the duration of the lock. Why is thread pinning bad? Because it means that if one core is idle, but all the threads are pinned to a specific core, you won't get full CPU utilisation. This undermines the performance benefit of virtual threads, because they are supposed to be schedulable via work stealing.


> 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.

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.


My experience with kotlin and c# is that in kotlin it just works, even with java interop, while in c# nullable value types seem like a far leakier abstraction.


> 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.

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.


Optional<> is such a weird addition to the language. I hope once this link lands, Optional<> will go the way of the dinosaur.

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.


I don’t even prefer it for indicating that something can return null. In most cases the Option API is more tedious and awkward than a null check and there are existing annotations that can be used to flag nullable return types, for documentation. The only places I’ve found Option to be helpful are where it helps with method composability, or in some hashmaps as a simple way to distinguish between “lookup not previously tried” vs “lookup resulted in no value” vs “lookup resulted in value” - it’s a hack and looked down upon because the Option value itself is null, but I view it the same as “Boolean” being nullable. As long as the details are encapsulated it’s not too problematic.


I use Optional quite a lot, but only ever as a poor man's

   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).


Indeed, Optional<> was designed/intended to be used in this way.

https://stackoverflow.com/questions/26327957/should-java-8-g...


Unfortunately for Java, Optional in Java is a bit of mess now, until you'll be able to specify nullability. Since T in Optional<T> has unspecified nullability, calling Optional.of() is still unsafe and can result in an NPE, if the argument is null.

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.


>but this could still easily happen by mistake.

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.


I do remember seeing this happen firsthand, but I don't remember where.

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.


While applying practices like treating warnings as errors on a CI server is something that every development team probably should be doing, it’s more a symptom of a generally weak development team than the thing that was holding the team back.

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.


All the value like types, like Optional, are planned to be value classes or value records, when Valhala lands.


Optional<> is a problem with all languages that add nulability control as an afterthought.

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?


> 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.

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.


As someone that works across JVM, CLR and Web ecoystems, dealing with C# approach is still a mess, unless one is lucky enough to only work in greefield projects.

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).


AFAIK you can still use it for older frameworks. The compiler embeds the attributes into the assembly when they're known to not be part of the runtime library [−3.7]. You can do the same with the various conditional nullability attributes.

[−3.7]: https://sharplab.io/#v2:EYLgHgbALAPgAgZgARwExIMJIN4FgBQSRKyc...


The one nullable gotcha with doing this is that the .NET standard library doesn't have nullable annotations in older frameworks.

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!


Yeah, I've been through that recently as well. The fix for now was to temporarily switch to .NET 8, apply a code fix that adds the inherited nullable annotations throughout the whole solution and revert back to .NET Framework 4.8.

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).


Yeah, those are the kind of tricks I don't like playing, because when things break down, someone has to sort out the mess.


I think this is all a compromise to allow the introduction of this gradually in a code base and across the standard library. You want to start marking these intentions in interfaces and base classes, but you don't want to immediately invalidate all existing implementations.


Its a footgun, but one chosen for practicality. There have been a number of other nullability annotation frameworks for java in the past that are more complete and correct, but also more difficult to use and understand for a typical developer. A classic example is "what about nullness of fields defined in constructors?" After all, there is a real moment in program execution where those fields store nulls. So people try to add things to represent "nullable, but once non-null it remains non-null forever" or whatever and then people don't practically use the system because it is too much conceptual complexity.


Another practical answer is to just accept that an edge case like that is one we won't completely solve for.

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?


> 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.

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.


how does the project level null flag work for libraries?

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


This way Java can claim that it has this feature when comparing to competing languages, without interfering with all the legacy code. It doesn't matter if it's not useful, some manager can just run down a checklist and pick safe ol' Java.


> 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.

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.


Looks good, finally a language-supported way to remove thousands of unnecessary Exceptions and null-checks. But the nullness-narrowing automatic conversions feel wrong. From the proposal :

  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.


As someone who has written Java in industry, I'd rather dynamic checks only happen when I explicitly ask for it and have everything else done statically. Bean validation works great since while the object might be incorrect temporarily, I know it's valid as soon as I validate it explicitly (or it gets validated by the framework before entering my code).

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.


"It is not a goal to require programs to explicitly account for all null values that might occur; unaccounted-for null values may cause compile-time warnings, but not compile-time errors"

Unfortunately this will only be checked at the runtime.


I initially interpreted "unaccounted-for null values may cause compile-time warnings, but not compile-time errors" as meaning "in some cases, an unaccounted-for null value might not cause a compile-time error", but in the context of the rest of the spec, I think it actually means "unaccounted-for null values are not permitted to cause compile-time errors, only warnings", which seems like a bad idea to me. I can see why allowing implicit conversion from unannotated "Object" to "Object!" is a reasonable compromise to work with existing code, but I don't see why conversion from "Object?" to "Object!" would not cause a compile-time error.

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.


If the first or second example would cause compiler error, it means that you would need to annotate every library usage. Your code will be full of casts on almost every line, until the given library would migrate to null types, if ever. It makes no sense.

For example they explicitly saying that standard library will not migrate to null types, at least for now.


You can still work with the nullable types, as is, from these libraries. You only need to cast/check when you want to use the non-nullability feature.


Is it really though? If your problem is introducing null's into the API, I'm not sure that's a language error. I mean I get it, its verbose and cleans things up. But isn't the problem the coders doing it?


Coders work with what they have. The work itself is shaped by the tools the workers use to produce it. If you give workers inadequate tools, you can't expect high quality.

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.


Not a fan of Optional, or streaming in general; where I've seen it/used it its basically ruined the codebase. I guess I find this defensive stuff a bit on the nose. If its not null, you're still gonna be testing be Non-Optional.


Yeah, the second not being an compile error allows for legacy code to call functions with notnull arguments but I do not think that is necessarily a good thing. Hard to say without using it in practice though.


Compiler will insert null checks in the function invocation. It's not a problem. And if you can't gradually migrate old code to null types, it means that most projects will never utilize those types.


Yeah, the more I think about it the more O agree with the design even if it surprised me at first.


IMO there really needs to be a way to mark all variables non-null by default at package or at least a file level. Otherwise there will be a strong safety argument for always using T! syntax on almost every variable and this will just be a lot of noise.


Under "other possible future enhancements":

> 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.


i don’t think it will be that bad. we have a standard in our projects today that all java variables in code we own are not null. if you would like to have a nullable variable, you must annotate it with @Null

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


If I understand you, then you're not getting any compiler check support for misuses, correct?


I have to assume they have a lint rule for that.

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.


I suppose an aggressive linter could assume unmarked should be treated as non-null and at least demand nullability markup where needed.


>It is not a goal (at this time) to apply the language enhancements to the standard libraries

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


Honestly one of the biggest problems with Java is that they are incrementally improving it, but then the new minimally improved code becomes legacy code for the next incremental feature. Legacy code that uses Optional is a liability for any proposal that uses explicit nullability and non nullability. Record types could have been non nullable by default and so on...


I think you're reading "at this time" as "never" when in reality, they want to do that separately since that sort of conversion is a huge undertaking in its own right.

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.


Key phrase "at this time". I'm sure it will become a goal once the feature is established.


forced? modern php is a bliss.


Having worked in modern PHP I have to disagree. Modern PHP is much better than it used to be but it is still much worse than any of the common alternatives (JS, Ruby, Python, Java, ...).


It's hard defending php in 2024 when you've used the alternatives that exist...

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


PHP is fine. It's like Python or Javascript, but with dollar signs.

I don't generally like duck-typing languages, but PHP is no worse than its competitors.


The standard library of PHP is still the same old crappy standard library that it used to have with only some minor fixes. The language itself is not that bad (still worse than Python, JS or Ruby) but the standard library is awful. One thing I have to give modern PHP is that Composer is pretty neat, better than NPM and the packaging mess of Python and roughly on par with Rubygems/Bundler.


The PHP standard library has issues for sure (shout-out to real_escape_string), but so do Python and JS (I can't speak for Ruby as I've never used it). JS's "hide everything behind a few magical objects" (Math, window, document, etc.) approach isn't much better in my opinion. Python is a bit better, but mostly because Python 3 was a clean break from Python 2.

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.


I do not care at all about the dollar sign, but I have worked both in PHP 4 and in modern PHP and I do not think the language has improved much. Other than Composer the dev experince was basically the same. It is still not up to par with Ruby or Python.


Would be cool if Java got this feature, explicit optionality at a language level a la T? is an enormous developer QoL in Kotlin and Typescript in my experience. In Java there's tools like NullAway [1] but they're a hassle.

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.

[1] https://github.com/uber/NullAway


Breaking 30+ years of source compatibility? Why?

Why don't just use any JVM languages?


Some us actually work on real software with dozens or hundreds of other devs and not toys. We can't always just "use any JVM language".


I wish Oracle would just offer Java editions, where they break syntactical backwards compatiblity, but retain backwards compatibility on the bytecode level. Of course, 99% of the language should stay the same with most of the syntactical changes just getting rid of the annotation mess that proper Java would be.


That is already what guest languages offer, but then there is always the caveat that their subculture only sees the JVM as a bootstrapping tool for their little ecosystem.


This change would not break any source compatibility.


Archive link because the site is currently down: https://web.archive.org/web/20240802081039/https://bugs.open...


Ahh they did switch to all loved "jira" - no surprises there.


the good old hn hug of death


"It is not a goal to require programs to explicitly account for all null values that might occur; unaccounted-for null values may cause compile-time warnings, but not compile-time errors"

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.


They can't do that or it will break all java programs in existence. Enforcing that would force all programs to be rewritten to handle null responses everywhere. With this addition they can apply the forced null handling to only operations involving the new types, and thus not break anything.

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 will only break existing programs if they start introducing Type! into existing APIs. I understand that introducing ! types in existing Java libraries would be nice, but I much prefer the ! mechanism to be more "correct". I agree that it will be a big improvement either way.

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.


It's unfortunate that we're learning all of these lessons so late...

* 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.


Unfortunately it seems like Java still hasn't learned the non-nullable-by-default rule—this proposal introduces two new types, with the default un-annotated type still answering :shrug: when asked if it's nullable.


It’s almost like you can’t change the default without breaking 467482 million lines of code?

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.


It's unfortunate for Java to be learning these things so late.

"We" and "Java" are not the same.

Java is like a third world village learning about boiling water killing germs.


That is definitely wrong. Java is not the only language that does this.

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 2000s

[1]: https://www.tiobe.com/tiobe-index/


To be exact, all of these languages are statically typed and were created or at least started taking their final shape[1] after 2009, when Tony Hoare (who has the best claim for being the one who invented the null reference) coined the term "Billion Dollar Mistake" and made the final convincing argument as to why no serious typed language should have null.

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.


> [...] made the final convincing argument as to why no serious typed language should have null.

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.


> Null is a perfectly valid and very useful feature, if modeled in the type system.

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.


That doesn't make it not useful, but I agree with your conclusion.


How do you define "immutable by default"? Swift for example has different syntax for mutable variables (`var x = y`) and immutable variables (`let x = y`).


Swift is technically "immutable by default" as you have to explicitly declare a variable to be mutable with `var`.

F# 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.


It's not just Java. It's C, C++, Ruby, Python, Go, BASH, C#, Javascript, PHP, PERL...

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.


> And then someone comes along and points out a lesson they didn't implement and everyone goes "Oh. Oops."

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.


I mean, I don't know if nulls "arise in nature" exactly, but null pointers arise quite naturally in computer memory when setting stuff up and tearing stuff down. I agree that it's vastly preferable, in a PL, for nulls to be semantically distinct from live pointers. But still, null pointers model something real, and it took a lot of research to get us to the point where we could build languages that can make the semantic distinction.


I completely accept them in an operating systems context. I have a soft spot for C. But they have no business in application programming. As you say, they are about the state of your computer's memory, (not your application!)

Then of course Rust came along and I'm questioning even that level of tolerance for them.


I didn't mean to imply there's just one village learning about boiling water killing germs.


That's not true. The Java community in general has known about this for many years, and using Java's `Optional` type for anything nullable has been considered best practice. The reason the language itself has moved slowly is that making core changes to an existing language like this is hard; there are backwards compatibility concerns that are always going to be at odds with correctness concerns, and it's important to hash out the details and make sure that the eventual system they land on is the right one, since it intends to stick around for decades to come.


Optionals weren't as well-received as you make them out to be.

IntelliJ will issue you a warning for using an Optional either as a field, or a method parameter.


The purpose of Optional, IMO, is to avoid returning null, not to avoid receiving null. It is easy to validate incoming params for null or to check if an internal class field is null. It is much harder to know if some method can possibly return null.

To put it another way, Optional as a param/field helps the _author_ of the class, Optional as a return helps the _user_ of the class.

Taking an Optional as a field/method parameter just introduces a third state you have to handle:

* The Optional is present

* The Optional is absent

* The Optional itself is null

I've worked on codebases that abused this where there is different behavior for each of the three cases.

On one hand whoever wrote that code should have thought about the contract of their method a bit more. On the other hand, respecting and looking into _why_ IntelliJ is warning about this behavior would help prevent such mistakes in the first place.

But, if you really want it, you can configure IntelliJ not to warn on this inspection.


The authors of Optional intended it to only be used as a return type for library functions, to avoid well-known headaches around documenting the nullability of these functions [1].

To quote:

> For example, you probably should never use it for something that returns an array of results, or a list of results; instead return an empty array or list. You should almost never use it as a field of something or a method parameter.

[1]: https://stackoverflow.com/questions/26327957/should-java-8-g...


I think we're in agreement. This is exactly how optional should be used.


That is not a fair description to Java or the team that has created it. Do you actually think that James Gosling, Guy Steele, Brian Goetz, Doug Lea, etc. are not aware of nullability?

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.


There are very few languages that got "things" better than Java (if I understand kstenerud correctly):

* 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?


C++ has many issues but it has actually supported non nullable references right from the start.

Everything is riddled with foot guns and escape hatches but references can't be null.


C++ is weakly typed language with undefined behaviours baked in. It definitely did not get "things" right.

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);
    }


C++ is not weakly typed, it's just a low-level language. Meaning you can directly interact with memory if you so choose. The same is true in Rust, and YES you can break the type system with unsafe code. However, we understand that this is not really enough to make the language weakly typed. Java and C# ALSO have this problem. You can make everything Object if you want. It'll compile, it'll run, and C#'s ArrayList class worked like this.

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.


> Also the above will not compile.

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.


> That's incorrect, there's a use of the nullptr

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.


strongly typed != static typing

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.


Circumventing casts are not evidence of weakness. Those are escape hatches. Something normally not allowed by the type system is being explicitly overridden by the programmer.

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.


C++ casts ALSO perform runtime checks, just not all of them. As you've stated.

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.


That example doesn't mean references in C++ can be null any more than the following example means that Rust references can be null:

    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!


The C++ code though, compiles, runs and doesn't segfault.

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.


Most of my work at Facebook was using Hack. Nullness is a key part of Hack's stype system [1] and this solves so many BS errors. That's not to say you couldn't get a null where you weren't expecting one, particularly because this was a later addition to the language so the code still had a lot of legacy "mixed" types that basically mean anything (mirroring the PHP roots where you could pass basically anything).

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?

[1]: https://docs.hhvm.com/hack/types/nullable-types


Presumably its just :

    String![]? labels = null;


seems all the good things about Kotlin are coming to Java now.

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.


But Java programmers will still use the old idioms, and be stuck with their old code bases. It will take a long time before these things are in the wild, so Kotlin programmers need not worry.


Java has a long history of getting inspiration from other languages. Happy to see it's continuing to evolve at a decent pace, while mostly maintaining compatibility with previous versions.


The formatted JEP link: https://openjdk.org/jeps/8316779


Your link is only for value types, but this one is more general, it's for any type. Obviously the two are related, but not the same. Nullability for value types has performance implications as well.


> In this JEP, nullness markers are explicit: to express a null-restricted or nullable type, the ! or ? symbol must appear in source. In the future, it may be useful, for example, for a programmer to somehow express that every type in a class or compilation unit is to be interpreted as null-restricted, unless a ? symbol is present. The details of any such a feature will be explored separately.

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 !.


It’s not. The fact that it’s got a ? means a developer explicitly marked it and told you it’s nullable.

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.


Not it is not. They mean different things.

    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!


Thanks for clarifying the distinction - that's worse than I thought. When an expression as simple as bb = b can throw something has definitely gone wrong.


It has nothing to do with the expression’s simplicity. There are rules uphold by the compiler, and those are displayed in the easiest example possible. There may be additional rules that could show a warning in these cases (e.g. java can track whether a variable gets initialized in both branches and similar quite well), but that’s besides the point. Take these examples in a way that the only thing you know about variable `a` is its type.


These are stupid rules. The point of the proposal is to invite discussion on the proposed rules. Introducing implicit type conversions that throw to a language which is otherwise strongly typed is an awful idea.

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.


How is it not type or null safe? It’s as safe as it can be.


And that is why you want to migrate everything to T? and T!.


Finally! I tried many other solutions/workarounds in the past years, like using the eclipse/intellij notnull annotations and the various efforts of projects like https://www.lastnpe.org/

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?)


Checkerframework does this well.


One annoying detail of this proposal, is that it swaps the meaning of the nullness markers compared to Kotlin, which uses ! to mean "it came from Java and had no nullability annotations so we don't know" and no marker to mean not nullable (https://kotlinlang.org/docs/java-interop.html#notation-for-p...).


Not a big deal. The exclamation mark only appears in IDE tooltips and things, you can't express it directly. It could therefore be changed without breaking anything. If you want a 'flex type' you have to use type inference:

    // 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 is indeed annoying. For Kotlin, its advantage is that it had the nullability concept from its inception. For Java, they need to make sure previous code behaviour does not change, meaning that they need to add an additional marker to the explicitly mark "not null" types.

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.


"Our colleague Alex Buckley, specification lead for the Java language and JVM, likes to say that one of the surprising things is that Java managed to get all the defaults wrong and still succeed"

[Source](https://blogs.oracle.com/javamagazine/post/what-are-they-bui...


In that regard, I would say Java is in good company => C, C++, JavaScript, PHP.


There's no "!" syntax in Kotlin for developers.


Correct. Platform types are non-denotable.


Can someone explain to me how nulls are even a problem? I know the basics of Java and did some basic projects in it for my CS degree, but never used it professionally or in a large project.

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?


> Any code calling a function that can return a Null should know that being handed a Null is a possibility and handle it, right?

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.


> I'd think that eliminating Nulls is a bandaid over the wrong problem.

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;


The thing is you don't know whether the method you're calling can return null. It's invisible to the type system. You can try to track the null-validity of every value with documentation, but this is tedious, error-prone, and the exact same problem types exist to solve accurately at compile time. If you try to add defensive null handling everywhere it makes for absurdly verbose code, and it's pretty dubious since you don't really know what the semantic meaning of the phantom nulls you're pre-emptively defending against are.

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.


Can't say I'm a fan of the proposed syntax. Random question marks and exclamation marks do not improve the readability of the language. Introducing new symbols wouldn't bother me as much if they made sense. E.g. square brackets make sense for arrays because they denote an area. 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.

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;


There's some precedent in Kotlin, C# and PHP using a postfix marker for nullability. Overall it seems to work good there. Keep in mind that some types may be complex (e.g. when using generics) so using a keyword for the modifier may cause too much verbosity. Verbose is not the same as readable, even if Java is known to confuse the two.

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.


> Keep in mind that some types may be complex (e.g. when using generics)

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 :-)


That's a good point, though I would argue that this is caused more by the ? in <? extends Bar> than the ? of the nullable type.

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.


Typescript and Swift are somewhat similar in syntax as well. Not 200% but enough it’s very obvious what’s going on.


Maybe because I’m used to it from C# and Typescript, but ? and ! seem a perfectly readable to me. Obviously ‘nullable’ is more understandble the very first time you encounter it in code, but this is not what you should optimize for in a professional language (as opposed to a teaching or scripting language).


That's the Stroustrup Rule:

  For new features, people insist on LOUD explicit syntax.

  For established features, people want terse notation.


Yes, and that reduces to "the more a feature is (expected to be) used, the terser should be it's syntax", because people forget.


In my opinon, the former syntax is easier to read. I've never found stacking keywords to be a very clear way of annotating types (see C's `unsigned long long`, where the second `long` is a shorthand for `long int`). The whole "private final static" chain already detracts from readability, in my opinion.

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.


No thanks. The extra noise of these keywords scattered everywhere would be a huge loss in readability in a language that is already way too verbose.

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.


Agreed, the added verbosity is IMHO the reason why the (useful) keyword final isn't used as widely as it could.


One of the few things I don’t like about Dart is that it uses final for constant values. It seems like a small thing but it adds up. Let would be much better.


I agree.

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.


I think grep -E '^[a-z]+ [A-Z]' *.go will do.


> Which is easier to understand?

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.


Too late to edit, but:

> 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!").


Lots of languages already use this syntax. Dart is another besides the ones already mentioned. (It uses "?" and has nullable types by default without needing "!".) Also it kind of does make sense to me. A Java "String" is not actually a String. It's a String or null. It doesn't fit into set notation where null is an empty set, not part of other sets like integers ect. So calling it "String?" or "Integer?" makes more sense because it denotes that its value may be a String/Integer but it is not guaranteed to be so.


*Non-nullable types by default


The first is easier to understand in a real codebase and not just a tiny snippet like this. The issue is that this is not the only keyword.


And since one of them may be a default option (i.e., the `nullable` as it's the current state), then it's just:

    private String? foo;
    private String! bar;
    // or
    private String foo;
    private nonnull String bar;


You need an explicit marker for compat with legacy code. The issue defines unspecified as "a null may occur, but we don't know whether its presence is deliberate".

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.


But the "older code" is already written. And it's written with meaning "a null may occur". It doesn't seem to change anything related to legacy.


Agreed that the type system treated `T` as nullable, but code may also have comments, annotations and invariants placing stronger constraints. This means that "a null may occur" also lived with "this won't ever be null but the type system does not let me express it" and in the absence of marker it's hard to distinguish both cases.


T? is widespread syntax already. E.g. Typescript, C# and Kotlin use it.


Adding new keywords risks breaking code where the keyword is used as variable name, which would be not so far fetched for something like nullable.


Which of these is easier to search for in a book or web page? Which will allow for clearer oral discussion?

I prefer using long options in shell scripts for similar reasons. Easier for future maintainers to search for.


I'm still more in favor of Option<T>


(An Option<T> itself can still be null...) With an Option, you need to .orElse() if you want to access the value it is holding (or map it). With null restricted types you do this check upfront, when you convert a T to a T!, and from then you can use the value without any gymnastics. It is a bit like virtual threads vs effect systems, the monadic IO type gives you far more power, but with great power comes great responsibility (and complexity). If you can get compiler support for the simpler solution that will always be prefereable.


Same, since nullability should be the exception then it is a good thing that is more verbose.


This is perfectly logical in languages which have defaulted to opt-in nullability from the start, but it’s heinous and would actively hinder migration in languages which are trying to transition (or something similar e.g. Swift is non-nullable but objc interop was a major design factor, and objc has absolutely ubiquitous nullability).


Not sure why you think it is heinous but I agree with the other part. In a language where null was the default I do not see a migration path where Option<T> can be used. Option<T> only works for languages where nullability was never a thing. The issue is obvious in how useless the Optional<T> class in Java is.


How is Optional useless? I've found it to be very ergonomic, though I do wish there was more support for it in the standard library.


Ew.


What does this mean ?


preference for added syntax instead of generic type I assume

understandable in a way but I'm more fond of types vs syntax


The former. `?` and `!` are not modifiers on the variable, they are part of the type. The latter is just complete nonsense.

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.


Conciseness is better, for the same reason we don't do:

    int a = b plus c;

It's easy to misguide people with regional words, symbols are universal


Conciness is a valid concern, but symbols are exactly as universal as words, just consider the division sign[0] in math, or the assignment operator in programming languages[1].

Plus, 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)#...


In the case of addition I kinda wish we actually did not have one enshrined operator. We have wrapping_add(), saturating_add(), checked_add() and undefined_overflow_add(). Which one should be default?


Come now, that's a disingenuous take. I explicitly mentioned that some symbols make sense while others don't. Obviously the + sign is pretty universally known for addition. Overloading + for string concatenation was probably a more intuitive option than PHP's . concatenation operator. But not every symbol is as ubiquitous as the plus sign.


Unfortunately, this breaks the language semantics and I don't think it's a very good idea. Java is a static language. If these are a huge problem in your codebase, you can discover the problems using nothing more than static analysis and healthy programming habits.

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.


It doesn't seem like this is really focused on changing "the core of the language" though, is it? I guess I don't even really get that particular criticism for languages like Java and C# which I always kind of see as "big tent" languages. Both have been pretty engaged in adding optional features that allow support for different development paradigms in recent years.

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.


So I think the biggest dig for me is that according to this JEP there are times that this code,

    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.


This code is not correct for TypeScript because it could be Proxy with getter. It works only because TypeScript types are not checked at runtime. Java approach is correct one. If you want to ensure that value is not changing under your feet, you need to save it to the local variable.


> might trigger a null pointer exception

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.


How does it break the semantics? It "Compatibibly interoperates" according to the JSR!

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?


You can not like the proposal but claiming that we can be null safe with just static analysis? c'mon now.


How do you know what is or isn't null?


Eliminating null pointer dereferences (or NPEs, in this case) as a class of bugs is not a "fleeting in vogue trend". It's a useful and IMO necessary step to make programming less error-prone in general.

> 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.


> In a Java program, a variable of type String may hold either a reference to a String object or the special value null. In some cases, the author intends that the variable will always hold a reference to a String object; in other cases, the author expects null as a meaningful value. Unfortunately, there is no way to formally express in the language which of these alternatives is intended, leading to confusion and bugs.

Casually just describes one of the largest sources of bugs in the history of programming.


It's interesting to see this right on the tail of https://jspecify.dev/blog/release-1.0.0/ by Meta et al. That was done AFAIK because JEP-305 died and it didn't seem like Oracle was going to ever do anything about it. Did Oracle do nothing to coordinate here?


I think you mean JSR-305 [1]. Even though the initiative died in committee, you can still use the reference implementation [2] and hundreds of thousands of projects do so. Most static analysis tools and IDEs support it as well.

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...


As a Java developer this is great. I’ve started to use the @Nullable and @NotNull annotations a lot for parameters and return values and they’re very helpful.

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.


I think we're still in the bashing rocks together stage of programming, but things like this definitely make me smile. Java is not an innovative language and the fact it is evolving towards something better shows that the industry as a whole is moving forward. We can't be more than a decade away from everyone being able to program in a language with the features of ML circa 1983.


Scala is doing fine. ;P

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".)


Scala is doing fine-ish.


Java is not an innovative language, but it's one of its strengths. We live in a world where everything new is being marketed as an improvement of the previous thing, which is often not the case. Java is a healthy counterbalance. It should evolve, but it's fine if it does it more slowly than others.


>Java is not an innovative language

Wild statement. Java certainly was incredibly innovative upon release. It didn’t explode in popularity by accident.


"Java is not an innovative language" does not mean "Java was never an innovative language".

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.


Java has done many nice things, but "pioneering things like generics in GC languages," wasn't one of them.

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.


that was 30 years ago? New Java developers have been born after the original inception and learned the language long after. Reputations change over time.

Core Java was staunchly against incorporating any functional features while the rest of the ecosystem was innovating


Sure, because Java isn't a functional language. Taking a wait-and-see approach to whether the style of programming will be a fad is defensible choice when you have to support it forever. And it kind of was a fad. The only thing that really came of it in most mainstream langages is passing functions as arguments. I've yet to encounter a code-base in the wild that's actually functional.


What does functional mean to you? From my POV I see lots of features in Java coming from the functional programming world, but perhaps I have a different understanding of the term to what you do.


    > Java is not an innovative language and the fact it is evolving towards something better...
So basically C#?


In this particular case it goes further. C#'s nullable-reference types are useful, have a few problems, e.g.

• 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.


Ironically, given how C# came to be, Microsoft is now a OpenJDK contributor and has their own distribution, turns out it is quite valuable to do so for Azure.

Just like 60% of Azure runs Linux workloads, as per official numbers.

Interesting the rounds that the world makes.


>Java is not an innovative language

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]

[0]: https://bugs.openjdk.org/browse/JDK-8251554


nullability is directly related to the value classes effort (the top article mentions value classes, and the JEP you linked mentions nullability): in order to get the most benefit out of value classes, you need some way to express guaranteed non-nullability to avoid needing to encode null (imagine Point[] vs Point![]): https://cr.openjdk.org/~jrose/values/flattened-values.html#i...


'Value' classes would be useful in arrays only + some version of direct buffer mapping. All other cases are pretty uninteresting when it comes to performance or memory layouts.


[flagged]


Kotlin is only relevant on Android.

And before someone tells me how they enjoy Kotlin on the backend, see StackOverflow 2024 results.


> Kotlin is only relevant on Android

Sure, that's why Spring has first class support for Kotlin, but go on.

> see StackOverflow 2024 results.

The number of people that enjoy Kotlin has absolutely no influence on my ability to enjoy or not enjoy it.


Spring has first class support for any JVM language that helps them bring money to the table.

Groovy and Scala were there first, where are all those Spring apps now?

Heck I remeber when Groovy Spring was supposed to take over the Java Web application development, and alongside Grails wipeout all those Rails projects, with regular sessions at local JUGs.

It definitly has an influence on available jobs.


> Spring has first class support for any JVM language that helps them bring money to the table.

The Kotlin support is on a complete different level. Go to https://start.spring.io/, you get an option of Java, Kotlin or Groovy - no Scala, Clojure etc. Dedicated Scala support especially never really took off.

> It definitly has an influence on available jobs.

I worked at two different companies on almost exclusively Kotlin codebases and have interviewed for several more. There are enough jobs to keep me employed with a good salary and even if those jobs disappeared, I would just learn some other language, no big deal. In the meantime, I get to use a language that's actually decently fun to write.

I get that the very existence of Kotlin somehow offends you, but you don't have to shout it from every rooftop. There's enough space on the planet for everyone.


What offends me is the anti-Java culture among Kotlin users, most pushed by Android folks, as if Kotlin would have any meaningful existence without the Java platform and ecosystem.

"Let's rewrite Java in Kotlin, duh"

"Look how much better Kotlin is than Java boilerplate" across Android docs, (show Java 8 example as counterpoint, in 2024)

Looking forward to Kotlin/Native growing beyond the JVM, then.

Otherwise it is really all about JetBrains and Google collaboration on ART.


Absolutely not. I worked on many backend projects where Kotlin was used with Java backend technology like Spring.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: