20

下面的测试程序源自一个更复杂的程序,它做了一些有用的事情。它使用 Eclipse 编译器成功编译。

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> {}
}

但是,使用 Oracle JDK 1.7 javac,它会失败:

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

哪个编译器是对的?

上述输出的一个可疑方面是CAP#1 extends Foo<?,?>. 我希望类型变量边界是CAP#1 extends Foo<CAP#2,CAP#3>. 如果是这种情况,那么推断的边界CAP#1将符合声明的边界。然而,这可能是一个红鲱鱼,因为 C 确实应该被推断为CAP#1,但错误消息是关于 A 和 B 的。


请注意,如果我将第 26 行替换为以下内容,则两个编译器都接受该程序:

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

但是,现在我无法引用捕获的Foo参数类型。

更新:同样被两个编译器接受(但也无用)是这样的:

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

它本质上导致A并被B简单地推断为Object,因此显然在任何情况下都没有用。然而,它确实证明了我下面的理论javac只会对通配符边界进行推断,而不是捕获边界。如果没有人有更好的想法,这可能是(不幸的)答案。(结束更新)


我意识到这整个问题很可能是 TL;DR,但我会​​继续,以防其他人遇到这个问题......

基于 JLS 7, §15.12.2.7 Inferring Type Arguments Based on Actual Arguments,我做了以下分析:

给定形式A << F,A = F或的约束A >> F

最初,我们有一个 form 约束A << F,它表示 type可以通过方法调用转换A为 type (第 5.3 节)。在这里,是和是。请注意,其他约束形式 (和) 仅在推理算法递归时出现。FAClass<CAP#1 extends Foo<CAP#2, CAP#3>>FClass<C extends Foo<A, B>>A = FA >> F

接下来,C应由CAP#1以下规则推断:

(2.) 否则,如果约束具有以下形式A << F

  • 如果F具有形式G<..., Yk-1, U, Yk+1, ...>, whereU是一个涉及 的类型表达式Tj,则如果A具有形式G<..., Xk-1, V, Xk+1, ...> whereV是类型表达式的超类型,则该算法递归地应用于约束V = U

在这里,GClassU并且TjC,并且VCAP#1。递归应用CAP#1 = C应该导致约束C = CAP#1

(3.) 否则,如果约束具有以下形式A = F

  • 如果,则隐含F = Tj约束。Tj = A

到目前为止,分析似乎与 javac 输出一致。也许分歧点在于是否继续尝试推断AB。例如,给定这条规则

  • 如果F具有形式G<..., Yk-1, ? extends U, Yk+1, ...>,其中U涉及Tj,则 ifA具有以下超类型之一:
    • G<..., Xk-1, V, Xk+1, ...>,其中V是类型表达式。
    • G<..., Xk-1, ? extends V, Xk+1, ...>.

然后将该算法递归地应用于约束V << U

如果CAP#1被认为是通配符(它它的捕获),则适用此规则,并且使用UasFoo<A, B>Vas递归地继续推理Foo<CAP#2, CAP#3>。如上所述,这将产生A = CAP#2B = CAP#3

但是,如果CAP#1只是一个类型变量,那么似乎没有任何规则考虑它的界限。也许规范部分末尾的这个让步是指这样的情况:

类型推断算法应被视为一种启发式算法,旨在在实践中表现良好。如果它无法推断出所需的结果,则可以使用显式类型参数来代替。

显然,通配符不能用作显式类型参数。:-(

4

2 回答 2

10

问题是您从以下推理约束开始:

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

这为您提供了 C 的解决方案,即 C = #1。

然后你需要检查 C 是否符合声明的边界——C 的边界是 Foo,所以你最终得到了这个检查:

#1 <: Foo<A,B>

可以重写为

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

因此:

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

现在,编译器在这里对 LHS 进行捕获转换(这里是生成 #2 和 #3 的位置):

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

意思是

A = #2

B = #3

所以,我们的解决方案是{ A = #2, B = #3, C = #1 }。

这是一个有效的解决方案吗?为了回答这个问题,我们需要在类型替换之后检查推断的类型是否与推断变量边界兼容,因此:

[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

因此错误。

当涉及到推理和捕获类型之间的相互作用时,规范没有明确说明,因此在不同编译器之间切换时有不同的行为是很正常的(但不好!)。然而,从编译器的角度和从 JLS 的角度来看,其中一些问题正在得到解决,因此这样的问题应该在中期得到解决。

于 2013-07-03T18:22:49.240 回答
1

我注意到两件事:

  1. CAP#1不是通配符,它​​是一个类型变量,因为捕获转换

  2. 在第一步中,JLS 提到这U是类型表达式Tj而是类型参数。JLS 没有明确定义什么是类型表达式,但我的直觉是它包含了类型参数的边界。如果是这样,U将是C extends Foo<A,B>并且V将是CAP#1 extends Foo<CAP#2, CAP#3>。遵循类型推断算法:

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

您可以继续将类型推断算法应用于上述内容,您将得到A= CAP#2and B=CAP#3

我相信您已经发现了 Oracle 编译器的一个错误

于 2013-07-03T02:48:03.567 回答