简体   繁体   中英

Can Java infer type arguments from type parameter bounds?

The following test program is derived from a more complicated program that does something useful. It compiles successfully with the Eclipse compiler.

import java.util.ArrayList;
import java.util.List;

public class InferenceTest
{
    public static void main(String[] args)
    {
        final List<Class<? extends Foo<?, ?>>> classes =
            new ArrayList<Class<? extends Foo<?, ?>>>();
        classes.add(Bar.class);
        System.out.println(makeOne(classes));
    }

    private static Foo<?, ?> makeOne(Iterable<Class<? extends Foo<?, ?>>> classes)
    {
        for (final Class<? extends Foo<?, ?>> cls : classes)
        {
            final Foo<?, ?> foo = make(cls); // javac error here
            if (foo != null)
                return foo;
        }
        return null;
    }

    // helper used to capture wildcards as type variables
    private static <A, B, C extends Foo<A, B>> Foo<A, B> make(Class<C> cls)
    {
        // assume that a real program actually references A and B
        try
        {
            return cls.getConstructor().newInstance();
        }
        catch (final Exception e)
        {
            return null;
        }
    }

    public static interface Foo<A, B> {}

    public static class Bar implements Foo<Integer, Long> {}
}

However, with the Oracle JDK 1.7 javac, it fails with this:

InferenceTest.java:18: error: invalid inferred types for A,B; inferred type does not
 conform to declared bound(s)
            final Foo<?, ?> foo = make(cls);
                                      ^
    inferred: CAP#1
    bound(s): Foo<CAP#2,CAP#3>
  where A,B,C are type-variables:
    A extends Object declared in method <A,B,C>make(Class<C>)
    B extends Object declared in method <A,B,C>make(Class<C>)
    C extends Foo<A,B> declared in method <A,B,C>make(Class<C>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends Foo<?,?> from capture of ? extends Foo<?,?>
    CAP#2 extends Object from capture of ?
    CAP#3 extends Object from capture of ?
1 error

Which compiler is right?

One suspicious aspect of the output above is CAP#1 extends Foo<?,?> . I would expect the type variable bounds to be CAP#1 extends Foo<CAP#2,CAP#3> . If this were the case, then the inferred bound of CAP#1 would conform to the declared bounds. However, this might be a red herring, because C should indeed be inferred to be CAP#1 , but the error message is regarding A and B.


Note that if I replace line 26 with the following, both compilers accept the program:

private static <C extends Foo<?, ?>> Foo<?, ?> make(Class<C> cls)

However, now I can't reference the captured types of the Foo parameters.

Update: Similarly accepted by both compilers (but also useless) is this:

private static <A, B, C extends Foo<? extends A, ? extends B>>
    Foo<? extends A, ? extends B> make(Class<C> cls)

It essentially causes A and B to be trivially inferred as Object , and therefore obviously not useful in any context. It does, however, lend credence to my theory below that javac will only perform inference on wildcard bounds, not capture bounds. If no one has any better ideas, this might be the (unfortunate) answer. (End Update)


I realize this whole question is likely TL;DR, but I'll continue in case someone else is hitting this issue...

Based on JLS 7, §15.12.2.7 Inferring Type Arguments Based on Actual Arguments , I've done the following analysis:

Given a constraint of the form A << F , A = F , or A >> F :

Initially, we have one constraint of the form A << F , which indicates that type A is convertible to type F by method invocation conversion ( §5.3 ). Here, A is Class<CAP#1 extends Foo<CAP#2, CAP#3>> and F is Class<C extends Foo<A, B>> . Note that the other constraint forms ( A = F and A >> F ) only arise as the inference algorithm recurses.

Next, C should be inferred to be CAP#1 by the following rules:

(2.) Otherwise, if the constraint has the form A << F :

  • If F has the form G<..., Yk-1, U, Yk+1, ...> , where U is a type expression that involves Tj , then if A has a supertype of the form G<..., Xk-1, V, Xk+1, ...> where V is a type expression, this algorithm is applied recursively to the constraint V = U .

Here, G is Class , U and Tj are C , and V is CAP#1 . Recursive application to CAP#1 = C should result in the constraint C = CAP#1 :

(3.) Otherwise, if the constraint has the form A = F :

  • If F = Tj , then the constraint Tj = A is implied.

Up to this point, the analysis seems to agree with the javac output. Perhaps the point of divergence is whether to continue attempting to infer A and B . For example, given this rule

  • If F has the form G<..., Yk-1, ? extends U, Yk+1, ...> G<..., Yk-1, ? extends U, Yk+1, ...> , where U involves Tj , then if A has a supertype that is one of:
    • G<..., Xk-1, V, Xk+1, ...> , where V is a type expression.
    • G<..., Xk-1, ? extends V, Xk+1, ...> G<..., Xk-1, ? extends V, Xk+1, ...> .

Then this algorithm is applied recursively to the constraint V << U .

If CAP#1 is considered to be a wildcard (which it is a capture of), then this rule applies, and inference continues recursively with U as Foo<A, B> and V as Foo<CAP#2, CAP#3> . As above, this would yield A = CAP#2 and B = CAP#3 .

However, if CAP#1 is simply a type variable, then none of the rules seem to consider its bounds. Perhaps this concession at the end of the section in the spec refers to such cases:

The type inference algorithm should be viewed as a heuristic, designed to perform well in practice. If it fails to infer the desired result, explicit type parameters may be used instead.

Obviously, wildcards cannot be used as explicit type parameters. :-(

The problem is that you start from the following inference constraint:

class<#1>, #1 <: Foo<?, ?>

Which gives you a solution for C, namely C = #1.

Then you need to check whether C conforms to declared bounds - the bound of C is Foo, so you end up with this check:

#1 <: Foo<A,B>

which can be rewritten as

Bound(#1) <: Foo<A, B>

hence:

Foo<?, ?> <: Foo<A, B>

Now, here the compiler does a capture conversion of the LHS (here's where #2 and #3 are generated):

Foo<#2, #3> <: Foo<A, B>

Which means

A = #2

B = #3

So, we have that our solution is { A = #2, B = #3, C = #1 }.

Is that a valid solution? In order to answer that question we need to check whether the inferred types are compatible with the inference variable bounds, after type substitution, so:

[A:=#2]A <: Object
#2 <: Object - ok

[B:=#3]B <: Object
#3 <: Object - ok

[C:=#1]C <: [A:=#2, B:=#3]Foo<A, B>
#1 <: Foo<#2, #3>
Foo<?, ?> <: Foo<#2, #3>
Foo<#4, #5> <: Foo<#2, #3> - not ok

Hence the error.

The spec is underspecified when it comes to the interplay between inference and captured types, so it's quite normal (but not good!) to have different behaviors when switching between different compilers. However some of these issues are being worked on, both from a compiler perspective and from a JLS perspective, so issues like this should get fixed in the medium term.

Two things I've noticed:

  1. CAP#1 is not a wildcard, it is a type variable because of capture conversion .

  2. On the first step, the JLS mentions that U is the type expression while Tj is the type parameter . The JLS does not explicitly define what a type expression is, but my gut feeling is that it includes the bounds of the type parameter. If that is the case, U would be C extends Foo<A,B> and V would be CAP#1 extends Foo<CAP#2, CAP#3> . Following the type inference algorithm:

V = U -> C = CAP#1 AND Foo<CAP#2, CAP#3> = Foo<A, B>

You can continue to apply the type inference algorithm to the above, and you will end up with A= CAP#2 and B=CAP#3 .

I believe you have spotted a bug with Oracle's compiler

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM