Java泛型 - 需要解释

3

我有一个关于Java泛型的问题。在下面的代码中,我们有一个接口B,该接口由另一种类型参数化,该类型必须实现接口A。

这段代码是正确的。 问题是:为什么它不与以下list()方法声明一起使用?

private <X extends A, Y extends B<X>> List<Y> list()

可运行的代码:

public interface A {
}
public interface B<T extends A> {
}
public class Test {

    private static class AA implements A {}
    private static class BB implements B<AA> {}

    private <R extends A, X extends R, Y extends B<X>> List<Y> list() {
        return null;
    }

    private void test() {
        List<BB> l = list();
    }
}

编辑:我已经重写了代码。现在我们有一只鸟,它可以通过声音进行参数化。问题是为什么需要无用的_t?

public class Test {
    public interface Sound {
    }
    public interface Bird<T extends Sound> {
    }

    private static class Quack implements Sound {}
    private static class Duck implements Bird<Quack> {}


    private <useless_t extends Sound, sound_t extends useless_t, bird_t extends Bird<sound_t>> List<bird_t> list() {
            return null;
    }

    private void test() {
            List<Duck> l = list();
    }
}

2
你想要实现什么目标? - krock
买些书,然后深入研究泛型。你完全没有理解它们的重点。我建议看Java Head First。 - Damian Leszczyński - Vash
我同意。你试图在那个列表类型中指定整个继承层次结构。我不知道你为什么想这样做,但几乎肯定不是最好的方法。如果你提供更多关于你想要实现什么的上下文,你可能会得到更好的答案。 - Jochen
3个回答

3

我的 Eclipse IDE 不能直接编译你的代码示例,但是如果给出额外的类型提示,它们就能够编译通过。在第二个示例中,无论是否使用类型参数 useless_t,以下这行代码都无法通过编译:

List<Duck> l = list();

但以下内容对我来说可以编译成功:
List<Duck> l = this.<Sound, Quack, Duck> list();

useless_t因式分解后,以下代码也可以编译:

List<Duck> l = this.<Quack, Duck> list();

因此,这基本上是编译器没有正确获取类型参数的问题,您需要明确地给出类型。
更新:如果您真的遇到了添加useless_t会有所不同的程序,则处于不安全的地带,并依赖于未指定的编译器行为。
您遇到了不同编译器表现不同的问题,即类型推断。 JLS在编译器必须推断类型和拒绝推断类型的位置上并不完全清楚,因此这里有些余地。不同版本的Eclipse编译器和不同版本的javac在它们推断类型的位置上存在差异。对于javac,在比较不同的1.5.0_x版本时也是如此,而Eclipse编译器通常可以推断比javac更多的类型。
您应该仅在所有常见编译器都成功的情况下依赖于类型推断,否则请给出类型提示。有时,这就像引入一个临时变量一样容易,但有时(如在您的示例中),您必须使用var.method()语法。
关于评论:如果我想使Duck.getSound()方法使用泛型返回Quack而不是Sound呢?
假设Bird接口具有以下方法:
public interface Bird<T extends Sound> {
    T getSound();
}

那么你可以这样实现它:
private static class Duck implements Bird<Quack> {
    public Quack getSound() { return new Quack(); }
}

这是泛型的一个用例 - 允许实现指定具体类型,这样即使是超类也可以使用该类型。 (Bird接口可以有一个setSound(T),或者对T执行其他操作,而不知道T的具体类型。)
如果调用方只知道实例的类型为Bird<?extends Sound>,则必须像这样调用getSound:
Sound birdSound = bird.getSound();

如果调用者知道 Quack,他可以执行一个 instanceof 测试。但是如果调用者知道这只鸟实际上是一只 Bird<Quack>,或者甚至是一只 Duck,那么他可以编写以下代码并且它会按预期编译:
Quack birdSound = bird.getSound();

但要小心:将接口或超类中使用的太多类型泛型化,可能会使系统变得过于复杂。正如Slanec所写,重新思考您的实际设计,看看是否真的需要这么多泛型。

我曾经走得太远,最终得到了一个基于此类接口的界面层次结构和两个实现层次结构:

interface Tree<N extends Node<N>,
               T extends Tree<N, T>> { ... }

interface SearchableTree<N extends SearchableNode<N>,
                         S extends Searcher<N>,
                         T extends SearchableTree<N, S, T>>
    extends Tree<N, T> { ... }

我不建议跟随那个例子。;-)

这个 useless_t 真的是没用的。另外,private <bird_t extends Bird<? extends Sound>> List<bird_t> list() 可以立即编译通过。 - Petr Janeček
话虽如此,在这个例子中,Duck 应该从它的构造函数和内部字段知道它的 Quacks,而不是一个通用类型。重新思考你的真实设计,看看是否真的需要有这么多泛型。 - Petr Janeček
1
有趣的是,它可以在我的Eclipse(3.7.2,Java6)上编译,但在javac上失败了。但如果从Maven编译,则可以正常工作。至于设计评论-如果我想使用泛型使Duck.getSound()方法返回Quack而不是Sound,该怎么办? - Sergey Alaev
@user1445898,这需要一个专门的回答...即使它表现为Sound,它仍将返回Quack。如果Quack覆盖了其超类型的某些方法,则会调用Quack的方法。如果Quack在接口上有一些新方法,并且需要使用它们,则需要使用instanceof或泛型或其他东西。 - Petr Janeček
@user1445898:你发布的代码无法编译。 "list()"方法没有定义在任何地方。至于声音,你的鸟类接口不应该泛化声音。声音就是声音,没有人真正关心一只鸟是发出嘎嘎声还是啾啾声等等。这是一个最好留给对象内部的实现细节,而不是通用签名的一部分。 - Matt
它在JDK8/Eclipse Luna中工作并编译正确 :) 因此这是Java编译器的错误。 - Sergey Alaev

1
我会这样说:AA通过定义List<AA> l = list()来实现A,你期望它扩展B<X>,但它并没有。无论如何,你可以看到编写这样的代码会很容易让人混淆。这太复杂了。

0

您对Java泛型有一些误解。需要记住的是,这是一个微妙的问题,List<Y>不是关于列表内容的,而是关于列表本身的修改。

让我们稍微推广一下;假设我有interface Animalinterface Dog extends Animal以及interface Cat extends Animal。(我会随着我们的讨论发明更多的类和接口。)现在,如果我声明一个返回动物作为List<Animal> createList()的方法,以下代码没有任何问题:

List<Animal> litter = createList();
Cat tabby = new Tabby();
litter.add(tabby);
Dog poodle = new Poodle();
litter.add(poodle);

那是因为狗是动物,猫也是动物;在类型List<Animal>上的方法签名是add(Animal);我们可以使用任何有效的Animal实例调用add,正如预期的那样。但是,List上的类型参数不会修改或限制列表的内容,它修改列表本身的类型;“猫的列表”不是“动物的列表”,“狗的列表”也不是。即使createLitter()方法实际上返回一个只包含Parrot实例的new ArrayList<Animal>(),上面的代码也是可以的。但是,你不能“缩小”列表的类型。例如,这是一个编译错误:
List<Bird> birds = createList(); // does not compile

假设允许这样做,createList 返回一个包含我们的虎斑猫的“动物列表”,那么以下代码将导致类转换异常:
Bird leaderOfTheFlock = birds.get(0);

你也不能“扩展”列表的类型。想象一下如果可能的话:

List<Object> things = createList(); // does not compile

这也是不允许的原因之一,因为代码现在可以将new Integer(0)添加到things中 - 因为Integer是一个Object。显然,这也不是我们想要的,原因相同 - "动物列表"不是"对象列表"。在List<Animal>上的类型参数"Animal"修改了列表本身的类型,我们正在谈论两种不同类型的列表。这导致了该点的第一个结果 - 泛型类型不遵循继承(is-a)层次结构。

如果不知道你想做什么,很难从这里开始并保持相关性。我不是要苛刻,但看起来你开始在代码中使用泛型,看看是否有效。我曾经与泛型斗争了多年。即使在阅读了一篇解释这个微妙点的博客后,我还必须重新创建上述内容的许多变体,以加强这个教训,并寻找各种方式,如果我违反规则,就会出现类转换异常。你问题的解决方案可能是代码的其他部分对于你尝试引入的严格类型系统没有很好地定义,而你看到的泛型问题只是其症状。尝试减少泛型并更多地依赖组合和继承。我有时仍然会因为过度使用泛型而自己给自己挖坑。同时,记住泛型的目的不是为了消除转换,而是将类型信息作为编译器验证代码处理类型正确性的辅助工具;或者换句话说,它将运行时错误(类转换)转换为源/编译时错误,因此重要的是要记住编译时拥有的类型信息(即使使用泛型也是有限的)和运行时拥有的类型信息之间的区别(实例的完整类型信息)。


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