Make Null a subclass of AnyVal under -Yexplicit-nulls

This is true, a common gotcha, which is why many of us wanted “explicit nulls” in the first place.

My preference would be the opposite of this proposal … this shouldn’t be true either:
Null <: Any, or in other words the root of everything should be Any | Null.

Any should NOT be the root, because Null isn’t an object, and thus misbehaves when it comes to any of the methods defined on Any. Null cannot be Any because that still violates Liskov’s substitution principle, unless you change what Any means.

For example, this code should trigger compile-time errors under -Yexplicit-nulls, but it currently does not:

scala> null.equals(1)
java.lang.NullPointerException: Cannot invoke "Object.equals(Object)" because "null" is null
  ... 32 elided

scala> val x: Int | Null = null
val x: Int | Null = null

scala> x.equals(1)
java.lang.NullPointerException: Cannot invoke "Object.equals(Object)" because the return value of "rs$line$2$.x()" is null
  ... 32 elided

And if this happens, then what are “explicit nulls” good for anyway?

And I don’t know how Java’s future additions to the language will unfold, however, what I said is true for both:

  • Kotlin, which has Any vs Any? (e.g., Any | Null) — the above code in Kotlin does generate compile time errors.
  • Java’s JSpecify, Object being non-nullable, so you need @Nullable Object, this being a PITA in code, because type parameters are by default considered not nullable, since <T> is considered to be <T extends Object>, therefore the code gets infected with declarations like <T extends @Nullable Object>. So even in Java’s solutions, Null is not Object.

So for me Null <: AnyVal is wrong because Null <: Any is already wrong as long as Any =:= java.lang.Object.

To showcase Kotlin, the sample above does yield compiler errors and in my opinion, when trying to model “explicit nulls”, Scala should be inspired by the languages that have been doing it for a while:

5 Likes