Fwd: Nullability of primitive types
Brian Goetz
brian.goetz at oracle.com
Thu Nov 4 13:33:21 UTC 2021
The following was received on the spec-comments list. I'll answer here,
because there are common misunderstandings of how the JVM works here,
but I'd rather not get into an extended back-and-forth.
The questioner asks: why not (just) make nullability a use-site
property, rather than a declaration-site property.
One serious issue that this question ignores is that there are many
classes for which not only is the all-zeroes value not a good default,
but *there is no good default*. As a simplified illustrative example,
take Rational:
primitive class Rational {
int n, d;
Rational(int n, int d) {
if (d == 0)
throw new IAE();
this.n = n;
this.d = d;
}
float toFloat() {
// HERE
return (float) n / (float) d;
}
}
Rational r;
float f = r.toFloat(); // DBZE
You might decide this is harmless in the case of Rational (after all,
you pretty quickly get an informative exception when an uninitialized
value is used), but let's zoom in on what is happening at HERE; an
instance method was invoke with a receiver that does not respect the
class invariants, because the instance *did not even come from a
constructor*. (Can you think of another situation where potentially
invalid objects are produced outside the constructor? Serialization,
perhaps? How did that go?)
For classes where the default (zero) value is valid, we can argue over
whether this is safe enough. But for classes where the default value is
*demonstrably outside the domain*, some degree of protection from
uninitialized values (which are unavoidable; arrays are always
initialized to the default value of the component) is required. When
you pull on that string, you find yourself down one of two paths: either
you are inventing a new null with similar but slightly different
properties (maybe you call it undefined, but it's a new null), or you
accept that "the natural default for this class is null." We tried the
former path for a while, but realized that it was an exercise in wishful
thinking. So what you come to is: nullability is a declaration-site
property of this class.
This example might not be too compelling, because Rational is so simple,
but think about how it undermines a basic principle of encapsulation.
The representation is supposed to be an implementation detail, and under
the control of the author, and users shouldn't have to think about
representation. But to code this class safely, you'd need "null" checks
on the entry of each method. No one will want to do that, and people
will forget. Now, all we need is one class that performs a security
check based on the object state, and accidentally interprets zeroes as
"no restrictions". If people are going to "code like a class", they
need a safe way for it to behave like objects we are used to, and this
pulls in all the affordances of references -- nullability, non-tearing,
etc. This is a declaration-site property.
The suggestions on reflection violate an important invariant: that each
descriptor corresponds to a unique mirror. (How would you differentiate
between them in a MethodHandle lookup?) While I am sure it is possible
to concoct a scheme to make this appear to work, at best this pushes
complexity somewhere else, but most likely, increases it dramatically.
For the "true" primitives, you get almost exactly what you're asking
for: Point is non-nullable, Point.ref is nullable (because its a
reference, and references are nullable.) Is this just a really
roundabout way to say "I would like a different syntax"?
-------- Forwarded Message --------
Subject: Nullability of primitive types
Date: Thu, 4 Nov 2021 09:03:23 +0100
From: Gernot Neppert <mcnepp02 at googlemail.com>
To: valhalla-spec-comments at openjdk.java.net
Dear experts,
regarding the ongoing debate about nullability of primitive classes:
even though we are talking about nullable vs. non-nullable _types_,
wouldn't it help to focus on null-permitting _usecases_?
There are class-members, method arguments and return-values of
primitive-class types.
IMHO, it would make real sense to defer the decision of permitting nulls to
each of these use-sites.
The "nullability" would be represented at each _use-site_ by the
corresponding Descriptor (L vs. Q) in the class-file,
and the _default_ would (obviously) be "L" for normal classes, and "Q" for
primitive classes.
Given a primitve class java.util.Complex,
the descriptor for a class-member declared "Complex number;" would be
"Qjava.util.Complex;"
wheras the descriptor for a class-member declared "@Nullable Complex
number;" would be "Ljava.util.Complex;"
Likeweise for method-signatures.
That way, there'd be no need to have two different class-mirrors for a
primitive class.
Of course, you would be able to query the "nullability" of a
java.lang.reflect.Field or a java.lang.reflect.Parameter via Core
reflection.
However, the API for obtaining java.lang.reflect.Methods would not need to
be changed at all!
Two methods whose signature only differed by "nullability" (aka L vs Q
signature) would be considered the same, thus the following would not be
allowed:
class ComplexCalculator {
Complex add(Complex one, Complex two);
@Nullable Complex add(@Nullable Complex one, @Nullable Complex two);
}
As for migration of existing none-primitive classes to primitive ones, it
would be possible to make @Nullable the default _usecase_ for a primitive
class:
@Nullable primitive class Optional<T> {
}
Then, for orthogonality, there'd be a means of specifying "Use this
primitive type as a non-nullable value here", maybe something like
@ByValue Optional<String> getName();
(As a sidenote, maybe a good name instead of @Nullable would be @ByRef).
And yes, with this proposal the Java primitive types (int, long...) would
remain special cases, and they would continue to have their "companion"
reference-types.
More information about the valhalla-dev
mailing list