Beyond the Gap: Introducing NotNull4J 0.1.0
Static analysis is a powerful ally in the fight against NullPointerException, but it isn’t a silver bullet. We’ve all seen it: a @NonNull parameter that somehow arrives as null due to reflection abuse, serialization quirks, or raw-type interop. Today, I’m excited to announce the initial release of NotNull4J (v0.1.0). Its a small multi-module library designed to bridge the gap between static type hints and runtime reality.
Why NotNull4J? Bridging the Local Variable Gap
Static analysis in Java has taken a massive leap forward with JSpecify, which provides a long-awaited standard for nullability contracts in APIs. However, JSpecify explicitly skips local variables.
In Java, local variables often exist in a state of flux. Even if an API is marked @NonNull, a local variable might not be initialized immediately or might be reassigned during complex logic. Unlike Kotlin, which has null-safety baked into the language syntax, Java must remain cautious. A local variable is effectively “nullable by default” until it is assigned.
JSpecify is great for defining the “contract at the door,” but it doesn’t follow you into the room. NotNull4J 0.1.0 allows you to set defaults or fail-fast the moment a null is found, keeping your local logic as clean and safe as a Kotlin codebase.
The Three Pillars of NotNull4J
- Reliability: The core engine launches with 97% test coverage, ensuring that your null-handling policies behave exactly as expected.
- Explicit Policies: Whether you need to
orThrow(),orLog(), or normalize collections withlistOrEmpty(), the API is designed for readability and intent. - Defensive Verification: The library introduces the
verify()pattern, performing a runtime “heartbeat” check when assigning supposedly non-null values to local variables.
Code Evolution: From “Paranoid Java” to NotNull4J
To see the value of NotNull4J, we have to look at how it transforms standard defensive code.
Standard Java (The “If-Else” Forest)
public void processOrder(@Nullable Order input) {Order order = (input != null) ? input : Order.EMPTY;String customerName = "Unknown";if (order.getCustomer() != null) {customerName = order.getCustomer().getName();}System.out.println("Processing: " + customerName);}
The NotNull4J Way (Kotlin-esque Clarity)
public void processOrder(@Nullable Order input) {// Enforcement at the point of assignment@LocalNotNull final var order = NotNull.orDefault(input, Order.EMPTY);// Safe extraction with lazy fallbacks@LocalNotNull final var name = NotNull.orGet(order.getCustomerName(), () -> "Unknown");System.out.println("Processing: " + name);}
Deep Dive: The verify() Pattern
The centerpiece of the library is NotNull.verify(). While static analysis tools might flag a null-check on a @NonNull parameter as redundant, NotNull4J treats these as essential defensive boundaries.
public void processUser(@NonNull User user) {// Defend against reflection or serialization bugs@LocalNotNull final var verifiedUser = NotNull.verify(user);// Now truly safe at both compile-time and runtime}
To prevent “SillyCheck” warnings in your own linters, we’ve handled the suppression internally so you can focus on writing clean code.
Architectural Enforcement with @LocalNotNull
NotNull4J isn’t just a utility class; it’s a workflow. The notnull4j-pmd module provides custom rules to ensure your team is using these runtime-safe methods correctly. It specifically enforces that variables marked with @LocalNotNull are initialized and reassigned only through validated NotNull4J paths.
Java + NotNull4J vs. Kotlin: Syntax Comparison
We recognize that while we cannot change Java’s syntax to match Kotlin’s ? or !!, we can provide the next best thing: expressive methods that provide the same functional results.
| Feature | Kotlin Syntax | NotNull4J (Java 11+) |
| Non-null assignment | val x: String = value | @LocalNotNull final var x = NotNull.verify(value); |
| Default fallback | val x = value ?: "default" | var x = NotNull.orDefault(value, "default"); |
| Lazy fallback | val x = value ?: compute() | var x = NotNull.orGet(value, () -> compute()); |
| Throw on null | val x = value!! | var x = NotNull.orThrow(value); |
| Safe collection | val list = input ?: emptyList() | var list = NotNull.listOrEmpty(input); |
Getting Started (0.1.0 Initial Launch)
We are currently in our initial launch phase while we prepare for the 0.1.1 Maven Central release.
The Verdict
JSpecify is great for defining contracts, but it isn’t enough to save us from nulls inside our method bodies. By using NotNull4J, you get the best of both worlds: standard API contracts and runtime-safe local variables.
Open Source and MIT Licensed
I believe null safety should be a standard, not a premium feature. NotNull4J is released under the MIT License, encouraging wide adoption and community contribution.
Check out the source, view the coverage reports, and grab the release over at GitHub: github.com/robdeas/notnull4j
