惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - 叶小钗
Stack Overflow Blog
Stack Overflow Blog
S
SegmentFault 最新的问题
D
DataBreaches.Net
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学
Engineering at Meta
Engineering at Meta
T
The Exploit Database - CXSecurity.com
A
Arctic Wolf
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Project Zero
Project Zero
G
Google Developers Blog
宝玉的分享
宝玉的分享
H
Heimdal Security Blog
美团技术团队
Schneier on Security
Schneier on Security
C
CERT Recently Published Vulnerability Notes
Martin Fowler
Martin Fowler
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
Help Net Security
Help Net Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Google DeepMind News
Google DeepMind News
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 最新话题
O
OpenAI News
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
Netflix TechBlog - Medium
S
Security Affairs
小众软件
小众软件
MongoDB | Blog
MongoDB | Blog
Blog — PlanetScale
Blog — PlanetScale
V
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
F
Fortinet All Blogs
G
GRAHAM CLULEY
云风的 BLOG
云风的 BLOG
S
Secure Thoughts

Java Code Geeks

PHP in 2026: The Language That Refuses to Die HTTP/3 Comes to the Java HTTP Client Fixing Java ClassCastException for Comparable Objects Spring AI 1.1 and theModel Context Protocol:Building Production AI AgentsWithout the Python Tax Elasticsearch keyword vs text Project Panama’s FFM API in Production: Replacing JNI Without Writing C Wrappers NATS vs. Kafka vs. Redis Streams for Java Microservices: When “Simpler” Actually Wins [DEALS] The Premium Learn to Code Certification Bundle (97% off) & Other Deals Up To 98% Off – Offers End Soon! Shifting Left on Security: How to Harden CI/CD Pipelines for Payment APIs
JSpecify vs. Kotlin’s Built-in Null Safety: Can Annotations Ever Match a Type System?
Eleftheria Drosopoulou · 2026-03-19 · via Java Code Geeks

The NullPointerException — or NPE — is one of those bugs that every Java developer knows all too well. Tony Hoare, the computer scientist who introduced null in 1965, famously called it his “billion-dollar mistake.” Decades later, the Java ecosystem is still cleaning up that mess. And now, with Spring Boot 4 officially adopting JSpecify in late 2025, the conversation has reignited: can a smart annotation standard finally close the gap with what Kotlin does out of the box?

To answer that honestly, we need to look at both approaches side by side — not just their mechanics, but their real-world limitations. So let’s start from the beginning.

1. The Problem Worth Solving

Before we compare solutions, it helps to understand why null is such a persistent headache in Java specifically. Unlike Kotlin, Java’s type system has no concept of nullability. Every reference type — StringUserList — can hold a null value at runtime, and the compiler simply does not care. This means a developer is entirely responsible for remembering which values might be null and checking accordingly.

Unsurprisingly, that discipline is hard to maintain at scale. According to a widely-cited 2016 OverOps studyNullPointerException is the most frequent exception in production Java applications. More recently, tools like NullAway and static analysis reports continue to list it as the top cause of Java crashes.

The Java community tried to address this before. JSR-305 brought @Nullable and @NotNull annotations, but it stalled. JetBrains, Eclipse, Android, and Uber each shipped their own flavors — and suddenly there were a dozen competing annotation libraries that tools supported inconsistently. That fragmentation, more than anything else, is what motivated JSpecify.

2. What JSpecify Actually Is

JSpecify is a collaborative, open specification launched by Google in 2021, developed alongside JetBrains, Meta, Sonar, Oracle, and Broadcom. Its 1.0 release arrived in July 2024, and Spring Framework 7 / Spring Boot 4 completed the migration to it in November 2025. It provides four core annotations:

AnnotationScopeWhat it means
@NullMarkedPackage / classAll unannotated types in scope are treated as non-null by default
@NullUnmarkedPackage / classReverts to Java’s default “unspecified” nullability — useful for gradual migration
@NullableType usageThis specific value may be null — handle it explicitly
@NonNullType usageOverrides a nullable default in a @NullMarked scope

The key idea is elegantly simple: instead of annotating every non-null thing (which is most of your codebase), you declare a zone as @NullMarked and then mark only the exceptions — the places where null is genuinely possible. This matches how most well-written code already works conceptually.

Good to know: Starting with IntelliJ IDEA 2025.3, the IDE automatically prefers JSpecify annotations when they’re on the classpath — even over JetBrains’ own annotations. Quick-fixes and refactorings generate them automatically.

However — and this is the critical part — JSpecify annotations are static analysis hints, not compiler contracts. The Java compiler itself does not enforce them. You need external tooling, specifically NullAway (a build-time checker maintained by Uber), to turn those annotations into actual build failures. Without NullAway, you get IDE warnings at best.

package-info.java — declaring a null-marked package

@NullMarked
package org.example.service;

import org.jspecify.annotations.NullMarked;

Using @Nullable in a Spring Boot 4 service

import org.jspecify.annotations.Nullable;

// Non-null return is the default inside a @NullMarked package
public String formatName(String name) {
    return name.trim().toUpperCase();
}

// Explicitly nullable — callers must handle this
@Nullable
public User findById(String id) {
    return repository.findById(id).orElse(null);
}

3. How Kotlin’s Type System Handles It Instead

Kotlin approaches the same problem from a fundamentally different angle. Rather than annotating existing types, Kotlin bakes nullability into the type system itself. Every type has two versions: a non-null form (String) and a nullable form (String?). The compiler enforces the distinction at every call site.

In practice, this means you simply cannot call a method on a String? without first checking whether it’s null — and the compiler won’t let you forget. There’s no annotation to miss, no tooling to configure, and no build plugin to add. The safety is structural, not declarative.

“The problem is the compiler: Kotlin’s is null aware, not Java’s. Hence, it’s able to deduce whether a variable can be null or not. Annotations require work and are error-prone.”— Nicolas Fränkel, Java & Kotlin Developer Advocate

Kotlin also provides a rich toolkit for working with nullable values rather than just avoiding them. The safe call operator (?.), the Elvis operator (?:), smart casts after null checks, and the let scope function all make nullable handling concise and readable — without boilerplate.

3.1 The Interoperability Catch

Of course, Kotlin’s null safety is not perfect in a mixed codebase. When calling Java APIs that lack null annotations, Kotlin treats return values as “platform types” — denoted internally as Type! — where nullability is unknown. In those cases, the developer is back to making judgment calls. This is exactly where JSpecify’s standardized annotations help: Kotlin 2 automatically translates JSpecify annotations into Kotlin nullability, so annotated Java APIs behave like proper Kotlin types.

Null Safety Enforcement: Where Each Approach Kicks In

Score out of 10 across four enforcement dimensions (author analysis, based on tooling capabilities)

4. A Direct Side-by-Side Comparison

With both approaches on the table, it’s worth being precise about what each one can and cannot do. The table below lays out the most important differences — not to declare a winner, but to help you understand the real trade-offs before making architectural decisions.

CriterionJSpecify (Java)Kotlin Type System
Enforcement layerStatic analysis (requires NullAway)Compiler-native
Runtime protectionNone — annotations onlyCompile-time prevents most NPEs
Backward compatibilityExcellent — no API changesN/A — requires Kotlin adoption
Gradual adoptionYes — package-by-package via @NullMarkedRequires file-by-file migration
Tooling requiredNullAway + Gradle/Maven plugin + IDE supportKotlin compiler — zero extras
Sub-package inheritanceNo — must annotate each sub-package separatelyN/A — type system is pervasive
Generic type supportPartial — improvingFull
Java interop (Kotlin side)Excellent — Kotlin 2 reads JSpecify nativelyNative
Spring Boot 4 statusDefault standardSupported alongside JSpecify

Null Safety Maturity Across Java Ecosystem History

Qualitative progress score (0–100) for Java annotation-based null safety over time — from JSR-305 fragmentation to JSpecify adoption

5. The Real Limitations of JSpecify

Even with NullAway wired up correctly, JSpecify has some genuine friction points that are worth knowing before you commit to a migration. Understanding them doesn’t diminish the effort — it just helps you set realistic expectations.

5.1 Sub-package Inheritance Doesn’t Work

This one trips up many developers. @NullMarked applied to a package-info.java file does not cascade down to sub-packages. If your codebase is structured as com.example.service and com.example.service.impl, you need a separate package-info.java in each. That’s repetitive, and it’s easy to miss a package during migration.

5.2 Build Configuration Is Non-Trivial

The Maven setup for NullAway requires a fair amount of configuration — enough that several developers have described it as “a bit icky.” Gradle is more approachable, but neither is plug-and-play the way Kotlin’s compiler simply is.

Known issue: As of early 2026, NullAway’s JSpecify mode is still evolving. With Spring or Reactor APIs using lambdas, you may encounter NullAway#1290. The recommended workaround is @SuppressWarnings("NullAway") for affected methods while the issue is tracked.

5.3 No Runtime Contract

Perhaps the most structurally important limitation: JSpecify annotations are not enforced at runtime. If code reaches your annotated class via reflection, from an unannoted third-party library, or through a framework-level operation, null can still slip through. Annotations prevent you from writing null-unsafe code — but they cannot stop null from arriving from outside your annotated perimeter. For runtime guarantees, you still need to combine JSpecify with Bean Validation (JSR-380) or explicit guard clauses.

6. So Is JSpecify a Band-Aid — or a Genuine Step Forward?

This is where we have to be honest about what “genuine step forward” even means. For a language like Java — where backward compatibility is almost sacred — you cannot add null safety to the type system without breaking every existing API in existence. That’s simply not on the table. The question, therefore, is not “why isn’t JSpecify as good as Kotlin?” The more useful question is: given Java’s constraints, is this the best possible move?

And the answer is largely yes — with caveats. JSpecify resolves the longstanding annotation fragmentation that made Java null tooling unreliable. It brings Google, JetBrains, Oracle, Uber, Meta, and Broadcom to the same table. It gives IntelliJ IDEA, Eclipse, and VS Code a single standard to support. And, crucially, it provides a path to build-time null safety that genuinely catches bugs before they reach production.

“The real mistake was not the invention of the null reference — it was not expressing it explicitly in the type system, as this is the implicit nature of nullability that causes so many NullPointerExceptions in production.”— Sébastien Deleuze, Spring Framework Team, Spring.io Blog (November 2025)

At the same time, it would be dishonest to claim that JSpecify achieves what Kotlin achieves. Kotlin’s null safety is structural — it simply cannot be worked around without explicitly opting out. JSpecify’s null safety is declarative — it relies on discipline, tooling configuration, and full annotation coverage to be effective. That’s a meaningful difference, especially in large teams or legacy codebases where annotation coverage will always be incomplete somewhere.

The Verdict

JSpecify is not a band-aid. It is the most mature, well-standardized attempt to retrofit null safety into Java’s existing type model — and with Spring Boot 4’s adoption, it finally has the ecosystem weight to become the actual default. However, it remains fundamentally different from Kotlin’s approach: annotations inform tools, type systems enforce contracts. Both are valuable; only one is guaranteed.

7. Practical Guidance: Which Approach for Which Situation?

Rather than framing this as a competition, it helps to think about when each approach is actually the right fit for a given team or project.

SituationRecommended approachReason
New Spring Boot 4 project, Java teamJSpecify + NullAwayNative framework support, gradual adoption, no language switch needed
New project, no language constraintsKotlinStrongest compile-time guarantees, no tooling overhead
Existing Java codebase, high NPE riskJSpecify (incremental)Add @NullMarked to critical packages first, expand over time
Mixed Java/Kotlin codebase (Spring)Both togetherKotlin 2 translates JSpecify annotations natively — best of both worlds
Library or API authorJSpecify annotationsEnables consumers in both Java and Kotlin to get proper nullability signals

Spring Team recommendation: The Spring team suggests starting with IntelliJ IDEA’s IDE warnings (zero configuration) to reduce NPE risk, then progressively adding @NullMarked to your most critical packages, and finally enabling NullAway for build-time enforcement in high-reliability modules.

8. What We Have Learned

Through this comparison, a clear picture emerges. JSpecify is a genuine, well-engineered step forward for Java null safety — especially given the language’s backward-compatibility constraints. By unifying a fragmented annotation landscape and securing industry-wide backing, it gives Java developers something they’ve never had before: a single, trustworthy standard that IDEs, static analyzers, and frameworks all speak the same language around.

We also saw that Kotlin’s approach is structurally superior when null safety is a first-class priority. Because nullability is baked into the type system, enforcement is automatic, universal, and requires no tooling setup. The compiler is the safety net — not an annotation and a build plugin.

However, the two approaches are increasingly complementary rather than competing. Kotlin 2 reads JSpecify annotations natively, Spring Boot 4 supports both, and a growing number of teams are running mixed Java/Kotlin codebases where JSpecify-annotated Java APIs behave like proper Kotlin types at the call site.

The honest takeaway: if you are writing Java, adopt JSpecify now — it is the best available option and has genuine ecosystem momentum behind it. If you are starting fresh and null safety matters deeply to you, Kotlin removes a category of bugs that Java annotations can only reduce, not eliminate.

Photo of Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.