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