Skip to content

Instantly share code, notes, and snippets.

@Gems
Created July 24, 2025 13:22
Show Gist options
  • Save Gems/38b60bb10ebf39e36ffa3fa44a54c88d to your computer and use it in GitHub Desktop.
Save Gems/38b60bb10ebf39e36ffa3fa44a54c88d to your computer and use it in GitHub Desktop.
Non-Null vs. Nullable by Default in Java: Industry Trends and Arguments

Non-Null vs. Nullable by Default in Java: Industry Trends and Arguments

Background: Java’s Nullability Problem

Java historically treats all object references as potentially null by default – there is no built-in way in the type system to indicate whether a variable can or cannot be null . This implicit “nullable by default” design has led to the infamous NullPointerException (“billion-dollar mistake” as Tony Hoare called it ) when null values aren’t handled properly. For decades, developers had to rely on documentation, runtime checks, or patterns like returning sentinel values/Optional to signal absence of a value .

To improve safety, the Java ecosystem introduced nullability annotations (e.g. @Nullable, @NonNull/@NotNull) and static analysis tools. However, a key question arises: should we assume types are non-nullable by default (requiring explicit annotation of the few nullable cases), or assume they are nullable by default (requiring explicit @NonNull on variables that must not be null)? The industry has debated this, and modern practices are converging in one direction.

Non-Null-by-Default (Explicit Nullable) Approach

Definition: Non-null-by-default means unannotated reference types are treated as non-nullable – they must not hold nullunless explicitly marked @Nullable. Under this convention, @Nullable (or @CanBeNull) is used to mark the _exceptional_cases where null is allowed, and everything else is assumed non-null.

Rationale and Pros: This approach mirrors the behavior of newer languages like Kotlin and Swift, where types are non-null by default and only specific opt-in types can be null . The majority of variables in well-designed Java code are meant to be non-null, so making non-null the default reduces clutter. Instead of writing @NonNull on 99% of methods and fields, developers only annotate the rare nullable cases . This significantly cuts down on “annotation noise” and focuses attention on the places where nulls are actually expected. As the Eclipse documentation notes, @NonNull occurrences tend to far outnumber @Nullable in typical APIs, so declaring NonNull as the default can eliminate a large number of redundant annotations . JetBrains (IntelliJ IDEA) also follows this philosophy: by default the IDE assumes code is non-null unless a @Nullable annotation (or null-checks in flow analysis) indicate otherwise, warning you if you pass or dereference a potential null without proper annotation .

With non-null-default, tools can catch mistakes early. For example, if a method isn’t annotated @Nullable, the IDE/analysis will flag any code that tries to use a null with it or any result without a null-check . In effect, “broken code looks broken” at development time, instead of causing surprises at runtime . This approach also aligns with design-by-contract thinking: callers assume non-null unless told otherwise, and callee implementations can trust their inputs to be non-null (or outputs to be non-null) without defensive checks .

Tool and Language Support: The non-null-by-default style is increasingly the norm:

  • Java IDEs & Compilers: Both IntelliJ IDEA and Eclipse JDT support global defaults of non-null. For example, Eclipse allows a @NonNullByDefault annotation at the package/class level so that all unannotated methods and fields are treated as non-null unless marked @Nullable . This avoids having to slap @NonNull on everything. IntelliJ’s inspections similarly assume unannotated types are NotNull by default (configurable in settings) and encourage adding @Nullable where needed, effectively making non-null the implicit baseline. JSR-305 (a popular but informal standard) introduced @ParametersAreNonnullByDefault and similar annotations, which many libraries (e.g. Google Guava, Square’s libraries) use to declare that everything in a package is non-null unless otherwise specified .

  • Static Analysis Tools: Modern null-checkers for Java embrace non-null defaults. For instance, Uber’s NullAway(an ErrorProne plugin) assumes every method parameter, return, and field is non-null unless annotated @Nullable . Any attempt to pass or return a null to a non-null parameter/result is a compile-time error in NullAway. Facebook’s earlier tool Infer (Eradicate) and the Checker Framework’s nullness checker also treat unannotated references as non-null by default . This drastically reduces the “annotation burden” – one study found that using NonNull-by-default led to far fewer annotations needed per thousand lines of code, encouraging better style with less overhead .

  • Frameworks and Libraries: There’s a clear trend of frameworks moving to non-null-default APIs. Spring Framework 5+ initially adopted JSR-305 nullability annotations (with an implicit NonNull default) and recently Spring Framework 7 has fully migrated to JSpecify annotations, marking its entire codebase as @NullMarked(non-null by default) . This means Spring’s own APIs now explicitly annotate the minority of nullable return values or parameters, and everything else is treated as non-null, enabling robust static checks. Square’s OkHttp library made a similar move – since version 3.8, “everything is non-null by default” in its API . Google’s teams (Guava, etc.) and others often use package-level non-null defaults and require nullables to be marked, which has become a de facto standard in many large codebases .

  • Languages: Kotlin (100% non-nullable by default) and C# (with nullable references feature in C# 8+) exemplify the industry inclination. In C#, when nullable reference types are enabled, unadorned types are non-null by default and the compiler forces you to handle any value that could be null, treating string vs string? distinctly . These languages influenced Java practices – developers now expect Java code to provide similar null-safety guarantees via annotations or upcoming language changes. (Notably, a draft JEP is exploring adding explicit nullness markers !and ? to Java itself , which would make Java’s type system non-nullable by default once opted in, with Foo?denoting a nullable type. This further underscores the trend towards non-null-by-default semantics.)

Summary of Pros: Non-null-by-default reflects the reality that most references should never be null, and it leverages tools to enforce that. It minimizes boilerplate (no need to write @NonNull everywhere) , makes APIs clearer (you assume non-null unless you see a @Nullable), and catches potential null misuse early. As one developer put it, marking everything with @Nonnull felt like “useless noise” for 99% of use-cases , so it’s more efficient to flip the default. The end goal is safer code: by explicitly annotating the nullable minority, you document and check those cases, turning Java’s null handling from a runtime minefield into a compile-time certainty in most places. In practice, teams adopting non-null-by-default plus static analysis have seen huge drops in NullPointerExceptions. The Spring team reports that with comprehensive null annotations (non-null default) and build-time checks, the risk of NPEs in production is virtually eliminated – any potential null has to be explicitly declared and handled .

Nullable-by-Default (Explicit NonNull) Approach

Definition: Nullable-by-default is essentially the status quo in Java: any reference not specifically marked otherwise is assumed to possibly be null (or at least the compiler/IDE doesn’t assume safety). In a strict interpretation, this approach would mean one must explicitly annotate every parameter, return type or field that should never be null with an annotation like @NonNull (or @NotNull). Unannotated references would be treated as accepting/returning null by contract.

Rationale and Pros: The main argument for keeping types nullable unless marked non-null is one of caution and explicitness. In existing large codebases with no annotations, it may be dangerous to suddenly assume everything is non-null – because some methods might actually return null (even if not documented). By treating unannotated references as “unknown or nullable,” static analysis errs on the safe side: it will warn you about possible null usage unless a @NonNullguarantees otherwise. This approach forces developers to consciously mark all the places that are guaranteed non-null, potentially uncovering undocumented assumptions. Some developers feel this is more explicit: you see @NonNull in the signature and know for sure that null is forbidden, whereas if nothing is written (and you’re assuming non-null by default) there’s a chance the author just forgot an annotation. In other words, proponents might argue it’s better to assume nothing(nullness unspecified) unless the code explicitly says non-null, thereby avoiding false confidence. This mindset underpins the initial design of many tools: for example, FindBugs/SpotBugs and older IntelliJ versions treated unannotated items as having unknown nullability – they would only produce warnings when an actual contradiction or a known @Nullable was involved, otherwise they wouldn’t assume safety. The upcoming JDK’s nullness feature also chooses not to guess intent: an unmarked type remains “unspecified” and the compiler will require an explicit ! or ? for full certainty . This explicit marking of non-null on each element can be seen as a way to avoid ambiguity.

Another scenario favoring explicit @NonNull on everything is when combining annotated code with legacy or third-party libraries. If your code is non-null-by-default but you call a library method that isn’t annotated, should you assume its return is non-null? In a nullable-by-default regime, you would not assume that – you’d treat it as unknown (possibly null) unless the library explicitly stated non-null. This can prevent mistakenly trusting a null-returning legacy API. In practice, tools like Eclipse allow a mode to treat unannotated external methods as non-null by default or not, depending on the team’s decision, and even allow “cancelling” a default in certain scopes to accommodate legacy overrides . Thus, one could argue the nullable-by-default approach might integrate more safely with unannotated code by forcing developers to consider nullability everywhere and mark non-null contracts explicitly, ensuring nothing is taken for granted.

Challenges and Cons: Despite the above, the explicit NonNull everywhere approach has seen limited adoption because it is cumbersome in Java. The annotation burden is very high – literally almost every method signature and field would need an extra @NonNull annotation. Developers have pushed back on this as noise“I don’t want to tell in 99% of cases that something is not null and pollute the code with useless annotations” . It’s the mirror image of the boilerplate problem, just annotating the opposite way. Maintaining such a codebase can be tedious, and if a single @NonNull is forgotten, you’re back to the default uncertainty (which defeats the purpose). This is why even tools that initially treated unannotated as unknown introduced ways to invert the default. For example, teams using IntelliJ or ErrorProne often decide to opt-in to a non-null-by-default style by using package-level annotations (JSR 305’s @ParametersAreNonnullByDefault, etc.) rather than individually marking everything @NonNull . The Eclipse manual explicitly points out that since @NonNull is “significantly more frequent” than @Nullable in well-designed code, “the number of annotations can be reduced by declaring @NonNull as the default” – strongly hinting that the explicit NonNull-everywhere approach is inefficient.

Another con is that nullable-by-default (with no default annotation applied) leaves a lot of room for latent bugs: if nothing is annotated, the compiler/IDE can only do limited null analysis (it can’t assume safety, but also can’t be sure what to warn about). This often results in “redundant” null checks or, worse, missing checks because each side (caller/callee) assumes the other will handle it . In other words, treating everything as nullable by default without a rigorous annotation of NonNull on each element is essentially the pre-annotation world – and that world had plenty of NPEs. So, unless one truly annotates every single non-null reference (a tall order), the nullable-by-default stance tends to degrade to “nullness unspecified”, which provides no guarantees. It’s telling that most modern Java code conventions say “assume parameters and return values are non-null unless otherwise documented” – effectively adopting a non-null-by-default policy in prose if not in code. This is because assuming everything might be null leads to defensive programming and complexity everywhere.

Summary of Pros/Cons: In summary, the nullable-by-default, explicit NonNull approach prioritizes explicit declarations of non-null contracts at the cost of many annotations. Its strength lies in not making any unwarranted assumptions – which can be safer when dealing with unannotated legacy code – and ensuring that any non-null guarantee is clearly documented on the code element itself. However, it’s largely viewed as impractical for day-to-day development because of the heavy annotation burden and the fact that it treats the common case (non-null values) as the special case to mark. As a result, the industry has gravitated away from this model in favor of the opposite.

Industry Trends and Ecosystem Support

The industry consensus is clearly inclining toward the non-null-by-default model, with broad tool support and best practices reinforcing it:

  • Major IDEs and Compilers: IntelliJ IDEA flags potential nullability issues assuming NonNull-by-default unless annotations indicate otherwise. It encourages using @Nullable to mark the minority of nullable cases, and can even auto-infer and insert nullability annotations for you to align with a NonNull-by-default stance . Eclipse’s compiler has a built-in null analysis that defaults to NonNull (when enabled) and offers the @NonNullByDefault annotation for packages/classes . Both IDEs recognize a wide range of null annotations (from JSR305, Checker Framework, Android, JetBrains, Lombok, JSpecify, etc.) and generally interpret unannotated types as non-null if a default is in effect . This shows that tool makers expect developers to use the NonNull-by-default pattern (and provide ways to do so with minimal fuss).

  • Static Analysis and Build Tools: As mentioned, tools like NullAway, Checker Framework, Google’s Error Prone nullness checker, SpotBugs, and others either assume NonNull-by-default or strongly encourage it. NullAway, for example, considers any absence of a @Nullable annotation as meaning non-null, and it forces developers to add @Nullable wherever null is actually possible . It even supports the new JSpecify annotations like @NullMarked (to mark a class or package as non-null-default) and @Nullable from JSpecify . The JSpecify initiative itself is a recent collaboration between Oracle, Google, JetBrains, Uber, VMware and others to standardize Java nullness annotations and semantics . JSpecify’s design makes NonNull the implicit default when you annotate a scope with @NullMarked , reflecting the shared understanding that this is the way forward for clarity and safety. The fact that so many players in the Java ecosystem co-authored JSpecify shows the momentum behind non-null-by-default – effectively, the industry is codifying that approach so all tools can agree on it.

  • Frameworks and Libraries: We’re seeing widespread adoption of nullity annotations in popular libraries, almost always using NonNull-by-default conventions. Spring, as noted, now treats its APIs as non-null by default (using JSpecify) across the entire framework . Apache libraries, Google’s Guava and common Java frameworks have long used JSR-305’s @ParametersAreNonnullByDefault in package-info files or similar mechanisms to indicate that their methods expect non-null unless marked Nullable. Square’s OkHttp and Retrofit, Netflix’s Feign, and many others have made the jump as well . Even if the exact annotation differs, the pattern is consistent: document non-null as the norm, and explicitly annotate the nullable exceptions. This has also influenced API design – for instance, methods that might have returned null in the past are increasingly returning Optional or throwing exceptions, reserving null only for cases that are clearly annotated as @Nullable.

  • Modern Languages Influence: The shift in Java isn’t happening in isolation – it parallels what’s happened in other languages. Kotlin’s success (with its type system that eliminates NPEs by making nullability part of the type) has set a high bar; calling into a null-annotated Java API from Kotlin is much nicer because Kotlin can recognize @Nullable vs non-null and treat types accordingly. (Kotlin will even treat Java methods under @ParametersAreNonnullByDefault as non-null, making interop safer .) Microsoft’s .NET platform similarly saw that developers want static null safety – hence C#’s nullable reference feature which, once enabled, essentially flips the default to non-null and provides warnings if you neglect to handle a nullable value . TypeScript (for JavaScript) introduced “strict null checks” where variables are non-nullable unless you add a | null union. These trends underscore a broad industry philosophy: prevent null errors by default, rather than expect developers to handle nulls everywhere by default.

  • Upcoming Java Developments: While Java hasn’t yet built null-safety into the language proper, the draft JEP on Null-Restricted Types (project Valhalla/Amber) indicates a future where you could write String! to mean “non-null String” and String? for “nullable String” . In that future, a class could conceivably declare all its fields and methods as non-null by default (with a single modifier or opt-in) and only use ? where needed . The fact this is being explored as a language feature (in a backward-compatible way) shows that even at the language design level, the momentum is towards making non-null the default semantic, and nullability an explicit opt-in marker. In the meantime, the combination of annotations + static analysis provides a “zero-cost” (in runtime terms) way to achieve similar safety .

In practice, teams that embrace non-null-by-default report significant improvement in code robustness. As the Spring engineers put it, by making nullability explicit everywhere (and defaulting to non-null), they turn what was a hidden hazard into “a zero cost abstraction” – the potential absence of a value becomes part of the code contract, checked by tools, virtually eliminating NullPointerExceptions in production . The remaining challenge is for the entire ecosystem (including third-party libraries and the JDK itself) to consistently adopt these annotations. Until then, developers must be aware that if a library isn’t annotated, some defensive checking or external nullability metadata might still be needed. But the overall direction is clear: the Java industry is inclined towards treating types as non-null unless explicitly declared otherwise, following in the footsteps of newer languages and backed by an array of tools and best practices that make Java much safer and more expressive regarding nullness than it used to be.

References and Sources

  • Spring Blog – Null Safety in Spring Apps with JSpecify and NullAway

  • Square Engineering Blog – “Non-null is the Default”

  • Stack Overflow discussion – “Java @Nonnull everything by default”

  • Eclipse Documentation – Using Null Annotations (Design by Contract)

  • Uber’s NullAway GitHub Wiki – Supported Annotations (NullAway assumes non-null unless @Nullable)

  • Checker Framework Manual – Nullness Checker Defaults (NonNull is the default qualifier)

  • Microsoft Docs – Nullable Reference Types (C#) (non-nullable is default in C# 8+)

  • OpenJDK JEP Draft – Null-Restricted and Nullable Types (proposed ! and ? markers in Java)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment