Generics type inference problem?

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Mon Jun 12 14:25:27 UTC 2017


Hi Attila,
the change in behavior you are observing is expected and part of the fix 
for:

https://bugs.openjdk.java.net/browse/JDK-8039214

Let me try to show you what the difference is between 8 and 9 - and let 
me doing so by rewriting your example to use unique type-variable names:

class NewClass {
         private static <T extends Throwable> List<TestFactory<T>> test(
                 Collection<? extends Supplier<? extends T>> factory) {
             return factory.stream()
                     .map(NewClass::testMap)
                     .collect(Collectors.toList());
         }

         private static <E extends Throwable> TestFactory<E> 
testMap(Supplier<? extends E> factory) {
             return null;
         }

         private static interface TestFactory<E extends Throwable> {
         }
     }

So, the variable 'factory' has this static type:

Collection<? extends Supplier<? extends T>>

If we want to access its members (e.g. call stream() ), we need to 
capture the type first, and obtain:

Collection<#CAP1>,

where #CAP1 is a fresh type-variable such that #CAP1 <: Supplier<? 
extends T>

Now, the instantiated type of 'factory.stream().map()' is something like:

<R> R map(Function<? super #CAP1, ? extends R>)

This means that when checking the method reference expression:

NewClass::testMap

We will be using #CAP1 as the argument type (see JLS 15.13.2). In other 
words, we will run a method lookup for a method name 'testMap' inside 
class 'NewClass' with argument types { #CAP1 }. This triggers an 
applicability check against the only method found (NewClass.testMap); as 
part of the applicability check we need to check that the actual 
argument type is compatible with the formal - so:

#CAP1 <: Supplier<? extends E>

To do this subtyping we need to rewrite the left hand side so that it 
looks like a 'Supplier' - we do so by recursing on the upper bound:

Supplier<? extends T> <: Supplier<? extends E>

And, to do this subtyping, we need to capture the left hand side (at 
least that's how javac does it) - as recursion into supertypes can 
sometimes require capture conversion. So:

Supplier<#CAP2> <: Supplier<? extends E>, where #CAP2 <: T

which is satisfied if:

#CAP2 <= E

(where '<=' means 'contained by').

Now, in JDK 8's javac, this containment relation was rewritten as follows:

T <: E (that is, we recursed on the upper bound of the left hand side.

This led to an inference constraint which ultimately led to E and T 
being solved in the same way, and the program compiled without errors.

This behavior is flexible but, ultimately, unsound - JDK-8039214 fixed 
that, so that in 9 the type containment is rewritten as:

#CAP2 <: E (no recursion on the left hand side).

This then runs into trouble because when we need to solve the inference 
variable E, we have two incompatible constraints:

E = T (from target type context of the collect() call)

and

E = #CAP2 (from above)

hence the error.


We worked on some potential mitigation for these issues, but ultimately 
they have been considered too risky for targeting JDK 9 - you can follow 
the issue here:

https://bugs.openjdk.java.net/browse/JDK-8160244

Thanks!

Maurizio


On 11/06/17 17:16, Attila Kelemen wrote:
> Hi,
>
> I'm not sure if my issue is a violation of the specs or a bug, so I'm 
> writing here instead of a bug report. The following class does not 
> compile (imports are omitted) in Java 9 but does in Java 8:
>
>     public final class NewClass {
>         private static <E extends Throwable> List<TestFactory<E>> test(
>                 Collection<? extends Supplier<? extends E>> factory) {
>             return factory.stream()
>                     .map(NewClass::testMap)
>                     .collect(Collectors.toList());
>         }
>
>         private static <E extends Throwable> TestFactory<E> 
> testMap(Supplier<? extends E> factory) {
>             return null;
>         }
>
>         private static interface TestFactory<E extends Throwable> {
>         }
>     }
>
> The above `test` method does not compile because type inference fails 
> to deduce the "appropriate" type for the return value of 
> `Stream.collect`. Which is strange given that `Stream.map` should map 
> to `Stream<TestFactory<E>>`. In fact, if I rewrite the method `test` to:
>
>         Stream<TestFactory<E>> factoryStream = 
> factory.stream().map(NewClass::testMap);
>         return factoryStream.collect(Collectors.toList());
>
> The code will compile fine.
>
> Is this behaviour in accordance with the spec or is this a bug?
>
> The exact versions I have tried this with:
>
> Java 8: java version "1.8.0_92", build 1.8.0_92-b14
> Java 9: java version "9-ea", build 9-ea+170
>
> Thanks,
> Attila Kelemen



More information about the compiler-dev mailing list