【问题标题】:Generics compilation error with ternary operator in Java 8, but not in Java 7在 Java 8 中使用三元运算符的泛型编译错误,但在 Java 7 中没有
【发布时间】:2014-03-19 14:46:02
【问题描述】:

这个类在 Java 7 中可以编译,但在 Java 8 中不行:

public class Foo {

    public static void main(String[] args) throws Exception {
        //compiles fine in Java 7 and Java 8:
        Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;
        CharSequence foo = foo(aClass);

        //Inlining the variable, compiles in Java 7, but not in Java 8:
        CharSequence foo2 = foo(true ? String.class : StringBuilder.class);

    }

    static <T> T foo(Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }
}

编译错误:

错误:(9, 29) java: 类 Foo 中的方法 foo 不能应用于给定 类型;必需:java.lang.Class 找到:真? Str[...]类
原因:推断类型不符合等式约束 推断:java.lang.StringBuilder 等式约束:java.lang.StringBuilder,java.lang.String

为什么这在 Java 8 中停止工作了?它是故意的/其他功能的副作用,还是仅仅是编译器错误?

【问题讨论】:

  • 这是因为它改进了类型推断。
  • @berry120 你认为这是一种改进吗? :)
  • 通过这个例子,我明白了你试图更清楚地表达的观点 - 我同意,在我看来,三元运算符并没有以应有的方式推断类型。
  • @AleksanderBlomskøld 答案有了一些进展。您可能想要查看。
  • 您使用的是什么 IDE?这在带有 Java 8 的新 Eclipse 上运行良好。

标签: java generics java-8


【解决方案1】:

我要冒昧地说这个错误(虽然它可能符合也可能不符合更新的 JLS,我承认我没有详细阅读过)是由于类型处理的不一致造成的通过 JDK 8 编译器。

通常,三元运算符使用相同的类型推断,就好像双参数方法的形式参数都基于相同的类型参数一样。例如:

static <T> T foo(Class<? extends T> clazz, Class<? extends T> clazz2) { return null; }

public static void main(String[] args) {
    CharSequence foo2 = foo(String.class, StringBuilder.class);
}

在此示例中,T 可以推断为对? extends Object &amp; Serializable &amp; CharSequence 的捕获。现在类似地,在 JDK 7 中,如果我们回到您原来的示例:

CharSequence foo2 = foo(true ? String.class : StringBuilder.class);

这与上面几乎完全相同的类型推断,但在这种情况下,将三元运算符视为一种方法:

static <T> T ternary(boolean cond, T a, T b) {
    if (cond) return a;
    else return b;
}

所以在这种情况下,如果将 String.class 和 StringBuilder.class 作为参数传递,则推断出的 T 的类型(粗略地说)是Class&lt;? extends Object &amp; Serializable &amp; CharSequence&gt;,这就是我们想要的。

其实你可以用这个方法替换原来sn-p中三元运算符的使用,因此:

public class Foo {

    public static void main(String[] args) throws Exception {
        //compiles fine in Java 7 and Java 8:
        Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;
        CharSequence foo = foo(aClass);

        //Inlining the variable using 'ternary' method:
        CharSequence foo2 = foo(ternary(true, String.class, StringBuilder.class));

    }

    static <T> T foo(Class<T> clazz) throws Exception {
        return clazz.newInstance();
    }

    static <T> T ternary(boolean cond, T a, T b) {
        if (cond) return a;
        else return b;
    }
}

...现在它在 Java 7 和 8 中编译(编辑:实际上它在 Java 8 中也失败了! 再次编辑:它现在可以工作了,Jdk 8u20) .是什么赋予了?由于某种原因,现在对 T (在 foo 方法中)施加了等式约束,而不是下限约束。 JLS for Java 7 的相关部分是15.12.2.7;对于 Java 8,有一个关于类型推断的全新章节 (chapter 18)。

请注意,在对“三元”的调用中显式键入 T 确实允许使用 Java 7 和 8 进行编译,但这似乎不是必需的。 Java 7 做了正确的事,即使存在可以为 T 推断出的适当类型,Java 8 也会给出错误。

【讨论】:

  • 我写了同样的答案。我有点同意你的看法。
  • 咳咳,我只用 Netbeans 7.4 和 RC 的 javac 进行了尝试,它为 ternary(…,…,…) 方法产生了与三元运算符 …?…:… 完全相同的错误消息。
  • @Holger,你是对的,我的错。我已经相应地编辑了答案。
  • @davmac IMO 这个答案不正确,请在这个帖子中查看我的答案
  • @vrz 查看我对您的回答的评论。
【解决方案2】:

根据当前规范,这不是 javac 错误。我在这里写了一个答案,对于类似的issue。这里的问题或多或少是相同的。

在赋值或调用上下文引用条件表达式是多表达式。这意味着表达式的类型不是对 lub(T1, T2) 应用捕获转换的结果,有关 T1 和 T2 的详细定义,请参阅JSL-15.25.3。相反,我们也从规范的这一部分得到:

多引用条件表达式出现在特定上下文中的位置 kind 具有目标类型 T,它的第二个和第三个操作数表达式类似地出现 在与目标类型 T 相同的上下文中。

多引用条件表达式的类型与其目标类型相同。

所以这意味着目标类型被下推到引用条件表达式的两个操作数,并且这两个操作数都归于该目标类型。因此编译器最终会从两个操作数中收集约束,从而导致无法解决的约束集,从而导致错误。


好的,但是为什么我们在这里得到 T 的等式界限呢?

让我们详细看看,来自调用:

foo(true ? String.class : StringBuilder.class)

foo 在哪里:

static <T> T foo(Class<T> clazz) throws Exception {
    return clazz.newInstance();
}

当我们使用表达式true ? String.class : StringBuilder.class 调用方法foo() 时,我们就有了。此引用条件表达式应在类型为Class&lt;T&gt; 的松散调用上下文中兼容。这表示为,见JLS-18.1.2:

true ? String.class : StringBuilder.class → Class<T>

JLS-18.2.1 得到如下:

将‹Expression → T›形式的约束公式简化如下:

...

  • 如果表达式是 e1 形式的条件表达式? e2 : e3, 的 约束简化为两个约束公式,‹e2 → T›和‹e3 → T›。

这意味着我们得到以下约束公式:

String.class → Class<T>
StringBuilder.class → Class<T>

或:

Class<String> → Class<T>
Class<StringBuilder> → Class<T>

后来从JLS-18.2.2 我们得到了:

‹S → T›形式的约束公式简化如下:

...

  • 否则,约束减少为‹S <: t>

我只包括相关部分。所以我们现在有:

Class<String> <: Class<T>
Class<StringBuilder> <: Class<T>

来自JLS-18.2.3,我们有:

‹S <:t>

...

  • 否则按照T的形式减少约束:
    • 如果 T 是参数化类或接口类型,或者是一个内部类类型 参数化类或接口类型(直接或间接),令 A1, ..., An 为 T 的类型参数。在 S 的超类型中,对应的类 或接口类型被识别,类型参数 B1,...,Bn。如果没有这种类型 存在,约束减少为假。否则,约束减少到 以下新约束:对于所有 i (1 ≤ i ≤ n),‹Bi

所以Class&lt;T&gt;Class&lt;String&gt;Class&lt;StringBuilder&gt; 是参数化类,这意味着现在我们的约束减少为:

String <= T
StringBuilder <= T

同样来自JLS-18.2.3,我们有:

‹S

...

  • 如果 T 是一个类型:
    • 如果 S 是一个类型,则约束简化为‹S = T›。

因此我们最终得到了 T 的这些约束:

String = T
StringBuilder = T

终于在JLS-18.2.4 我们得到了:

‹S = T›形式的约束公式,其中 S 和 T 是类型,可简化为 如下:

...

  • 否则,如果 T 是推理变量 α,则约束减少到界限 S = α。

对于边界为T = StringT = StringBuilder 的类型变量T 没有解决方案。没有编译器可以用 T 代替满足这两个限制的类型。因此,编译器会显示错误消息。


所以根据当前规范,javac 是可以的,但是规范是否正确?那么应该调查7和8之间的兼容性问题。为此,我已提交JDK-8044053,以便我们跟踪此问题。

【讨论】:

  • 这可能值得仔细研究。您的建议是 JLS 在 Java 7 中的实现方式与 Java 8 不同。我没有时间翻阅版本之间的变更日志,但它可能 考虑到您的解释,似乎就是这种情况。
  • 尽管您已经密切关注 JLS,并且可能已经确定了出现错误的原因(我现在没有时间仔细检查),但我不明白为什么 JLS- 18.2.4 要求是必要的。其他地方也出现了类似的问题,请参阅bugs.openjdk.java.net/browse/JDK-8043980 - 在这里,它似乎已被识别为编译器错误。 JLS 可能是错误的——这也不是第一次发生这种情况;例如,请参阅stackoverflow.com/questions/5385743/…
  • @Makoto,不是我说的是,鉴于规范 javac 的当前状态给出了正确的答案。我已经编辑了我的答案以强调这一点。我还添加了对我刚刚提交的规范错误的引用,以调查此问题并检查是否存在规范错误。
  • @davmac JDK-8043980 无法复制,因此已关闭。我稍后会检查您所指的 SO 中的问题。我的回答只是想证明根据当前的规范 javac 是正确的。此处是否存在规范错误必须进行分析,但不是我们只能推测。
  • @vrz 请阅读 cmets:“无法在最新的 langtools 存储库中重现。似乎已经解决了这个问题。” - 它是可重现的,只是不能使用最新的(repo)javac。但是,它可能指的是不同的问题。我有兴趣看到对您提交的错误的响应。谢谢
【解决方案3】:

问题仅出现在参数和赋值的上下文中。即

CharSequence cs1=(true? String.class: StringBuilder.class).newInstance();

有效。与其他答案声称不同,使用通用 &lt;T&gt; T ternary(boolean cond, T a, T b) 方法不起作用。当调用被传递给像&lt;T&gt; T foo(Class&lt;T&gt; clazz) 这样的泛型方法以便搜索&lt;T&gt; 的实际类型时,这仍然失败。但是它在分配示例中有效

Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;

因为结果类型已经明确指定。当然,转换为 Class&lt;? extends CharSequence&gt; 的类型也总是可以解决其他用例的问题。

问题在于该算法被指定为首先找到第二个和第三个操作数的“最小上限”,然后再应用“捕获转换”。对于Class&lt;String&gt;Class&lt;StringBuilder&gt; 这两种类型,第一步已经失败,因此甚至没有尝试考虑上下文的​​第二步。

这不是一个令人满意的情况,但是,在这种特殊情况下,有一个优雅的替代方案:

public static void main(String... arg) {
  CharSequence cs=foo(true? String::new: StringBuilder::new);
}

static <T> T foo(Supplier<T> s) { return s.get(); }

【讨论】:

  • "问题在于算法被指定为首先找到第二个和第三个操作数的“最小上界”,然后应用“捕获转换”。对于这两种类型,第一步已经失败了 Class 和 Class" - 但是 为什么 它会失败?最小上限应该是Class&lt;? extends Object &amp; CharSequence &amp; Serializable&gt;
  • @davmac:我没有说它是正确的,我只是把它确定到这一点,但像你一样,我无法在新规范中识别“身份约束”的相关部分(然而)。也许当我有更多时间时我会完成它,但我想告诉我到目前为止我发现了什么。但在我看来,一个如此难以阅读的规范必须在实现中产生问题……
  • @Holger,很好的观察。如果引用条件表达式未出现在赋值或调用上下文中,则将其视为独立表达式。因为它不是一个 poly 表达式,所以 javac 8 机器没有应用于它,它的类型等同于 javac 7 获得的类型。
猜你喜欢
  • 2014-06-16
  • 2012-08-25
  • 1970-01-01
  • 2021-07-07
  • 2012-04-21
  • 2017-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多