【问题标题】:Can Java infer type arguments from type parameter bounds?Java 可以从类型参数边界推断类型参数吗?
【发布时间】:2013-07-04 02:22:49
【问题描述】:

下面的测试程序源自一个更复杂的程序,它做了一些有用的事情。用 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&lt;?,?&gt;。我希望类型变量边界为CAP#1 extends Foo&lt;CAP#2,CAP#3&gt;。如果是这种情况,那么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)

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


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

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

给定A &lt;&lt; FA = FA &gt;&gt; F 形式的约束:

最初,我们有一个A &lt;&lt; F 形式的约束,它表示A 类型可以通过方法调用转换(§5.3) 转换为F 类型。这里,AClass&lt;CAP#1 extends Foo&lt;CAP#2, CAP#3&gt;&gt;FClass&lt;C extends Foo&lt;A, B&gt;&gt;。请注意,其他约束形式(A = FA &gt;&gt; F)仅在推理算法递归时出现。

接下来,C 应通过以下规则推断为 CAP#1

(2.) 否则,如果约束具有A &lt;&lt; F 的形式:

  • 如果F 具有G&lt;..., Yk-1, U, Yk+1, ...&gt; 的形式, 其中U是一个涉及Tj的类型表达式, 那么如果A 具有G&lt;..., Xk-1, V, Xk+1, ...&gt; 形式的超类型 其中V 是一个类型表达式, 该算法递归地应用于约束V = U

这里,GClassUTjCVCAP#1。对CAP#1 = C 的递归应用应该导致约束C = CAP#1

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

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

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

  • 如果F 具有G&lt;..., Yk-1, ? extends U, Yk+1, ...&gt; 的形式, 其中U 涉及Tj,那么如果A 的超类型是以下之一:
    • G&lt;..., Xk-1, V, Xk+1, ...&gt;,其中V 是一个类型表达式。
    • G&lt;..., Xk-1, ? extends V, Xk+1, ...&gt;

然后这个算法递归地应用于约束V &lt;&lt; U

如果CAP#1 被认为是通配符(它的捕获),则适用此规则,并以U 作为Foo&lt;A, B&gt;V 继续递归如Foo&lt;CAP#2, CAP#3&gt;。如上所述,这将产生A = CAP#2B = CAP#3

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

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

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

【问题讨论】:

  • 您使用的是哪个版本的javacjavac -version
  • @Jeffrey:比 1.7 更具体? javac 1.7.0_25
  • @BevynQ:是的,我的问题是那个示例“解决方法”。但是,它没有用,因为make 的目的是捕获通配符,例如用于在进一步的方法调用中表示相同类型的多个实例或使用这些类型参数创建其他对象实例。该问题与将结果分配给Foo&lt;?,?&gt; 无关;该部分适用于任一编译器。
  • FWIW,IntelliJ 也成功编译了您的示例。
  • @BevynQ:感谢您提供帮助。我只是澄清一下a)您提到的解决方法是逐字逐句地在原始问题中,所以我不明白它添加了什么信息,并且b)编译器错误出现在方法调用中,而不是它拒绝分配结果。如果您删除所有提及foo(即没有结果变量、没有赋值、没有if、没有return),错误仍然会发生。

标签: java generics


【解决方案1】:

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

class&lt;#1&gt;, #1 &lt;: Foo&lt;?, ?&gt;

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

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

#1 &lt;: Foo&lt;A,B&gt;

可以改写为

Bound(#1) &lt;: Foo&lt;A, B&gt;

因此:

Foo&lt;?, ?&gt; &lt;: Foo&lt;A, B&gt;

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

Foo&lt;#2, #3&gt; &lt;: Foo&lt;A, B&gt;

这意味着

A = #2

B = #3

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

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

[A:=#2]A &lt;: Object
#2 &lt;: Object - ok

[B:=#3]B &lt;: Object
#3 &lt;: Object - ok

[C:=#1]C &lt;: [A:=#2, B:=#3]Foo&lt;A, B&gt;
#1 &lt;: Foo&lt;#2, #3&gt;
Foo&lt;?, ?&gt; &lt;: Foo&lt;#2, #3&gt;
Foo&lt;#4, #5&gt; &lt;: Foo&lt;#2, #3&gt; - not ok

因此错误。

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

【讨论】:

  • 感谢您的快速回复!为什么Foo&lt;?, ?&gt; 会重新引入新的捕获?如果编译器在推理阶段的检查阶段执行单独的捕获转换,那么您似乎永远无法使用通配符获得可满足的(非平凡的)推理约束。 IOW、#2 和#4 对应同一个通配符,但#4 &lt;: #2 永远不会为真。 JLS 似乎不需要这个(事实上,在该部分中根本没有提到捕获),它破坏了合理的代码而没有解决方法,所以这是一个 javac 错误,对吧?
  • 这是一个规范错误——规范首先要求新的捕获;我的观点是,根据推理后检查的执行方式,某些编译器可能会受到这种行为的影响。 Javac 肯定是。但是您所说的是 100% 正确:在子类型化期间生成捕获的变量作为推理过程的一部分会导致无法满足的约束——这就是为什么要在该领域重新制定规范的原因。
  • 谢谢,很高兴听到这个消息。您知道规范工作是针对 Java 8、9 还是更高版本?目前,我只是使用原始转换作为解决方法,因为我从 Eclipse 确信逻辑是正确的。
【解决方案2】:

我注意到了两件事:

  1. CAP#1 不是通配符,它​​是一个类型变量,因为capture conversion

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

V = U -> C = CAP#1 AND Foo&lt;CAP#2, CAP#3&gt; = Foo&lt;A, B&gt;

你可以继续将类型推断算法应用到上面,你最终会得到A= CAP#2B=CAP#3

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

【讨论】:

  • 同意,CAP#n 是由于捕获转换通配符而引入的类型变量。我在那里得到的是捕获转换是否完全发生在类型推断之前。花了一段时间才找到它,但答案似乎是肯定的:“如果表达式名称出现在它受 [...] 方法调用转换 [...] 的上下文中,那么表达式的类型name 是捕获转换后声明的字段、局部变量或参数的类型。§6.5.6.1
  • @Trevor 耶!也在寻找这一点(5年后)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多