[lvti] Handling of capture variables

Dan Smith daniel.smith at oracle.com
Fri Mar 31 23:39:00 UTC 2017


As described in the JSR 286 spec document, inferring the type of a local variable to be a non-denotable type (one that can't be written in source) is something to be careful about, due to "potential for confusion, bad error messages, or added exposure to bugs".

The most significant area here (in terms of likely frequency) is the presence of capture variables in the type. I did some analysis of the Java SE APIs to identify and illustrate problematic cases.

== Case 1: wildcard-parameterized return type ==

Any method (or field) that returns a wildcard-parameterized type will produce a non-denotable type on invocation, because the return type must be captured (JLS 15.12.3).

var myClass = getClass();
var c = Class.forName("java.lang.Object");
var sup = String.class.getSuperclass();
var entries = new ZipFile("/etc/filename.zip").entries();
var joiner = Collectors.joining(" - \n", "<start>", "<end>");
var plusCollector = Collectors.reducing(BigInteger.ZERO, BigInteger::add);
var future = Executors.newCachedThreadPool().submit(System::gc);
void m(MethodType type) { var ret = type.returnType(); }
void m(TreeSet<String> set) { var comparator = set.comparator(); }
void m(Annotation ann) { var annClass = ann.annotationType(); }
void m(ReferenceQueue<String> queue) { var stringRef = queue.poll(); }

Using wildcards in a return type is sometimes discouraged, but other times it's the right thing to do.  So while I wouldn't say these methods are pervasive, there are quite a few of them (especially where the common idiom is to almost always use a wildcard, as in Class and Collector).

There are no capture variables present for methods that return arrays, lists, etc., of wildcard-parameterized types, because capture doesn't touch those nested wildcards:

void m(MethodType type) { var params = type.parameterArray(); }
void m(MethodType type) { var params = type.parameterList(); }

== Case 2: instance method returning a class type parameter ==

A method (or field) whose return type is a class type parameter will produce a capture variable when invoked for a wildcard-parameterized type.

void m(Class<? extends Runnable> c) throws Exception { var runnable = c.newInstance(); }
void m(Map<String, ? extends Throwable> map) { var e = map.get("some.key"); }
void m(List<? extends Set<String>> sets) { var first = sets.get(0); }
Object find(Collection<?> coll, Object o) { for (var elt : coll) { if (elt.equals(o)) return elt; } return null; }
void m(Optional<? extends Number> opt) { var num = opt.get(); }
void m(IntFunction<? extends Reader> f) { var reader = f.apply(14); }
void m(Future<? extends ZipEntry> future) { var entry = future.get(10, TimeUnit.SECONDS); }

If you substitute a wildcard-parameterized type into the return type, that also leads to capture:

void m(List<Set<? extends Number>> list) { var set = list.get(0); }

This is true for for-each, too (for now, javac fails to perform capture correctly, so you don't see this in the prototype):

void m(List<Set<? extends Number>> list) { for (var set : list) set.clear(); }

== Method category 3: instance method returning a type that mentions a class type parameter ==

A method (or field) whose return type *mentions* a class type parameter (e.g., Iterator<E> in Iterable.iterator) will also produce a non-denotable type when invoked for a wildcard-parameterized type.  Unlike Category 2, which tend to be "terminal operations", these types often arise in chains.

var constructor = Class.forName("java.lang.Object").getConstructor();
void m(Map<? extends Number, String> map) { var keys = map.keySet(); }
void m(Map<? extends Number, String> map) { var iter = map.keySet().iterator(); }
void m(TreeMap<String, ? extends Throwable> map) { var tail = map.subMap("b", "c"); }
void m(TreeSet<String> set) { var reverseOrder = set.comparator().reversed(); }
void m(List<? extends Number> list) { var unique = list.stream().distinct().sorted(); }
void m(List<? extends Throwable> stream) { var best = stream.min(Comparator.comparing(e -> e.getStackTrace().length)); }
void m(Function<? super String, File> f1, Function<? super File, Integer> f2) { var f = f1.andThen(f2); }
void m(Predicate<? super File> discard) { var keep = discard.negate(); }

== Case 4: method with inferred type parameter in return type ==

A method (or constructor) whose return type includes an inferred type parameter may end up substituting capture variables or other non-denotable types.  This typically depends on the types of the arguments, again with a wildcard-parameterized type showing up somewhere.

void m(Enumeration<? extends Runnable> tasks) { var list = Collections.list(tasks); }
void m(Set<?> set) { var syncSet = Collections.synchronizedSet(set); }
void m(Function<? super String, ? extends Throwable> f) { var es = Stream.of("a", "b", "c").map(f); }

There are also cases here that are specified to produce capture vars but do not in javac:

void m(List<? extends Number> ns) { var firstSet = Collections.singleton(ns.get(0)); }

----------------

With that in mind, looking at our three options for dealing with capture variables:
1) Allow the non-denotable type
2) Map the type to a supertype that is denotable
3) Report an error

(3) isn't viable. "You can't use 'var' with 'getClass'" is already pretty bad. Prohibiting all the uses above would be really bad.

We've thought a lot about (1) and (2). The JEP includes this example:

void test(List<?> l1, List<?> l2) {
    var l3 = l1; // List<CAP> or List<?>?
    l3 = l2; // error?
    l3.add(l3.get(0)); // error?
}

On 'l3 = l2': I wouldn't say it's an important priority that all 'var' variables have a type that is convenient for future mutation. But we do expect users do be able to easily see *why* an assignment wouldn't be allowed. Unfortunately, capture variables are such a subtle thing that they're often invisible, and programmers don't even realize that they appear as an intermediate step. So, most people would see 'var l3 = l1' and expect that the type of l3 is List<?>.

On 'l3.add(l3.get(0))': This is a cool trick. The use of 'var' essentially serves the same purpose as invoking a generic method in order to give a capture variable a name:

<T> dupFirst(List<T> list) { list.add(list.get(0)); }
...
dupFirst(l1);

On the other hand, it's a subtle trick, and the average user isn't going to understand what's going on. (Or, more likely: 'l3.add(l3.get(0))' looks fine to them, but they won't understand why it stops working when that gets refactored to 'l1.add(l1.get(0))'.)

So, in terms of user experience, it seems like (2) is the desired outcome here.

That choice isn't without some sacrifice: it would be a nice property if lifting a subexpression out of an expression into its own 'var' declaration yields identical types. Since (2) changes the intermediate type, that doesn't hold. That said, hopefully our mapping function is reasonably unobtrusive...

How do we define the mapping? "Use the bound" is the easy answer, although in practice it's more complicated than that:
- Which bound? (upper or lower?)
- What if the bound contains the capture var?
- What do you do with a capture variable appearing as a (invariant) type argument?
- What do you do with a capture variable appearing as a wildcard bound?

We're working on finalizing the details. While this operation isn't trivial, it turns out it's pretty important: we already need it to solve bugs in the type system involving type inference [1] and lambda expressions [2]. It's a useful general-purpose tool.

—Dan

[1] https://bugs.openjdk.java.net/browse/JDK-8016196
[2] https://bugs.openjdk.java.net/browse/JDK-8170887


More information about the amber-spec-experts mailing list