为什么 `Class<T> == Boolean.class` 在 `T extends Comparable< 时会导致编译器错误?超级T>`?

Why does `Class<T> == Boolean.class` cause a compiler error when `T extends Comparable<? super T>`?

我正在使用泛型抽象出 Comparable 数据类型,如下面提供的代码所示。这种情况源于 Java Swing 组件,特别是试图调整 table 模型以使用泛型。

我有解决该问题的有效方法(下面的案例 3A)。但是,在得出该解决方案的过程中,当我将 class 签名从 T extends Comparable<T> 更改为 T extends Comparable<? super T>.

时,我对编译器的行为感到困惑

为什么编译器希望 Comparable 是原始类型(下面的案例 3A)?我希望 class 签名可以工作(案例 6A,Class<T extends Comparable<? super T>>),但它需要转换并在使用 if (type == Boolean.class).

时导致编译器错误

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

可运行的测试用例如下。提供外部ScoreDetailedScoreclasses来演示涉及继承的案例,它突出了案例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;
    }
}

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

但是例如,当 T extends Comparable<? super T> 时,如果您有 value 声明为 T,当然您应该能够将 value.getClass() 转换为 Class<T>。但是由于 erasuer,编译器只记得 value.getClass() 应该 return 和 Class<? extends Comparable>,所以它会警告你转换为 Class<T> 时要小心。如果您对事情有把握,可以使用 @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<T>Class<Boolean>(也就是Boolean.class的类型,natch)是不同的类型。如果 T extends Comparable<? super T>,它们不能相互赋值,因为 Boolean 实现了 Comparable<Boolean>;它实施Comparable<? super Boolean>。所以你不能检查 ==.

是否相等

(如果 T extends Comparable<T> 则与 Boolean 兼容,您可以进行比较。)

所以不要使用 ==。请改用 if (type.equals(Boolean.class)) {...},编译器会很高兴,您也会很高兴。

查看Object.getClass()的javadoc,value.getClass()的return类型是Class<? extends |T|>,即Class<? extends Comparable>

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

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

不过,JLS#5.5.1可能有点草率。首先,让我们跟随它的原话。 Class<a1> 可以转换为 Class<a2>,前提是类型参数 a1, a2 不可证明是不同的。显然我们不希望 Class<Boolean> 转换为 Class<FLoat>.

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

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

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

有趣的是,javac8 会禁止比较 Class<? extends Boolean>Class<? extends Runnable>。显然,Boolean 是 final 的事实是这里的一个因素。这听起来很像 JLS#5.5.1

似乎为了"provably distinct"测试,javac8有时会使用casting转换测试。

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

Dog dog = (Dog)(Animal)cat;

Dog.class == (Class)Cat.class