What is null
?
New Java programmers often struggle with this idea. A simple example reveals the truth:
String s = null;
Integer i = null;
Object o = null;
The symbol null
must therefore be a value.
As every value in Java has a type, null
must therefore have a type. What is it?
It obviously cannot be any type that we ordinarily encounter. A variable of type String
cannot hold a value of type Object
— the Liskov substitution properties simply do not work that way round.
Nor does Java 11 local variable type inference help:
jshell> var v = null;
| Error:
| cannot infer type for local variable v
| (variable initializer is ‘null’)
| var v = null;
| ^ — — — — — -^
The pragmatic Java programmer may simply scratch their head and decide, as many have done before, it doesn’t really matter all that much. Instead they can pretend “null
is merely a special literal that can be of any reference type”.
However, for those of us who find this approach unsatisfying, the true answer can be found in the Java Language Specification (JLS), in Section 4.1:
There is also a special null type, the type of the expression null (§3.10.7, §15.8.1), which has no name.
Because the null type has no name, it is impossible to declare a variable of the null type or to cast to the null type.
There it is. Java allows us to write down values whose types we cannot declare as the types of variables.
We might call these “unspeakable types” or, more formally, non-denotable types.
As null
shows, we’ve actually been using them all along. There are two more obvious places where these sort of types appear. The first arrived in Java 7, and the JLS has this to say about them:
An exception parameter may denote its type as either a single class type or a union of two or more class types (called alternatives).
The true type of a multi-catch parameter is the union of the distinct possible types being caught. In practice, only code that conforms to the API contract of the nearest common supertype of the alternatives will compile. The real type of the parameter is not something we can use as the type of a variable.
In the following, what is the type of o
?
jshell> var o = new Object() {
…> public void bar() { System.out.println(“bar!”); }
…> }
o ==> $0@3bfdc050jshell> o.bar();
bar!
It can’t beObject
, because we can call bar()
on it, and the Object
type has no such method. Instead, the true type is non-denotable — it doesn’t have a name we can use as the type of a variable in Java code. At runtime the type is just a compiler-assigned placeholder ($0
in our example).
By using var
as a “magic type” the programmer can preserve type information for each distinct usage of var
, until the end of the method. We cannot carry the types from method to method. To do so, we would have to declare the return type — and that’s precisely what we can’t do!
The applicability of these types is therefore restricted — Java’s type system remains very much a nominal system and it seems unlikely true structural types will ever appear in the language.
Finally, we should point out that many of the more advanced uses of generics (including the mysterious “capture of ?” errors) are really best understood in terms of non-denotable types as well — but that’s another story.