Conditional members

Brian Goetz brian.goetz at oracle.com
Tue Mar 29 19:52:58 UTC 2016


Yet another in a series of disconnected, bottom-up (starting at the VM) 
memos laying the groundwork for the enhanced generics model.

Basic Problem
=============

It may be desirable, for purposes of expressiveness or migration 
compatibility, to declare class members that are only members of a 
specific subset of parameterizations of a generic class.  Examples include:

  - Reference-specific API assumptions.  In our analysis of the 
Collection classes, we identified various methods that fail to make the 
jump to any-generics for various reasons.  These include methods like 
Collection.toArray(), whose signature makes no sense for primitive 
parameterizations, or Map.get(), which uses `null` (not in the domain of 
primitives) to indicate "not present."  We can't take these methods away 
from reference instantiations, but we don't want to propagate them into 
primitive instantiations.

  - Better implementations enabled by known type parameters. Generic 
classes will provide generic implementations, but sometimes better 
implementations are possible when concrete types are known.  In this 
case, an implementation would provide a generic implementation and zero 
or more implementations that are restricted to more specific 
implementations.

  - Functionality available only on specific implementations.  For 
example, List<int> could have a sum() method even though sum() does not 
make sense on all instantiations.  (This is the declaration-site version 
of what C# enables at the use site with extension methods -- allowing 
methods to be injected into types, rather than classes.)


We've not yet spent a lot of time identifying the proper way to surface 
this in the language.  For methods, one possibility is to use receiver 
parameters (added in Java SE 8) to qualify the receiver type:

     int sum(List<int> this) { ... }

This gets the point across clearly enough (and is analogous to how C# 
does extension methods), but has several drawbacks: doesn't scale to 
fields, nor does it scale well to a conditional-membership model that is 
anything other than "I am a member of parameterization X".  (Where this 
might fall down, for example, would be when we want members declared as 
"I am *not* a member of parameterization X".)

Note that in the second motivating example, there will be two members 
signatures with the same name and signature; we want one to take 
precedence over the other.

We call these "conditional" or "restricted" members.


Classfile Strawman
==================

Here's a strawman of how we might represent this at the VM level.

We define a new attribute, `Where`, which can be applied to instance 
fields, instance methods, and constructors:

|    Where {
         u2 name_index;
         u4 length;
         u2 restrictionDomain;|// refers to a ParamType constant
     }

The restriction domain indicates the parameterization to which this 
member is restricted; in the absence of Where attribute, it is assumed 
to be ThisClass<any, any, ...>.

When loading a parameterization of a generic class, we perform an 
applicability check for each member as we encounter it; in the model 
outlined here, this is a straight subtyping check of the current 
parameterization against the restriction domain.

It is possible there could be duplicate applicable methods; this arises 
when we have a specialization-specific "override", as in:

class Foo<any T> {
     // total method m(T)
     void m(T t) { }

     // Specialization of m(T) for T=int
     void m(Foo<int> this, int i) { ... }
}

When we find a duplicate applicable member, we perform a "more specific" 
check comparing the restriction domains; in this case, the second method 
has a restriction domain of Foo<int>, which is more specific than the 
(implicit) Foo<any> restriction domain of the generic method, so we 
prefer the second member.

This procedure is strictly linear; as each member is read from the 
classfile, we can make a quick determination as to whether to keep or 
discard it; if we keep it, we might replace it later with a more 
specific one as we find it.  Modulo cases where there are multiple 
applicable overloads that are equally specific, it is also 
deterministic; whether we find the generic version of m() or its 
specialization first, we'll end up with the same set of members.

If there are duplicate applicable members in a classfile where neither's 
restriction domain is more specific than the other's, then the VM is 
permitted to make an arbitrary choice (as they are both applicable and 
equally specific.)  The static compiler can work to filter out such 
situations, if desired, such as imposing a "meet rule"; if we had:

     void foo(Foo<int,any> this)
     void foo(Foo<any,int> this)

a meet rule would require the additional overload

     void foo(Foo<int,int> this)





More information about the valhalla-spec-observers mailing list