value type hygiene

John Rose john.r.rose at
Tue May 15 06:36:31 UTC 2018

On May 14, 2018, at 6:53 PM, Paul Sandoz <paul.sandoz at> wrote:
> Hi John,
> The answers below might depend on experimentation but what might you propose the behavior should be for the following code, assuming we have no specialized generics, ArrayList is not yet modified to cope better:

This is the right sort of question to ask and answer about value types,
and the sooner we get L-world up and running, the quicker we can
validate our answers.

We'll probably end up with a bunch of rules of thumb about how to
handle nulls.  In this case, null is a sentinel value used by the external
API.  There are two main choices with cases like this:  (a) disallow
the null when used with null-hostile types (V[]) and (b) adapt null
to another value when a null-hostile type is detected.

> value class Point { … }
> class VW {
>  public static void main(String[] s) {
>    List<Point> l = new ArrayList<>();
>    l.add(P.default);
>    l.add(P.default); // assuming this works :-)

(There's no reason why it shouldn't work.)

>    Point[] p = new Point[10]; // Flattened array is created
>    l.toArray(p); // What should happen here?  
>  }
> }
> (I know toArray is value hostile and maybe should be deprecated or changed but I find it a useful example to think about as it may be indicative of legacy code in general.)

In the case of List.toArray(.) the simple answer IMO is (b), and the
sentinel value is clearly T.default, to be computed something
like this:

diff --git a/src/java.base/share/classes/java/util/ b/src/java.base/share/classes/java/util/
--- a/src/java.base/share/classes/java/util/
+++ b/src/java.base/share/classes/java/util/
@@ -186,13 +186,13 @@
         for (int i = 0; i < r.length; i++) {
             if (! it.hasNext()) { // fewer elements than expected
                 if (a == r) {
-                    r[i] = null; // null-terminate
+                    r[i] = a.getClass().getDefaultValue()
                 } else if (a.length < i) {
                     return Arrays.copyOf(r, i);
                 } else {
                     System.arraycopy(r, 0, a, 0, i);
                     if (a.length > i) {
-                        a[i] = null;
+                        a[i] = a.getClass().getDefaultValue()
                 return a;

Eventually when int[] <: Object[], then int[].class.getClass().getDefaultValue()
will return an appropriate zero value, at which point the above behavior will
"work like an int".

Another way to make this API point "work like an int" would be to throw an
exception (ASE or the like), on the grounds that you can't store a null into
an int[] so you shouldn't be able to store a null into a Point[].

> Should the call to l.toArray link?

Yes, because Point[] <: Object[].  There's a separate question on whether
the source language should allow the instance List<Point>; I think it should
do so because that's more useful than disallowing it.

> If so then i presume some form of array store exception will be thrown when ArrayList attempts to store null into the flattened array at index 2?

In the case of the List API it's more useful for the container, which is attempting
to contain all kinds of data, to bend a little and store T.default as the proper
generalization of null.  Under this theory, Object.default == null, and X.default
is also null for any non-value, non-primitive X.  (Including Integer but not int.)

> Or:
>  Point[] p = l.toArray(new Point[2]);
> a flattened array is returned (the argument)? assuming System.arraycopy works.

It does.  (Or will.)  Reason:  Point[] <: Object[].

> Or:
>  Point[] p = l.toArray(new Point[1]);
> a non-flattened array is returned?

The reflective argument's class is Point[], so Arrays.copyOf has no choice but
to create another instance of Point[], which will also be flattened.  It appears
that Arrays.copyOf won't need any code changes for values.

(As I replied to Frederic, it is technically possible to imagine a system of
non-flat versions of VT[] co-existing with flat versions of VT[] but we shouldn't
do that just because we can, but because there is a proven need and not
doing it is even more costly than doing it.  There are good substitutes for
non-flat VT[], such as Object[] and I[] where VT <: I.  We can even contrive
to gain static typing for the substitutes, by using the ValueRef<VT> device.)

> since Arrays.copyOf operates reflectively on the argument’s class and not additional runtime properties. 

I don't get this.  What runtime properties are you thinking of?  ValueClasses?
That exists to give meaning to descriptors.  The actual Class mirror always
knows exactly whether it is a value class or not, and thus whether its arrays
are flat or not.

> What about:
>  Object[] o = l.toArray();
> A non-flattened array is returned containing elements that are instances of boxed Point?
> Paul.

Yes, this is a non-flattened array, since Object[] is never flattened.

Here's another option if (a) and (b) don't work out for List:  Globally
define a mapping between value types and null, and make the VM
silently "unbox" null into the correct value type.  This isn't a cure-all,
because it masks true NPE errors in code.  And it only applies when
a null is being stored into a container which is strongly typed as
a value type.

When faced with a non-nullable container of a value type VT,
promote stored nulls to VT.default, for all VT's or else for VT's
which opt in (maybe VT <: interface PromoteNullToDefault).
If we buy that trick, then a[i] = null turns into a[i] = VT.default
automatically everywhere, not just in AbstractCollection.
This is technically possible but IMO would require experimentation
with a real VM running actual code, to see where the paradoxes
arise from erasing nulls quietly to VT.default.

I'd rather try to get away with changing the API of list to store
not null but rather VT.default, when the passed-in array is a value
array.  This change only affects behavior on new types (value arrays)
so it is backward compatible, in some strict sense.  And it is arguably
unsurprising to a programmer who is working with value arrays.
At least, I think it is a defensible generalization of the old rule to
store a null after the last stored output value.

— John

More information about the valhalla-spec-observers mailing list