value type hygiene

Paul Sandoz paul.sandoz at oracle.com
Tue May 15 19:32:51 UTC 2018



> On May 14, 2018, at 11:36 PM, John Rose <john.r.rose at oracle.com> wrote:
> 
> On May 14, 2018, at 6:53 PM, Paul Sandoz <paul.sandoz at oracle.com> 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.)
> 

Agreed.


>>   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/AbstractCollection.java b/src/java.base/share/classes/java/util/AbstractCollection.java
> --- a/src/java.base/share/classes/java/util/AbstractCollection.java
> +++ b/src/java.base/share/classes/java/util/AbstractCollection.java
> @@ -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[].
> 

A third approach could be to check if the array is non-nullable and not store a default value, which may be surprising, but storing a default is arguably less useful in general for arrays of value types but it is suppose mostly harmless (i am thinking of cases where a value type has a default that is hostile to be operated on, like perhaps LocalDate).


>> 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.)

Agreed, i just wanted to do the thought experiment given the current behavior of List/ArrayList as if it's unmodified legacy code.


> 
>> 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[].
> 

Ok.


>> 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.
> 

Ok, i was unsure about the class mirror, and whether there would be runtime associated with the array instance.

And just to be clear so i got this straight in my head...

ValueWorld
—

value class Point {}

class A {
  static void m() {
    Point[] pa = new Point[10];
    B.m1(pa); // returns false
    B.m2(pa); // returns true
  }
}


RefWorld
—

final class Point {} // note that the class is declared final

class A {
  static boolean m1(Point[] p) {
    return p.getClass() != Point[].class ;
  }

  static boolean m2(Point[] p) {
    return Point[].class.isAssignableFrom(p.getClass()); 
  }

}

And, for the same reasons, that also applies to the class mirror for Point in the value world and ref world.

Which got me thinking of the implications, if any, for checked collections :-) e.g. Collections.checkedList, which currently does:

E typeCheck(Object o) {
    if (o != null && !type.isInstance(o))
        throw new ClassCastException(badElementMsg(o));
    return (E) o;
}


>> 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.
> 

Ok.


> 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.

Me too.

Paul.

>  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