Should @Nullable still be used for method returns when IDEs can infer nullity?

Viewed 137

IDEs like IntelliJ IDEA are able to infer when a method can return null--and pretty reliably, it seems. Given this, should methods still be annotated with @Nullable?

2 Answers

Yes.

There is an ocean of difference between signatures and implementations.

The right mindset to adopt is to think about code as a thing that is constantly in motion. Every day brings another 50 commits that touch many parts of the code; new releases are continuous or at least show up often, and there is an entire dimension's worth of 'compatibility' between releases. Rarely, 'the new release' is just so utterly incompatible with the old, that anything written against the old is flat out completely broken on the new. Usually, 'the new release' is intended to be 100% compatible with all code that was written against 'the old'. Often this is the intent but not quite, entirely, 100% true - extremely exotic/academic scenarios can be imagined.

Most usual of all is that things are 99.9% compatible.

The point is: Things change.

A signature dictates what you can and cannot change between releases whilst still calling it 'compatible'. The javadoc is included in this (if a javadoc promises that e.g. a method will never return an empty string and in some minor update it now does, you've messed up and that should have been an update marked as 'incompatible', i.e. if you follow semver, hello "version 2.0").

Signatures are a double-edged sword. On the one hand, it makes life easy by spelling out what you can rely on. If @Nullable is either in the signature or is 'inferred', then code that calls this method knows that the returned value might be null, whereas without that annotation or inference, callers do not get the benefit of compile-time notification that one should be ready to receive nulls.

But on the other hand, once you spell it out in a signature, that's it. You're stuck now. It cannot ever change unless you go for that 'v2' (incompatible update) idea which few libraries ever go for.

Therefore, an automated tool can never just pile a bunch of rules into your signatures. If you write code that currently, say, can be code-analysed to conclude that null is never returned from it, then an IDE would be entirely in the wrong if it adds that inference into a signature - after all, maybe it's just a placeholder and you intend for this method to return null later on in some point update; any code written against this method very much should deal with nulls, even if right now that can never happen.

Nullable is a bit of a special snowflake, in that it's a case of type loosening which is by definition future-compatible. (@Nullable String is in effect a supertype of @NonNull String), so inferring @Nullable and then silently making it part of the signature would actually be harmless, but that's a rare case, so inference engines probably shouldn't put in the considerable effort to update signatures silently just for those scenarios where that won't cause harm.

Separate from the 'signatures are promises about this API that hold now and forever, whereas implementations can change at any time' issue, Part of the entire point of @Nullable is to have the compiler be a second pair of eyes. You SHOULD be configuring your IDE to aggressively warn you when you are returning potential null values out of a method that is not explicitly marked as @Nullable (never errors, compilers should warn and not error to make debugging simpler, and you should never ship code with warnings in it to production-level endpoints).

To be more specific:

  • @Nullable should be a TYPE_USE and not a METHOD targeted annotation, though most nullity annotation frameworks mess this up. Eclipse's and checkerframework get it right.
  • If you use these annotations you need to use a defaulting mechanism, because annotating every single last type in every source file with @NonNull or @Nullable is ridiculous. Usually, it's a matter of sticking @NonNullByDefault on the package or class. At which point, failure to stick @Nullable on the method's return type means it is non-null, and thus any attempt to return null in the method will straight up be a warning (which, as discussed, means it can't be released until you clear that warning up).

The IDE may be quite capable of inferring these values, but a machine inferred nullability does not communicate the intent of the developer as clearly as an explicitly marked function. Placing the annotations will definitively state that intent of a function to be nullable or not null, and consumers of that function will have more confidence that this will not change.

Additionally, IntelliJ offers deeper integration of these features to automatically add runtime assertations to ensure even at runtime that the contracts defined using @Nullable and @NotNull are maintained.

The check is done by the Constant conditions & exceptions and @NotNull/@Nullable problems inspections. You can configure the way these inspections work in the Settings/Preferences Ctrl+Alt+S dialog. Go to Editor | Inspections | Java | Probable bugs.

When you compile your project, the IDE adds assertions to all methods and parameters annotated with the @NotNull annotation. The assertions will fail if null is passed in code where @NotNull is expected. You can disable this option and configure the list of annotations in the Settings/Preferences dialog Ctrl+Alt+S. Go to Build, Execution, Deployment | Compiler.

Related