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

心不动则不痛 提交于 2020-02-25 02:15:16

问题


I'm using generics to abstract out Comparable data types, as illustrated in the code supplied below. This case arose from a Java Swing component, specifically from attempting to adapt table model to use generics.

I have a working solution to that problem (Case 3A below). However, during the process of arriving at that solution I became confused by the behavior of the compiler when I changed the class signature from T extends Comparable<T> to T extends Comparable<? super T>.

Why does the compiler want Comparable to be a raw type (Case 3A below)? I expected the class signature to work (Case 6A, Class<T extends Comparable<? super T>>), but it requires a cast and causes a compiler error when using if (type == Boolean.class).

Why do cases 2B, 3A/B, and 4A/B allow if (type == Boolean.class) but cases 1A/B, 2A, 5A/B and 6A/B cause a compiler error? Specifically, I'd like an explanation of this error in the case of 2A vs. 2B, which share the same class signature.

The runnable test cases are below. The external Score and DetailedScore classes are supplied to demonstrate a case involving inheritance, which highlights the difference between Case A and Case 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;
    }
}

回答1:


See javadoc of Object.getClass(), the return type of value.getClass() is Class<? extends |T|>, i.e., Class<? extends Comparable>.

The type of expression value.getClass() undergoes wildcard capture before it's used further; therefore we see the captured type Class<capture#2-of ? extends Comparable> in messages.

The question surrounding type==Boolean.class, according to JLS#15.21.3, depends on whether one type can be casted to another. Casting between 2 types is forbidden if it's provable at compile time that it is impossible; in which case == test is forbidden too. That makes sense.

However, JLS#5.5.1 might be a little sloppy. First, lets follow its exact words. Class<a1> can be casted to Class<a2>, only if type arguments a1, a2 are not provably distinct. Obviously we don't want Class<Boolean> casted to Class<FLoat>.

For your case between Class<T> and Class<Boolean>, according to JLS#4.5.1, they are not provably distinct, due to Boolean<:Comparable. Therefore it should be allowed. Your compiler forbids it, but javac8u45 allows it.

Follow the same reasoning, type==Boolean.class should be allowed in all your cases. That is the behavior of IntelliJ. But javac8 does not; it forbids case 5 and 6.

Reexamining JLS#4.5.1, Class<? extends Number> and Class<? extends Runnable> would be incomparable, because bounds Number and Runnable have no subtype relationship. That would be too restrictive, because there could be a subclass of Number that implements Runnable. Indeed, javac8 does allow the two types to be casted or compared.

Interestingly, javac8 would forbid comparing Class<? extends Boolean> and Class<? extends Runnable>. Obviously, the fact that Boolean is final is a factor here. That sounds a lot like JLS#5.5.1.

It appears that for "provably distinct" testing, javac8 sometimes uses casting conversion test.

While this whole thing is a mess, fortunately we can always add an intermediary cast to a common supertype to resolve all the issues, so this is not too big of a problem

Dog dog = (Dog)(Animal)cat;

Dog.class == (Class)Cat.class



回答2:


Compiler warnings are not the same as errors... for example, your "case 2A" will compile... it will generate a warning but will give you a usable class file. These are generally due to erasure, which is a complicated topic that I don't pretend to fully understand.

But for example, when T extends Comparable<? super T>, if you have value which is declared as a T, of course you should be able to cast value.getClass() to Class<T>. But due to erasuer the compiler only remembers that value.getClass() should return a Class<? extends Comparable>, so it warns you that you should be careful when casting to Class<T>. You can shut up the warning, if you're sure about things, by using the @SuppressWarnings annotation, like this:

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

A good IDE (like Eclipse) will suggest this annotation as a possibility to fix the warning; if you aren't using a good IDE I'd suggest you start.

So let's talk about the actual errors instead. In Java you can only check for object equality if two things are the same type or are assignable to the same type. For example, this works:

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

    }
}

because, at the point of comparison, f could be a Bar under the covers; it's allowed. On the other hand, this does not work:

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

    if (b == i) {  // ERROR

    }

There is no way these two could ever be equal. It's an error.

From the compiler's point of view, Class<T> and Class<Boolean> (which is the type of Boolean.class, natch) are different types. If T extends Comparable<? super T>, they are not assignable to each other, since Boolean implements Comparable<Boolean>; it does not implement Comparable<? super Boolean>. So you cannot check for equality with ==.

(If T extends Comparable<T> then that is compatible with Boolean, and you can do that comparison.)

So don't use ==. Use if (type.equals(Boolean.class)) {...} instead, and the compiler will be happy and you'll be happy.



来源:https://stackoverflow.com/questions/31296596/why-does-classt-boolean-class-cause-a-compiler-error-when-t-extends-comp

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!