【问题标题】:Why does `Class<T> == Boolean.class` cause a compiler error when `T extends Comparable<? super T>`?为什么`Class<T> == Boolean.class`在`T extends Comparable<时会导致编译器错误?超级T>`?
【发布时间】:2015-07-08 15:05:40
【问题描述】:

我使用泛型来抽象出 Comparable 数据类型,如下面提供的代码所示。此案例源于一个 Java Swing 组件,特别是尝试将表模型调整为使用泛型。

我有一个可行的解决方案来解决这个问题(下面的案例 3A)。但是,在获得该解决方案的过程中,当我将类签名从 T extends Comparable&lt;T&gt; 更改为 T extends Comparable&lt;? super T&gt; 时,我对编译器的行为感到困惑。

为什么编译器希望 Comparable 成为原始类型(下面的案例 3A)?我希望类签名能够工作(案例 6A,Class&lt;T extends Comparable&lt;? super T&gt;&gt;),但它需要强制转换并在使用 if (type == Boolean.class) 时导致编译器错误。

为什么情况 2B、3A/B 和 4A/B 允许if (type == Boolean.class),但情况 1A/B、2A、5A/B 和 6A/B 会导致编译器错误?具体来说,我想在 2A 与 2B 的情况下解释这个错误,它们共享相同的类签名。

可运行的测试用例如下。提供了外部ScoreDetailedScore 类来演示涉及继承的案例,突出了案例A 和案例B 之间的区别。

ComparableTest.java

public class ComparableTest<L, T extends Comparable<? super T>> // Case A
//public class ComparableTest<L, T extends Comparable<T>> // Case B: Works when used without inheritance.
{
    public static void main(String[] args)
    {
        new ComparableTest<String, Boolean>(new String("B"), Boolean.TRUE);
        new ComparableTest<String, Float>(new String("F"), new Float(1f));
        new ComparableTest<String, Score>(new String("S"), new Score(5f));
        new ComparableTest<String, Score>(new String("D"), new DetailedScore<String>("DS.S", 5f));
        new ComparableTest<String, DetailedScore<?>>(new String("DS"), new DetailedScore<String>("DS.DS", 5f));
    }

    public ComparableTest(L label, T value)
    {
        // Case 1A: Compiler Error: Type mismatch: cannot convert from Class<capture#2-of ? extends Comparable> to Class<T>
//        Class<T> type = value.getClass(); // Q: Why can't I use Class<T>?

        // Case 2A: Compiler Warning: Type safety: Unchecked cast from Class<capture#2-of ? extends Comparable> to Class<T>
//        Class<T> type = (Class<T>) value.getClass(); // Case 2B: This works if 'T extends Comparable<T>' (see note in class declaration above).

        // Case 3A: Compiler Warning: Comparable is a raw type. References to generic type Comparable<T> should be parameterized
        Class<? extends Comparable> type = value.getClass(); // Q: Why must Comparable be a raw type here?

        // Case 4A: Compiler Warning: Type safety: Unchecked cast from Class<capture#2-of ? extends Comparable> to Class<? extends Comparable<?>>
//        Class<? extends Comparable<?>> type = (Class<? extends Comparable<?>>) value.getClass();

        // Case 5A: Compiler Warning: Type safety: Unchecked cast from Class<capture#2-of ? extends Comparable> to Class<? extends Comparable<T>>
//        Class<? extends Comparable<T>> type = (Class<? extends Comparable<T>>) value.getClass();

        // Case 6A: Compiler Warning: Type safety: Unchecked cast from Class<capture#2-of ? extends Comparable> to Class<? extends Comparable<? super T>>
//        Class<? extends Comparable<? super T>> type = (Class<? extends Comparable<? super T>>) value.getClass();

        // Case 1A, 2A: Compiler Error: Incompatible operand types Class<T> and Class<Boolean>
        // Case 2B, 3A/B, 4A/B: OK.
        // Case 5A/B, 6A/B: Compiler Error: Incompatible operand types Class<capture#4-of ? extends Comparable<T>> and Class<Boolean>
        if (type == Boolean.class)
        {
            System.out.println("Treating " + label + " as boolean (" + type.getCanonicalName() + ")");
        } else if (type == Float.class) {
            System.out.println("Treating " + label + " as float (" + type.getCanonicalName() + ")");
        } else {
            System.out.println("Treating " + label + " as (" + type.getCanonicalName() + ")");
        }

        return;
    }
}

Score.java

public class Score implements Comparable<Score>
{
    private Float value;

    public Score(Float value)
    {
        this.value = value;
        return;
    }

    @Override
    public int compareTo(Score o)
    {
        return this.value.compareTo(o.value); // for brevity
    }
}

DetailedScore.java

public class DetailedScore<D> extends Score
{
    private D detail;

    public DetailedScore(D someDetail, Float value)
    {
        super(value);
        this.detail = someDetail;
        return;
    }

    public D getDetail()
    {
        return this.detail;
    }
}

【问题讨论】:

  • getClass() 返回 Class&lt;?&gt; 而不是 Class&lt;T&gt;... 但您可以放心地转换它。
  • @assylias 是的,示例中的案例 6A 转换为 Class&lt;? extends Comparable&lt;? super T&gt;&gt;;但是,在使用 == 运算符时,这仍然会导致编译器错误。
  • 哪个编译器版本?
  • @bayou.io Oracle JDK:Java(TM) SE Runtime Environment (build 1.7.0_55-b13)Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode) 在 Windows Server 2003 R2 Standard x64 上。

标签: java generics java-7 comparable


【解决方案1】:

编译器警告与错误不同...例如,您的“案例 2A”将编译...它会生成警告但会给您一个可用的类文件。这些通常是由于擦除,这是一个我不假装完全理解的复杂话题。

但是例如,当T extends Comparable&lt;? super T&gt; 时,如果你有value,它被声明为T,当然你应该能够将value.getClass() 转换为Class&lt;T&gt;。但是由于橡皮擦,编译器只记得value.getClass() 应该返回一个Class&lt;? extends Comparable&gt;,所以它警告你在转换为Class&lt;T&gt; 时要小心。如果您确定某些事情,您可以关闭警告,使用 @SuppressWarnings 注释,如下所示:

    @SuppressWarnings("unchecked")
    Class<T> type = (Class<T>)value.getClass();

一个好的IDE(比如Eclipse)会建议这个注释作为修复警告的可能性;如果您没有使用好的 IDE,我建议您开始。

所以让我们来谈谈实际的错误。在 Java 中,如果两个事物是相同类型或可分配给相同类型,则只能检查对象是否相等。例如,这有效:

public static class Foo {

}

public static class Bar extends Foo {

}

public static void main(String...args) {
    Foo f = new Foo();
    Bar b = new Bar();

    if (f == b) {

    }
}

因为,在比较时,f 可能是 Bar这是允许的。另一方面,这不起作用:

    Boolean b = Boolean.FALSE;
    Integer i = 5;

    if (b == i) {  // ERROR

    }

这两者不可能相等。这是一个错误。

从编译器的角度来看,Class&lt;T&gt;Class&lt;Boolean&gt;(这是Boolean.class 的类型,natch)是不同的类型。如果T extends Comparable&lt;? super T&gt;,它们不能相互分配,因为Boolean实现Comparable&lt;Boolean&gt;;它确实实现Comparable&lt;? super Boolean&gt;。所以你不能用==检查是否相等。

(如果T extends Comparable&lt;T&gt; 则与Boolean 兼容,您可以进行比较。)

所以不要使用==。改用if (type.equals(Boolean.class)) {...},编译器会很开心,你也会很开心。

【讨论】:

  • 有趣。 JDK 中的 Swing 类使用相同的 == 比较(参见 AbstractListModel.java 中的第 113 行)。我很确定这就是我使用这个运算符的原因。
  • 那行是if (listeners[i] == ListDataListener.class) {...} 并且listeners 被声明为Object[],这意味着listeners[i] 是一个对象......在这种情况下,ListDataListener.class 可以分配给Object (就像每个非原始的一样),所以比较是有效的。 listeners[i] 很可能是 ListDataListener 类。
【解决方案2】:

参见Object.getClass()的javadoc,value.getClass()的返回类型为Class&lt;? extends |T|&gt;,即Class&lt;? extends Comparable&gt;

表达式value.getClass() 的类型在进一步使用之前会经过通配符捕获;因此我们在消息中看到捕获的类型Class&lt;capture#2-of ? extends Comparable&gt;

根据 JLS#15.21.3,围绕type==Boolean.class 的问题取决于是否可以将一种类型转换为另一种类型。如果在编译时证明这是不可能的,则禁止在两种类型之间进行转换;在这种情况下,== 测试也被禁止。这是有道理的。

不过,JLS#5.5.1 可能有点草率。首先,让我们按照它的确切说法。 Class&lt;a1&gt; 可以转换为 Class&lt;a2&gt;,只有当类型参数 a1, a2 不能被证明是不同的。显然我们不希望将Class&lt;Boolean&gt; 转换为Class&lt;FLoat&gt;

对于您在Class&lt;T&gt;Class&lt;Boolean&gt; 之间的情况,根据JLS#4.5.1,由于Boolean&lt;:Comparable,它们无法证明是不同的。因此应该允许。您的编译器禁止它,但 javac8u45 允许它。

遵循相同的推理,type==Boolean.class 在所有情况下都应该被允许。这就是 IntelliJ 的行为。但是 javac8 没有;它禁止案例 5 和 6。

重新检查 JLS#4.5.1Class&lt;? extends Number&gt;Class&lt;? extends Runnable&gt; 将无法比较,因为边界 NumberRunnable 没有子类型关系。那太严格了,因为可能有一个 Number 的子类来实现 Runnable。实际上,javac8 确实允许转换或比较这两种类型。

有趣的是,javac8 会禁止比较 Class&lt;? extends Boolean&gt;Class&lt;? extends Runnable&gt;。显然,Boolean 是最终的事实是这里的一个因素。这听起来很像 JLS#5.5.1

似乎对于“可证明不同”的测试,javac8 有时会使用强制转换测试。

虽然这整个事情一团糟,但幸运的是我们总是可以在一个公共超类型中添加一个中间转换来解决所有问题,所以这不是什么大问题

Dog dog = (Dog)(Animal)cat;

Dog.class == (Class)Cat.class

【讨论】:

  • 谢谢,这是我正在寻求的解释。另外,我不知道中间转换对编译器有影响——我假设在编译期间这些转换被删除了。
  • 解释是......这是一团糟:)例如根据 JLS#5.5.1 的规则,我们如何将 Boolean 转换为 Comparable&lt;?&gt;?规范不是自洽的。缺乏基础研究。编译器在摸索。
猜你喜欢
  • 2023-03-15
  • 2019-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多