JCON EUROPE

JSpecify: Getting Rid of the Billion-Dollar Mistake in Java for Good This Time | Michael Simons (EN)

35:58 · 20 Apr 2026 – 23 Apr 2026 · YouTube

About this talk

This talk explores the challenges and solutions surrounding null reference handling in Java, focusing on the introduction of J Specify, an initiative aimed at improving null safety in Java applications. The speaker, Michael Simons, a Java Champion and senior staff engineer at Neo4j, discusses the historical context of null references and how they emerged as a significant issue in programming. He explains that unlike existing solutions, J Specify introduces a ternary system that allows developers to define code parts as null safe or not, addressing the limitations of previous approaches. The talk also covers integrating J Specify with tools like NullAway and ErrorProne to enhance code verification and safety in applications and libraries. Simons emphasizes the need for intent when using annotations and how they affect library consumers, suggesting that while new projects can adopt this null safety framework, existing projects may face challenges during migration.

Full transcript

[music] >> Welcome to J Specify and the question if we get rid of the billion dollar mistake in Java for good this time. I'm quite happy that so many people come to this slot at that time of the day just before or quickly after lunch and with a talk that will have little AI in it. This is kind of a technical talk. I maybe mislabeled it as

intermediate giving Renee's talk yesterday about the billion rows challenge that was really hardcore. This is going to be less hardcore on the Java side of things. So, my name is Michael Simons. I'm a Java Champion working as senior staff engineer at a company called Neo4j. We created a graph database with the same name, but we're not going to talk about that database today. This is my silly

little safe harbor statement that this talk and the speaker contain or hold personal opinions. And I'm also want to set the scope in which the work on J Specify was undertaken. For the best of the last decade, I was or I still am the maintainer of Spring Data Neo4j. An object graph mapping framework for said database. This is a library project and we moved all the Spring

Data portfolio projects, not only that one, but also JPA and onto J Specify annotations. Um along with the Spring Framework itself, right? Depending on your area of work and what you're working on like if it's an application or a library, the mileage you get out of J Specify and related technology does vary, I'd say. I'm also saying this is all also only opinions just because someone thought

this is a good idea with Java Champion and whatnot. My opinion does not hold more than your opinion. So, we can all contribute to that ecosystem. I've been told to put up this QR code for the Slido in cinema five for asking questions. I have this on my phone as well so that we can discuss things afterwards. Um some things will remain true for the end of

time. This is one of this naming things are hard. What's an null pointer? So, in an unmanaged language like C, you use pointers to dereference memory addresses like the addresses of stuff you formally allocated and deallocated. Um a null pointer in most C dialects is defined as a pointing to memory address zero. That holds true in many cases, not all of them. When you dereference this, your

program will most likely crash with a segmentation fault or you get memory access violations. If you ignore null pointers at the end of arrays or lists, all kind of other stuff can happen and you will likely receive a CVE at some point. Thank god we are using Java. It's a memory managed language. We don't deal in pointers. Like everything in Java except the primitive values, but we're

not talking about those today, are references to string objects, integer, what have you. I have the assumption that most people in the room can actually hear that slide. So, uh the older people in the room maybe recognize the dialogue in an AWT version or even as an applet. Today, you find those things in your Grafana dashboards, in your alerting, whatever. Why are we dealing with that? This

person is Sir Charles Anthony Richard Hoare, who actually died a couple of weeks ago. He has developed quicksort more than 60 years ago, so leaving a lasting legacy legacy in software engineering. Um he worked his whole life on a formal verification logic logic of programs. And um he introduced a null reference in Algol W, a successor to Algol 65. with an assumption that I also guess that

many of us did at least once or twice in their career. It was something that was really simple. And this is more true than ever today. I can just use a claw a coding assistance to do things that are very simple and that going to haunt you for the rest of your life, even if you set out to formally verify a program that it cannot contain any

null references that crash your program when dereferenced. Come back to this later on. of course there is an XKCD for everything in the world. This is the XKCD for standards. You have 14 standards, people think on the internet, "Hey, this needs to be unified this time." And a couple of weeks later you have 15 And this also holds true for anything not null in the Java language

or as an addition to the Java language. Starting 20 years ago with JSR 305, never really pushed through as a JSR. Uh was a whole suite for annotations to de- to detect software defects, right? This became available in FindBugs, later on in SpotBugs. We have a ton of vendor specific annotations coming from obviously IDE uh vendors like the JetBrains annotations. We have something from the Eclipse compiler.

All these things are useful in isolation and tooling like um what we see later on can also use them, but they are not agreed upon as a standard. Um nonnull from Project Lombok is very much not intended as a verification tool. Will reappear later on. Java X validation constraints are also not validation tools. Um at least not No, they are validation tools. They're not verification tools. You

use these constraints to validate user input for forms fields and the like. So, we have a whole bunch of stuff here. There's even more. I think there's checker framework and a bunch of others. How does JSpecify fit into this? Um JSpecify appeared in 2020, 6 years ago. I completely missed it back then um because I was so swamped into adding reactive support to Spring Data Neo4j. Um

Yeah, that was the rage and hype back then. Missed that thing despite uh the owners of the Spring framework, I think back then it was still Pivotal, very much partaking in it. It's not really a JS JSR, but it's defined by a committee committee in a very similar fashion. Google was leading that effort. Um I'm very happy that for example Zonda was taking part of it. I

really like Zonda tools for static code analysis. It's great things. committee designed thing. Sometimes that works, sometimes it doesn't. I think this was one of the occasion where this worked. What makes this so special? In contrast to many other um sets of annotations, it gives you not a binary, non-null, or nullable. It gives you a ternary option, right? It leaves the way to keep your code as

it always has been. Stuff in Java can be null, can be not null. What it is in the end, you never know, right? The language didn't give you a language to speak about it. JSpecify changes that. It gives you language to say this part of my code is null safe, um, trust me and this part is not null safe null safe. Use the unspecified types for anything

that you cannot or won't migrate, right? So, we had this this term today already a couple of times. It's all about intent, really. In the box of J Specify behind these coordinates are only four annotations, nothing more, nothing less. There is actually no tooling in it. Um, I think this is great, uh, because it will give that version number some kind of stability. There's only so much

you can change with four annotations, right? There's no other tooling that you need to change. And you can put this into your library as a transitive dependency. So, these two pairs of annotations are one for marking stuff as nullable or not nullable and two two sets for for scoping. Why I'm saying this thing with transitive dependency because these annotations have runtime retention. That means after compiling your

library, your program, they stick in the byte code. They are effectively transitive dependencies. You can exclude them from being transitive, but that will give your users all kinds of funky errors. Some personal experience and at least saw Mark in the room earlier with API Guardian. API Guardian is also a set of metadata annotations that gives you more language to express whether an API is, um, stable, experimental,

deprecated, or whatnot in a greater way than you can do with just at deprecated. At one point, um, when using API Guardian, I was frustrated about that that it becomes part of my API, but think about it. You express something about your API. It is intrinsically part of your API. So, this is a given that that needs to be runtime retained. But also, it becomes part of

your API. You cannot just throw this in in a minor release and have a nice day. You will break people down the road, right? None of these annotations are inherited. So, you have to be very explicit again. There There's intent to be set. You can mark an interface, but you need to also um mark annotations um implementations, sorry. They're also not repeatable, but this is a is

a side show. So, scoping. You go ahead and use null marked or null unmarked for defining null safety in any scope. So, I listed the important scopes here. These are Java module systems, packages, types, method, and uh constructors, right? Null marked, null unmarked go on those, and you say everything in that scope is Null marked meaning everything that is nullable needs to be annotated explicitly. I personally

would go uh to annotating the module info every time these days, especially in libraries, because uh that view here is a nice little lie that uh IDEs tend to present. You Java packages are not hierarchical, even though they look like a tree. If you modernize a framework like Spring Data Neo4j that is not on the module path, you need to annotate every package you want to be

null safe. So, package names doesn't give you hierarchy. system does. So, I'm a big fan of the module system. So, I already said this um my personal approach was marking everything um null marked by module and then see what breaks. undo the um you can undo null marked in a smaller scope with null unmarked. Um and the reverse is also true. I would just use null unmarked

for single packages and modules or for some classes in packages that I cannot migrate yet. So, my go-to annotation would always be the one thing to to express my intent. yeah. The whole the whole concept of J Specify is based on on a formal type system. Um there's a huge and I think fantastic documentation about the operators that are present for tooling. So, we're talking about the

fact that a nullable string is a very different type than a non-null string. One type contains the null element in its union type, the other does not. So, they don't mix and match. They don't match in uh collections, they don't match in anything that is generic, right? yeah. It's it's basically type theory the the whole way down. So, I put in like a warning from J Specify

that complains about doing funky stuff in the predicate and we'll come to that in a in a second. This is the stuff you're going to see when just annotating There's a yet unnumbered uh JEP, Java Improvement, that speaks about null restricted and nullable types. It use very much the same reasoning than J than than what J Specify does. It just uses a different notation for nullable strings.

If that will work out, we will get at some point in the future string question mark for nullable strings, string bang for non-nullable strings then, of of course, other types. this is maybe unusual. We're dealing with a lot of different types in different um this different ways to declare nullable things in generics and lists, right? I really like that examples because it give a whole idea what

you can do. Here, the list interface is null marked. That means I need to be explicit about everything that is nullable. what is not nullable is the add function. I must add something to it. I cannot add null to it despite the fact that the list can contain nullable objects. Getting something by an index will always um return something, but that might be nullable. Sorry, I'm it's

the same type. So, I can add null to it. So, I don't need to be explicit here. Getting the first element, of course, gives a nullable nullable thing when the list is empty. Maybe first never gives an optional that is null, but an optional that might contain a nullable thing. Um it's a it's a whole new It feels like a whole new language of expressiveness. How does

this look in practice? There's one easy way to turn your nice project into a sea of aqua or orange-ish color. Like, we're going to dig into this on the on the right-hand side. Uh sorry, on the left-hand side, I'm annotating the module declaration of a program of mine. And uh and the uh right-hand side turns orange in a couple of cases. My OCD in IntelliJ is really

triggered by this if I see the sea of orange warnings. So, what have we here? At the top, uh IntelliJ is complaining about the volatile string parameter, whatever that is. That that thing here. It's complete- computed in a lazy fashion. It's not assigned in construction of that class, so this should be now as marked as nullable because it is null on initialization. The next complaint is the

check whether that string parameter in that method filter bomb from string is null or not. That is superfluous if I'm living in a perfect world where this is checked by the compiler. The next errors are yeah, errors because I'm actually passing null values to the constructor types. So, it's telling me hey, you're messing up somewhere else. Then again, I have superfluous checks. Let's see how to deal

with them by slowing down a bit. So, this was an exempt um from from a project of mine. This is just a fictive library of a calculator. So, I didn't put anything in the package scope here. I marked the whole class as null marked. Meaning the sum and here is now a required parameter. It can never be Then I have nullable integer, nullable this syntax or this

variant to use type-based annotation always triggers me off. The funky thing here is that the variadic parameter, the dot dot dot, is nullable in its own right. That is the second nullable in that argument list and the content is also uh nullable. It's the first at nullable here. That's just due to the fact how array parameters are declared in in in the Java world. Then we have

a couple of checks in that code and we're going to walk through this. Why does this compile? Um The the the nice example here, it shows all the valid inputs to the method. Giving it one parameter, sure gives a one. Giving it the four including the null works just fine because cannot move back here. It doesn't Giving it a null works just fine because I have the

null check in in the sum in the middle. Um gives you a sum of six. The last one gives you a one again, no exception. I would say I annotated the the crap out of it. That should not compile, but it does with a warning. You can even write a test checking for the exception that I had in the objects required non-null that compiles, runs, and works.

So far, so good, so sad. At least I was sad for a moment. Because it was already enough work to annotate it. So. NullAway, the things you have to do if you bolt those things onto a language that comes not with these intrinsic, right? NullAway is a error prone error prone plugin developed by Uber. Um Error-Prone in its own right is a Java C plugin that augments

the compiler for more errors to check. It basically hooks into the compilation process, into the type analysis, and a couple of more things, and it just helps you to make your code better. It's It's a good good thing to use. NullAway itself reads J Specify annotations and a whole bunch of other ones I had before on the slide. NullAway enforces explicit nullness and checks possible null references,

exactly what Hoare wanted to have in the end. Like back to this example, I really don't want to compile this, and I actually don't want to have that check for the exception here. And with NullAway, we can turn this compilation into that error here. I think it's a nice error messages. A nice error message tells me exactly what I'm doing wrong, the right place, and even contains

a link to where I can look up this this this error message. So, this is exactly what we want. To use J Specify successfully, you need to use it with something like NullAway and ErrorProne. Otherwise, it's kind of pointless, in my opinion. We can even get more errors out of it. You see the if others equals not equals null and objects require non-nulls in the middle, I

can remove them and IntelliJ says, "You're messing up here." It turns occur again. Rightfully, but still compiles. NullAway gives me again a detailed error message that the um enhanced for loop will break on run time. I did expect two errors here because the contents of the variadic argument might also be null. I think this is an oversight in NullAway here because it's obviously detectable and part of

the specification. So, maybe it's worth filing an issue afterwards after that talk. You might rightfully ask why I still have that null check in uh the initial initialization when I'm now living in a world that is null safe. >> [snorts] >> Like, the reality is not perfect. I was setting the context at the beginning of this talk. I'm a library developer. I cannot force any of you

to use that great calculator library with NullAway applied. And this holds true for Spring Data Neo4j, for other frameworks that use uh J Specify annotation, for example, um Jackson was also up for the task. You need to have this in there. I'm not living in a perfect world. And there's a jolly good chance that a null makes it to the code path and it blows up into

my face or in your face, that is to say. Does anyone know already the good solution for this, what I'm going to suggest here? I I wanted to say, "Just going to use Lombok and annotate this parameter twice and have Lombok generate the bytecode for me. I think this is sensible and please roast me on social media for that." I I mean No, you should not do

this, really. Have you tried Kotlin, though? >> I mean, in Kotlin this stuff is in the language, but I'm not only putting that for funsies in here. Uh JSpecify annotations have a real implication of that code. You're uh somebody who used that calculator from Kotlin on uh the left on the right-hand side, the Kotlin code compiles just fine. I have two optional integer variables, the one with

the question mark. The assertion works Kotlin assumes everything is potentially nullable in Java. I go ahead, smash those annotations on the class as in the example, and you yell at me because your Kotlin code does not compile anymore. The Kotlin compiler reads those annotations and now knows that the first argument to that method cannot be null, and yeah. Obviously, I can yell a little bit more, and

then it works again, but adding those annotations should be done in a major release, not in a minor one. the one thing that I kind of found the hardest was the plumbing, at least for Spring Data Neo4j about a year ago. Uh and yeah, Spring Data Neo4j is on Maven. And the whole thing was making Arrow prone and NullAway work. Um back then, the documentation was very

Gradle specific, sparse on the Maven side. And yeah, took a couple of iterations to make Arrow prone work with the compiler plugin. Java these days, since 21, restricts access to the to the to the to their modules more and more. You need to be explicit again in 10 to say which modules you use. And error-prone needs to use all those modules from the compiler to do its

magic. This is a list I copied back from the um um error-prone documentation. I think it grew in the meantime a bit. But, you need to have to do this. The rest are basically compiler options. Some of them I've never heard, honestly, before. And they are kind of easy to configure. The set out configure compiler plugin. And um the first thing you have to do is change

how the Java compiler works with with its set of um files. You need to go from by to do to simple or by file. Both works. Then you make it uh stop uh the flow on error. This is still only Java compiler. And then you say, "Java compiler, please use the error-prone plugin." And within error-prone, nullaway, stop on error. this is I think this is essentially only

do this on null marked packages. So, I'm not even consider it null unmarked packages. I don't want to touch them. I think this is pointless. If this gives you too many errors, um I would go again with an exclude-include list. Um disable all the error-prone checks, and then selectively enable nullaway so that you can deal with those. Otherwise, you end up with potentially a lot of errors

[snorts] Um a year later, we are kind of happy to be able to just use a plugin. This is from an Asian developer. Really helpful nullability Maven plugin. It does all this compiler configuration for you. It brings in the JVM config, and it has also the potential to spare you annotating every package variable under the sun. Um tried this out on my own project. No need to

read the diff. The The idea here is that it is not negative, right? So, really nice easy straightforward to use. maybe you're wondering about the recommendation. I'm I'm kind of twofold here. If you're a developer, application developer in this day and age, and starting a fresh application from scratch in a null-safe framework like Spring Framework 7, I'd say there's a good chance to start using it right

from the start. It doesn't cost you that much if you do this from scratch including the null in in in your in your CI pipeline. Existing projects, I feel are kind of harder. I I put on a slide that migrating it was no fun, but this is kind of a lie. I liked going through the code, right? I really did because I found stuff that were actually

wrong where I missed out on null checks over there was superfluous. Depends on how your head ticks, really. And as a library developer, if you're fine of changing your API and do you want to participate in that new world of null safety, I would say it's a go. Try it out. But be aware that you increase the work of maintenance you have now. You need to be

aware that you need to maintain the annotations, and you need to keep the compile time You can be part of the ecosystem or not, My approach to this was just look for nullable type usage. Everything that there has There is already a null check, it's definitely nullable. Mark this as nullable. Mark everything Run the analysis on your code and the annotated code. You hopefully have tests in

place that exercise the code paths. Run analysis on those two, and in the just grind through it. We did this on Spring Data Neo4j. I did this on Neo4j Migrations, which which is a Flyway-based database migration tool to move databases from one version to the other. And I did this last week in about 5 hours of active working time. Um we also maintain a JDBC driver for

Neo4j. I'm kind of reluctant to put this in there because this is a standing API. I would not rather it's a standardized API. I would I feel uncomfortable changing that. builder for our query language for Cypher. I'd love to. I just don't have time because I work at the moment in another new internal project, but I'm actively considering adding these bits on our new parts that we

have for the same reason I had for starting a new application from scratch. Um you will have some more time for your lunch break, I guess. Um coming to my fair to the famous last words. How about your favorite AI tool? When you do this 100% straight, this is all kind of boring stuff. It's formal verification all over, completely static analyzable, and the stuff that Hoare wanted

to have 60 years ago. For an You know what I'm wanting to say. For looking at your code base, you don't want need that glorified pattern recognition machine. There's better tooling for it. Does it help you to work through this task of modernizing a project? Well, it depends, and I did try this with Claude last week, latest model whatsoever. that field that we are talking about here,

a placeholder field, and the weather are in fact nullable. It even says so in plain English in the comment. And the fun fact that feed that particular feature I used even clawed for implementing it. Somehow it should have been find a specification I wrote for it. What it did it annotated the field correctly. It missed out on the wizard. It did ignore my instructions for finding out

how to run Maven and get the I have two options. Bash this until it works or just fix it myself. I went for the latter because I found that very less frustrating than arguing with the machine in all honesty. So, maybe this is not controversial enough. I have some more stuff. How about the JVM and the future of Java? I mentioned reactive programming. This was all the

rage 6 years ago. We did a lot of effort to get there. And I know project reactor and all this things have a lot of stuff built in like back pressure, flow control and what have you. And the reality is seems to be quite happy with asynchronous with futures and giving virtual threats these days and structure concurrency to come. I think the way forward for me, again

opinion, is clear. Same as GraalVM native image. Don't get me wrong. I love GraalVM native image when it works. It's fantastic for CLI binaries, but it's a hurdle to get really running in big applications in The stuff I mentioned on the slides project laden with class data sharing, profile guided optimizations, the head of time class loading and linking may not give you 100% performance start off of

GraalVM, but if you get 70 to 80, you may be fine. We might see something else with JSpecify and the JEP I mentioned in the future. My hope is that this will be less of of a this diverge here because it uses the same theory as an underlying thing. So, I feel confident in using this stuff now and not expecting too many too many hiccups in the

future. Also, having said that the set of annotation is rather small and now basically done, you will be fine if these annotations come from multiple of your dependencies. I did like the keynote this morning. So, I stole something here from Marcus. Annotations are cheap, intent is hard, verification is everything. That's basically what Marcus said about the AI coding thing and we find the same Monica here. So,

the old is the new new, I guess. And uh that Oh, somebody wanted to take a picture, feel free. Uh yeah, that's all I have. Um feel free to contact me on social media or by email. I do write books. And what I find really funny here, the thing that most up-to-date is the arc42 by example thing, architecture documentation. It's now from 2014. I think in the

age of AI, this is more relevant than ever to be able to express yourself and the thing you want to be building. Thank you. >> [applause] >> Uh I have a couple of question in the Slido. Um I'm just starting from the top. Why is it important to consider null when we have optional of nullable? Um optional of nullable might contain something that is null. Um so,

you might use or else throw when dereferencing it, but you still have a nullable object in your hand. You need to check the path of what's inside it so that you can formally verify that the local variable is null or not nullable. I've forgotten that doing the slides. Um there is no annotation for local variables for a reason. If I'm completely in a null safe scope, I

can formally prove that nothing inside that scope is null. So, that's the reason I need to look into the nullable whether its content is null or not in addition to the throwing getter or the explicit throwing method. Then the next question, um when to use null unmarked? Um I personally did not use it. I would use it um if I have think about a gigantic class whatever

as part of a package with a lot of branches and checks in it. You you don't have time for. Or you have some other formal proof that nothing in there can happen. You might just want to exclude that class from your work. How does it all affect lib users? Yeah, as as a Spring Framework user or um Neo4j Migrations user, you can compile your own application with

NullAway and make sure that NullAway detects that you pass null values to our library. So, it helps you. It gives you a benefit. If you don't use it, it's just there. It's part of the deployment, but nothing bad happens. Um then should we swap all old annotations to J Specify? Uh I think JetBrains null annotations are not I cannot comment on the latter. I don't know that.

I was angry about JetBrains annotations because there are actually a lot of changes in in versions, and if you deal with um mixed code bases including Kotlin, you have to deal with mixed versions of those annotations. Would I swap out old annotations? Definitely not, I'd say. I would rather check if I can use them with another way in our pool if I'm up to up to this

stuff. Especially in applications, I'd say. I mean, what's the Uh yeah, other ecosystems do rewrite all the time, rewrite all the time. I would that far, really. Opinion again. How do you handle object fields that are statically nullable but in a certain message supposed to be non-null? Hard to say, right? In that certain message, I expect null checks, right? If I say this thing Don't know if

this answers the question. Will you switch to {exclamation mark} and {question mark} if Valhalla JetBrains will be merged? I don't know if I'm still doing software engineering at that time, so I'm not [laughter] I'm not looking in the glass ball, right now, so. >> For more bad puns, follow me on Mastodon, please. >> [laughter] >> So. Thank [snorts] you. Have a nice lunch.

From event

JCON EUROPE

20 Apr 2026 – 23 Apr 2026

All event videos
Back to Watch