What if null was an Object in Java?
donraab.medium.com
donraab.medium.com
After using Typescript for the past couple years, it's such a joy when you define a variable of type Foo and know it won't contain null. Granted, TS also has to deal with undefined vs null weirdness (and even more weirdness in that an object not containing a property is subtly different from that object property being undefined), but in general the support for "optional" typing works very well.
Java tried to add things like the Optional class, but without first-class language support it's just a mess and you have developers doing crazy things, like having a method return an Optional that will sometimes also return null, by design (yes I've actually seen this).
Turns out some ops without assignment and some printed messages would go into the pipeline (as in, to be piped in with `|`) and become part of the return value of the functions ; to make that work powershell changes the return type, appends the message or the non-assigned op to the hashtable and returns that as an object table.
To this day I'm not aware of any language doing this kind of thing, but maybe js does ? Anyway I fear I'm going to get some grey hair because of that.
Wow that's terrible. When Optional landed in Java (8?) I came to the conclusion that the point was to use it as a contract from the developer of a method: "I return an Optional in this method so I guarantee it won't ever be null". If this is not respected, there's absolutely no reason to use Optional.
Why is that? I've never seen a good explanation of this point of view.
This all feels like just syntactic sugar to me. Sure you might not have null, you might have an empty object or any other variation. At the end of the day, if a value was expected to be available but is not available (for whatever reason), reliable code is going to have to deal with that absence. No matter how it is expressed.
If you have a non-null returning function that you need to change so it can return null, then your typechecker will tell you all the places where you now need to handle the new null return.
It doesn't need to be a very large codebase before this becomes a very useful tool to help when refactoring.
https://www.infoq.com/presentations/Null-References-The-Bill...
It's a shame Java was designed before they had generics, so the core of the language can't take advantage of them.
You can actually write extension functions for nullable types and calling them on a null value does not cause a Nullpointer exception. This also works with Java classes.
For example, there's a CharSequence?.isNullOrBlank(): Boolean function in the Kotlin standard library that allows you to implement a null or blank check on strings. There's a similar isNullOrEmpty function on Lists (and yes, this does work for nullable generic types too).
There are many good reasons to switch from Java to Kotlin. But this is probably one of the bigger ones and genuinely low hanging fruit. Made me feel stupid after years of dealing with Java NPEs routinely and writing a lot of very verbose and needlessly defensive Java code in attempts to avoid them. All that cruft goes away with a proper language. Other languages that do this too are available of course. Pick one. Any one that isn't Java. Dealing with this stuff in 2024 is stupid.
In java I can make my own static function
isNullOrBlank(String input)
Which works on nulls too, right?
It's annoying to have to reference another class - but otherwise it's not much less ergonomic.
So T? doesn't compose, while Option<T> does, which I'd consider a big difference.
However, without a builtin option type, duck-typing, or a pleasant way of converting the types, Option<T> may become a hassle (especially when different dependencies ship their own). And as T? is shipped with the language this is probably why it is used when the composability is not required.
P.S.: In C# T? even neatly composes.
return obj?.a?.b
is equivalent to if (obj != null && obj.a != null) {
return obj.a.b
} else return null;Which leads to a bit of an argument - nesting gives poor ergonomics, while collapsing impacts the ability to write generic code.
Does nesting Option's really has practical use or does it quickly become confusing?
In your example, Option<Option<T>> return type doesn't tell me by itself that this differentiates between a missing file or a missing setting. I would need to get this information from somewhere else.
In other words, nested optionals might not be very readable, but they're a necessary product of having a highly modular, reusable toolkit, and you can always massage them into more informative, domain-specific types at whatever point that becomes appropriate.
The thing is that nullable covers majority of cases and is quicker to read and write (Foo? vs Option<Foo>). Doing a?.b?.c is also more elegant than anything equivalent using an Option type.
Is there any languages that successfully combines both, nullable '?' syntax and an Option type?
Rust has dozens of helper methods for option - like map, map_or, map_or_else, and so on. When you’re starting out, it’s quite hard to find what you want in the morass of options. And many of the helper functions take a closure, which messes up your ability to do control flow (return, break, continue) from the containing function. In Swift, like typescript, I found the syntax sugar around options to be much easier to learn. Eg obj?.a?.b rather than checks docs obj.and_then(|o| o.a).and_then(|a| a.b).
“If let” in rust helps. (Similar to guard let in Swift) Ie, you can write if let Some(x) = x { … }. But you still can’t combine that with other conditions in the if statement - which drives me nuts.
myobj.dothing(x).otherthing(y)
Without having to check the intermediate results. When it's just a tagged union you end up having to check for null everywhere anyway. It's better than "untyped" null because it can't pop up anywhere but it's not super ergonomic. I think Nullable where the Option is implicit does it better.Of course the Rumsfeld Matrix further breaks down the three different types of unknowns.
https://en.wikipedia.org/wiki/There_are_unknown_unknowns
>"Reports that say that something hasn't happened are always interesting to me, because as we know, there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns—the ones we don't know we don't know. And if one looks throughout the history of our country and other free countries, it is the latter category that tends to be the difficult ones." -Donald Rumsfeld
1) Known knowns: These are the things we know that we know. They represent the clear, confirmed knowledge that can be easily communicated and utilized in decision-making.
2) Known unknowns: These are the things we know we do not know. This category acknowledges the presence of uncertainties or gaps in our knowledge that are recognized and can be specifically identified.
3) Unknown unknowns: These are the things we do not know we do not know. This category represents unforeseen challenges and surprises, indicating a deeper level of ignorance where we are unaware of our lack of knowledge.
And Microsoft COM hinges on the IUnknown interface.
https://en.wikipedia.org/wiki/Tony_Hoare#Research_and_career
>Speaking at a software conference in 2009, Tony Hoare apologized for inventing the null reference:
>"I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." -Tony Hoare
https://news.ycombinator.com/item?id=19568378
>"My favorite is always the Billion-Dollar Mistake of having null in the language. And since JavaScript has both null and undefined, it's the Two-Billion-Dollar Mistake." -Anders Hejlsberg
>"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg
Eg they can't distinguish between one or two layers of nullable. Or between null and an optional null.
(In eg Rust syntax between `Option<Option<MyType>>` vs `Option<MyType>`. Or `()` vs `Option<()>`, where `()` would be how you spell `null` in Rust.)
Using tags is simpler and cleaner.
It's incredibly clean and convenient until you wake up one morning and have no idea what is happening in your code
Integer, + => 0
Float, + => 0.0
Array, add => []
Hash, merge => {}
and so on.
I think maybe we can debate the operations/functions, but they make sense. For Integer in some ways you can define almost all other operations that you commonly use based on the addition.
So while nil is an object when trying to find a representation in other group I find it logical or expected.
Also Ruby will not automatically try to coerce nil when not asked to do so
like for example 0 + nil will throw an error.
But, for me, it makes sense to have a default representation for Null (that it is not automatically coerced—only if the developer explicitly asks for it) for one of the most common operations in that specific group.
I love using Ruby when I'm the only one who will ever have to look at or touch it.
I somehow can't help reading that as some sort of high school sports-cheer: "Gimme an S to the quote to the I to the oh to the F to the zero! Goooo Rubies!"
It doesn't solve anything fundamental though. You could make null.toString() return "null", but in most cases that will just be a bug. You're missing a null check or failed to initialize something.
It's, technically, an instance of multiple inheritance. Java doesn't generally allow this, but there's tons of special cases in the compiler for things that you can't do yourself. For example, defining operators is done in the compiler, but you can't define operators for your own classes.
You could get most of the way to the blog post's outcomes by simply adding a few methods to Object (is(Not)Null, primarily) and having null be a magic (because it's all classes and none) instance that will NPE on all but the few defined Object methods, I think, but none of that answers the burning question of _why_ that I feel the article doesn't really address.
Kotlin solved Java's problem by making it a compiler error if a value that can be null isn't checked and shown to be null or the actual value, eliminating an entire class of exceptions.
I confess I’m not fond of checker framework. I find the error messages can be obtuse but it is very effective.
The problem with nullability annotations in Java is that they are not enforced, and there is no commonly adopted standard. There are like ten competing libraries with similar annotations. There was JSR 305 ("Annotations for Software Defect Detection"), but it has been dormant. When you import a third party library, you never know what kind of nullability annotations it uses and if it uses them at all.
String foo = null;
String bar = "";
foo.equals(bar) --> true
This works well provided the data type has a sensible zero value like collection typesEDIT: I'm blocked from posting so I won't be responding further, thank you for discussion.
Similarly an Option<byte> has 257 different values while a byte has 256 different values. That the byte has a good zero value doesn’t change that - the whole reason for choosing a maybe-byte is having 257 values, not 256.
As the example of byte vs option<byte> either you want 256 states or you want 257. If you have 256 or 257 states you want to represent will decide which type is correct. The other choice of type is incorrect.
In some languages, these things are blurred because the language doesn’t let you choose a correct type for the state space, but I’m talking about the case where you can (coincidentally the set of languages I’d use).
E.g a boolean in java has two states true/false while a Boolean with capital B has 3 states true/false/null.
In that context you can choose type to represent how many states you have. E.g if it’s a field representing a cache of a bool value you can represent “not yet calculated” with null. It you were to magically convert null to false for a Boolean it only has two states! It’s now unusable for the purpose.
The whole point is to track the absence of a value. Why would one want to eliminate it?
null is way more trackable that some other special value like 0, -1, 999, "", etc.
You need a 257th value to have a byte sentinel.
To put this in more concrete terms, if this change were integrated in Java, how would you indicate the difference between a JSON document which doesn't contain a field VS one where the field is an empty string?
I base this mostly on an assumption of C still being one of the most widely used languages for the code that's running out there in the world. In C, after all, NULL is just a magic value rather than a distinguishable representation of no value, though that's just one example out of a host of others, all the way down to the venerable NUL-terminated string.
As for your question: something like 'keys(jsonobject).contains("fieldName")' ? Or 'NoSuchFieldException' thrown if you do 'jsonobject.get("fieldName")' ?
(The latter of which, given the general uneasiness NullPointerException creates in us devs, is often how a Java API will work anyway! Checked exceptions won the day! Until the Functional interface was made, at least.)
Or to answer it in the same spirit as this overall comment, why would you need to distinguish between missing and empty? Can't you just define the semantics of the document s.t. those two things having the same effect?
> As for your question: something like 'keys(jsonobject).contains("fieldName")' ? Or 'NoSuchFieldException' thrown if you do 'jsonobject.get("fieldName")' ?
> (The latter of which, given the general uneasiness NullPointerException creates in us devs, is often how a Java API will work anyway! Checked exceptions won the day! Until the Functional interface was made, at least.)
I was thinking more of the case where you deserialize a JSON object into a Java object, and then inspect the Java object. Regardless, it was just an example - the problem of distinguishing "no value" from "any value" is pervasive in programming, and all languages must have some strategy for it. If we got rid of null from Java, then Option<T> would probably be the only general candidate. Which, to be fair, would be slightly better, as it would at least force you to check.
> Or to answer it in the same spirit as this overall comment, why would you need to distinguish between missing and empty? Can't you just define the semantics of the document s.t. those two things having the same effect?
Not in the general case, no. At least not without doubling every field and adding other inconsistency issues ({"result": "ABC", hasResult: false}).
Rarely do you have a textual database column where the empty-vs-NULL distinction is semantically meaningful in the application domain. Most of the time, either the column value is missing (arguably better represented by NULL) or has a non-blank value. “Present but blank” is rarely meaningful or useful
Sometimes I pair that with (TRIM(column)=column) to prevent leading or trailing whitespace being saved, which also stops all-blank values being saved
This works really well if you have an RDBMS which supports CREATE DOMAIN, so then you can attach these constraints to a user-defined type and don’t have to repeat them for each column, you just set the type of the column to that user-defined type
Go: *string
Java: Option<String> or @Nullable String
Rust: Option<String>
TypeScript: string | undefined (or string | null)Not if you use Oracle.
Java Optional std lib class, instead of a language (typesystem level) feature was one of biggest mistakes in its recent history.
Variables should be the exception, not the norm. I have no patience for names that designate some fluid work in progress instead of a value.
They can be useful, but I can never remember which one of let, run, with, apply, and also I currently need. Also, I've noticed that they motivate overly "clever" code, especially but not exclusively in junior programmers.
That's why I like writing scope-heavy and then transforming to an aesthetic middle ground between scope expression and imperative. Name stuff when you have something meaningful to say, when there's something helpful to express in a name, not when the syntax mandates a locally unique ID.
I’d put any one of extension methods, value and data classes, immutable variables, structured concurrency, and top-level functions ahead of scope functions for reasons to switch to Kotlin. But hey, if you’re switching, we’re already friends :) .
You missed optional and named arguments in the list, those really change cost/benefit decisions in API design. (and, unfortunately, make argument names part of the public interface, this must be the most controversial part in all of kotlin)
I singled out scope functions because all those other things feel very much like the usual set of language differences, whereas scope functions feel completely orthogonal. They could be added to every single language that has excursions and imperative elements, and they would be about the same improvement everywhere.
> The {@code Void} class is an uninstantiable placeholder class to hold a reference to the {@code Class} object representing the Java keyword void.
When Java introduced Generics they re-used "Void" type. Method calls need to use "null" when "Void" is the type. So in a way, "type of null" is "Void".
Contrast that with SQL, which had my external scorn for null != null
So you can do WHERE table.col = Null…. Ugh
It's all inherited from C. Null works the same, references are just pointers, and primitives have to be boxed since collections store references. Not sure if I'd say it's well thought out, but coming from C, all the weirdness makes sense.
For anyone not familiar, or who doesn't know the reason null behave like this in SQL: in this word, null essentially means "don't know". So you can't assume a = b when a and b are null. They could be different values, just that we don't have them.
This thing being called null, where null has another semantic in the languages from which we send SQL request is confusing, and I suspect many of us would prefer null behave the same way as everywhere else.
The null != null thing is something I quite like, as it simplifies a lot of joins/queries.
and as jraph said, null in relational databases has a very different meaning than null in most programming languages.
Don't know vs not assigned
Nihilistic Oriented Programming is where you don't use any classes, just customized instances of null.
For me the given examples from Smalltalk are more appealing. I would trade null for nil, or at least have some nullsafe construction like .? in swift
value = get(collection, key) do
error(“$key in the collection not found”)
endThis syntax has captivated my attention lately as it seems like a viable alternative to doing an explicit null check before using the value.
Actually using this instead of nulls in any sort of general case would be using CPS for all faillible APIs, and that sounds heinous.
Eliminates null checks, Optionals, and NPEs. Probably moots annotations like @NotNull too, but maybe some use cases need those for client APIs.
Makes iterating data structures like graphs simple. Faster too, because the JIT NOP a Null Object's methods. (Mostly; profile to confirm, then tweak as needed.)
I implement a Null Object for each base class.
Each base class has a static final member NULL referencing its Null Object implementation (flyweight, singleton). Then assign variables to AwesomeThing.NULL instead of null.
A spiffy javac could code generate Null Object implementations. (It's on my todo list.) For scalars, just use the default null value should be. int is 0, float is NaN, etc. Their boxed values will need small shims too.
Customizing javac (with some compiler plugin or something) is deep down on my TODO list. So I'm unlikely to be the (first) person to do this work. Sorry.
And since Objective-C contained a lot of C, it really just separated null dereferences from their causes making debugging much harder.
I don't know Obj-C, so I'll have to research those use cases, to understand better. eg: Are those across processes? Is this an Obj-C idiom? How to make illegal state representations impossible?
Per the Null Object pattern description, Null Object captures all of the null handling and behavior in one place, vs scattered around. Stuff like null checks and throwing exceptions. It does not eliminate the need for the concept of null.
I'm probably (mostly) wrong about eliminating @NotNull annotations. Especially in method signatures (defensive programming). Last night while commenting, I was thinking about annotating POJOs, to info serialization.
It also means that forgetting to set a value will often silently give wrong results instead of immediately producing an error.
While most Null Objects would (probably) be code generated, you can provide your own as needed.
Said another way: Most of my Null Object implementations have been boilerplate, and therefore could be code generated. But occasionally some are not.
It'd be nice if Java had a concept of a never-null reference (like a C++ reference vs. a C++ pointer), but the @NotNull annotation wasn't enforced the last time I checked.
Also, there's no way for an object to express that invariant because encapsulation is so weak. Given only this constructor (and no reflection):
Foo() { foo.bar = new Bar(); /* foo.bar is final; Bar() does not throw */ }
callers can still get an instance of Foo with bar set to null.Anyway, null handling in java somehow manages to be worse than C, where you can at least inline a struct into another, statically guaranteeing the instance of the inlined struct exists.
I can't think of another statically typed language that screws this up so badly. It just keeps getting worse with stuff like Optional and @NotNull.
(Disclaimer: I haven't followed java for 4-5 years; it's possible they finally fixed this stuff.)
Basically Java had nulls from the start. A decade or so later some people who didn't like nulls introduced their own Optional type, as a third-party library. Enough people liked it that Optional was added to Java's standard library.
But as it's just an object, it can be null. Some null avoidance enthusiasts also use third-party @Nullable and @NotNull annotations, which some automated code checking tools will attempt to verify during compile/test.
In a team without Optionals, every time you touch a null that you didn't expect, you have to decide "Is this deliberately null, or was it a mistake?" Without that knowledge, you don't know whether your code should assert against the null, or allow it to pass through as a valid value.
With Optionals, it becomes much simpler to cut through that nonsense. A null is a bug and you fix it (with the exception of json at the boundaries of your system, etc.) If you do find a value where you change your mind about its nullability, changing it to/from Optional will give you compile errors in exactly those other parts of the code that you now have to check/change.
Every non-primitive is nullable in Java. Adding Optional doesn't/can't change that.
You can have a gentlemen's agreement to prefer None to null.
YMMV. Obviously it depends on your teammates.
Java might be the only language where a simple assignment `x = y` can throw a NullPointerException (due to auto-unboxing)
Thread A sets a shared reference to a newly allocated and null initialized reference to Foo:
shared = new Foo();
While that's running, thread B invokes a method on the reference that assumes bar is non-null: shared.useBar(); // null pointer exception
Later, thread A runs the constructor for Foo.For example, this is code that any C or C++ compiler will happily run and do something:
struct Bar {
int b;
};
struct Foo {
struct Bar bar;
} foo;
strcpy((char*)(&foo), "ABC");
Or in relation to null C++ references: int& foo(int* p) {
return *p;
}
int &r = foo(nullptr); //UB, but in practice will likely result in a null reference at runtime
Similarly, accessing an object from multiple threads without synchronization means its value is not fully defined in Java. Unlike C or C++, it is at least known to be a Java type, not a memory corruption vulnerability.In your example, it's the dereference of the pointer to p that is undefined behavior, so anything that happens after that point is also undefined behavior. Note that means there is never an actual assignment to r if p is null.
As I mentioned earlier, this might seem like quibbling with definitions, but this is the proper mental model to have with respect to C++'s semantics.
Having said that, I don't disagree with the main crux of your point, which is that C++'s semantics are terrible and there is little that the language provides to write correct code, but I do think there are subtleties on this matter that are worth clarifying.
My point is that we can compare two things: valid programs, or programs that compile.
In valid C++ programs, references can't be null and there are no data races. In valid Java programs, all final fields initialized in an object's constructor have that value for the lifetime of the object.
If we compare invalid programs that compile, which is an important point as well, then those guarantees go out the window. But here Java is much more forgiving than C++: if you have improper synchronization, you may see fields which are null instead of having their final value, which is bad and confusing. But in C++ with improper synchronization, you can see literally any outcome at all.
https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.ht...
>An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.
If you don't publish a reference to the object from within the constructor, you will not see a null value of the final field, even if the object itself was unsafely published across threads via a non-volatile field.
https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename...
I did hack MACLISP to have multiple NILs once. A surprising number of things appeared to run OK for a while. Of course the PDP-10 had a hardware addressing mode just for NIL so if you weren't the official one compiled code didn't believe you were a legit NIL.
Making another T was more boring, except that the few bugs that were triggered seemed to appear more quickly than in the NIL case. I guess a lot more people explicitly compared with 'T. Eh, grad students, what do you expect?
As to why I did this, well it was decades ago but surely a case of work avoidance.