Java泛型的地狱

27
我怀疑这个问题之前已经在这里提出了(并得到了答案),但我不知道如何命名这个问题。为什么只有在不传递类本身时,我才能毫无问题地表达通配符?所有的问题都归结于这段代码。除了对genericsHell(ShapeSaver.class)的调用之外,其他所有东西都按预期工作。
interface Shape { }

interface Circle extends Shape { }

interface ShapeProcessor<T extends Shape> { }

class CircleDrawer implements ShapeProcessor<Circle> { } 

class ShapeSaver<T extends Shape> implements ShapeProcessor<T> { }

class Test {
    void genericsHeaven(ShapeProcessor<? extends Shape> a) {}

    void genericsHell(Class<? extends ShapeProcessor<? extends Shape>> a) {}

    void test() {
        genericsHeaven(new CircleDrawer());
        genericsHeaven(new ShapeSaver<Circle>());
        genericsHell(CircleDrawer.class);
        genericsHell(ShapeSaver.class); // ERROR: The method genericsHell is not applicable for the arguments (Class<ShapeSaver>)
    }
}

希望有人能用英语解释一下这个问题。我相当确定它与协变性有关,这意味着将ShapeSaver分配给通用的ShapeProcessor可能会导致运行时异常。但我不确定具体细节。 - OverZealous
是的,我真的很好奇听这个解释。喜欢这个标题。回响了我的经验 :) - Owen
+1 提醒我起来了,我之前问过一个类似的问题https://dev59.com/PlfUa4cB1Zd3GeqPHWLZ - 这可能是通过使用.class导致信息丢失的类似问题。 - Paul Bellora
2
我喜欢这个问题的标题。有人可以用那个标题写一本书。 - Marcelo
2个回答

18
ShapeSaver.class的类型是Class<ShapeSaver>。当将其传递给genericsHell()时,编译器需要检查Class<ShapeSaver>是否是Class<? extends ShapeProcessor<?>的子类型,这可以归结为ShapeSaver是否是ShapeProcessor<?>的子类型。子类型关系不成立,方法调用失败。
对于@Bohemian的解决方案应该也是如此。在T推断后,子类型检查发生在T的限制检查中。它也应该失败。这似乎是一个编译器错误,它以某种方式错误地解释了Raw可分配给Raw<X>的规则,就好像RawRaw<X>的子类型。另请参见Enum.valueOf throws a warning for unknown type of class that extends Enum? 你问题的一个简单解决方案是声明:
void genericsHell(Class<? extends ShapeProcessor> a)

确实,ShapeSaverShapeProcessor的子类型,并且调用可以编译。

这不仅仅是一个解决方法。有一个很好的理由。严格来说,对于任何Class<X>X必须是原始类型。例如,Class<List>是可以的,Class<List<String>>则不行。因为真正表示List<string>的类不存在,只有一个代表List的类。

忽略那些警告你不应该使用原始类型的建议。考虑到Java类型系统的设计,我们有时必须使用原始类型。即使Java的核心API(Object.getClass())也使用原始类型。


你可能想做像这样的事情:

genericsHell(ShapeSaver<Circle>.class);

很遗憾,这是不被允许的。Java本可以引入类型字面量和泛型一起使用,但它没有这么做,这给很多库带来了很多问题。java.lang.reflect.Type非常混乱且无法使用。每个库都必须引入自己的类型系统表示来解决这个问题。

你可以借用一个,例如从Guice中借用,然后就能够...

genericsHell( new TypeLiteral< ShapeSaver<Circle> >(){} )
                               ------------------  

(学会在阅读代码时跳过ShaveSaver<Circle>周围的垃圾)

genericsHell()方法体中,你将拥有完整的类型信息,而不仅仅是类。


太棒了 - 这个答案值得点赞,因为它真正建立在Bohemian的“如何”之上,并最终达到了“为什么”。(不幸的是,我只是被大众所迷惑) - Paul Bellora
@irreputable,非常感谢您详细的解释。这个问题可能并不令人惊讶,实际上是我使用Guice的结果。我真的希望在不久的将来开始转向Scala时能够避免所有这些丑陋的问题。 - Jeff Axelrod
@irreputable,如果您不介意在您的答案中添加适当的Guice函数声明:genericsHell(TypeLiteral <? extends ShapeProcessor <? extends Shape>> typeLiteral)。 - Jeff Axelrod

11

输入 genericsHell 方法可以使其编译通过:

static <T extends ShapeProcessor<?>> void genericsHell(Class<T> a) {}

编辑:这使编译器能够从上下文中指定,或通过编码显式类型,指定ShapeProcessor不是字面上的任何ShapeProcessor,而是与作为参数传递的那个相同的类型。如果调用被显式地打了类型(编译器在内部完成),代码会像这样:

MyClass.<ShapeSaver>genericsHell(ShapeSaver.class);

有趣的是,它会给出一个类型警告,但仍然可以编译。但是,显式类型并不是必需的,因为从参数中已经有足够的类型信息来infer泛型类型。


你的问题缺少一些声明,所以我添加了它们以创建一个Short Self-Contained Correct Example - 也就是说,这段代码可以原样编译。

static interface Shape { }

static interface Circle extends Shape { }

static interface ShapeProcessor<T extends Shape> { }

static class CircleDrawer implements ShapeProcessor<Circle> { }

static class ShapeSaver<T extends Shape> implements ShapeProcessor<T> { }

static void genericsHeaven(ShapeProcessor<? extends Shape> a) { }

// The change was made to this method signature:
static <T extends ShapeProcessor<?>> void genericsHell(Class<T> a) { }

static void test() {
    genericsHeaven(new CircleDrawer());
    genericsHeaven(new ShapeSaver<Circle>());
    genericsHell(CircleDrawer.class);
    genericsHell(ShapeSaver.class);
}

1
那么为什么需要输入方法呢? - Paul Bellora
1
+1 花了我一些时间,但是在阅读了相关文章之后,这个方法变得很有意义。因此,输入该方法允许编译器推断出一个原始类型,这会导致它无法验证嵌套类型信息。举个例子,在输入该方法之后,以下调用会导致原始错误:MyClass.<ShapeSaver<?>>genericsHell(ShapeSaver.class)(请注意嵌套的 <?>)。 - Paul Bellora
好的建议,特别是对于 SSCCE +1。 - mKorbel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接