何时需要使用Some<E extends Some<E>>而不是Some<E extends Some>?

8

注意:这个问题与枚举无关,因此不是重复的问题。 枚举被强制仅与自身进行比较,因为编译器生成类型参数,而不是Java递归类型参数。

我正在尝试找到将类声明为以下形式的优势:

public class Some<E extends Some<E>>

相对于将其声明为:

public class Some<E extends Some>

我尝试过提供返回E的方法和返回Some<E>的方法,在复杂类层次结构中进行不同的交叉调用,每次尝试删除额外的<E>时 - 没有新的错误/警告出现。

你能否向我展示一种证明这个额外的<E>优势的方法? 我认为这是因为JDK声明的存在:<E extends Comparable<? super E>>

SO上其他问题的回答给出了以下解释:

通过使用附加结构,您知道任何扩展枚举的类仅可与其本身进行比较

但是,我可以轻松地打破这个理论:

public static class Animal<E extends Animal<E>> {
    public boolean compare(E other) {...}
}

public class Cat extends Animal<Cat> { }
public class Dog extends Animal<Cat> { } // Note "Cat" !!!

尽管使用了通用递归,我仍然可以比较狗和猫
Dog dog = new Dog();
dog.compare(new Cat());

翻译理论:

你应该知道,任何继承了Animal的类只能与自己进行比较

这是错误的 - 我比较了继承Animal的类Dog和Cat,而不是它本身。


好的,这是因为这就是泛型的工作方式(编译后 Some == Some<E>)。搜索原始类型和关于泛型的信息。 - Marco Acierno
@Pshemo:实际上这是那个问题的重复;遗憾的是,那个问题没有好的答案 :( - Oliver Charlesworth
1
由于面向对象编程的目的是使编写和理解程序更加容易和快速,而阅读这段代码让我眼花缭乱,内心深处的孩子哭泣,所以很可能这个问题只是学术性质的;如果你曾经遇到过需要编写 Some<E extends Some<E>> 的情况,那么你很可能在程序的某个地方犯了架构错误。要么就是你已经转向了故意混淆 Java 代码的黑暗面。 - Parthian Shot
1
复制标记 - 这个问题在其他 Stack Overflow 问题中还没有答案。其他问题表明 Java 编译器 改进了 Enum,因为它将其生成为 extends Enum<SAME CLASS>,但是额外的递归类型参数目前并没有增加价值(请参见我的回答)。 - Kinga Odecka
我想再进一步提问,什么情况下需要使用 Some<E extends Some> 或者 Some<E extends Some<E>> 而不是仅仅使用 Some<E> - newacct
显示剩余4条评论
4个回答

3

有很少的情况需要像 class Some<E extends Some<E>> 这样的限定。大多数情况下,人们写这个限定时,实际上并没有在代码中使用,而 class Some<E> 也同样有效。

然而,在某些特定的情况下,class Some<E extends Some<E>> 中的限定确实被使用。例如:

abstract class Some<E extends Some<E>> {
    abstract E foo();
    Some<E> bar() {
        return foo();
    }
}

关于您的问题——class Some<E extends Some>怎么样?首先,最明显的问题是你正在使用一个原始类型。新代码中永远不应该使用原始类型。但您并不相信。
上述类别(class Some<E extends Some>)的原始类型确实可以编译,并且会发出警告(您可以忽略它,自己决定)。然而,原始类型意味着可能会对其进行不安全的操作。
需要花费一些努力才能提供示例来证明它是不安全的。这里有一个例子:
abstract class Some<E extends Some> {
    abstract E foo();
    Some<E> bar() {
        return foo();
    }
}

class SomeFoo extends Some<SomeFoo> {
    SomeFoo foo() { return this; }
}

class SomeBar extends Some<SomeFoo> {
    SomeFoo foo() { return new SomeFoo(); }
}

class SomeBaz extends Some<SomeBar> {
    SomeBar foo() { return new SomeBar(); }
}

// then in some method:
Some<SomeBar> a = new SomeBaz();
Some<SomeBar> b = a.bar();
SomeBar c = b.foo();

代码编译时没有错误,但会出现警告,并在运行时抛出ClassCastException

这真的很有趣,确实在SomeBar c = b.foo();行抛出了ClassCastException,但我无法理解为什么会发生这种情况,你能否稍微解释一下?这三个子类让我感到困惑。 - EpicPandaForce
2
@Zhuinden:这是因为在Some.bar()中存在一种不安全的未检查转换,将foo()的结果(类型为E,它是Some的子类型)转换为Some<E>。原始类型Some可以隐式转换为Some<E>,但这是不安全的,因为它可能是Some<SomethingElse>。我只是举了一个例子来说明这种情况。基本上,只有当E也是Some<E>时,转换才是安全的。但是在class SomeBaz extends Some<SomeBar>中,E并不满足这个条件(SomeBar没有扩展Some<SomeBar>)。 - newacct

2

关于这个陈述

每次我尝试去除额外的 - 没有新的错误/警告出现。

这不应该是这种情况。它应该打印一个警告,因为您正在使用 原始类型 Some,而其结果缺少类型安全性,正如 newacct 的答案 所示。


Dog/Cat 示例有点牵强,并且有些缺陷。您建议声明一个类

public class Dog extends Animal<Cat> { } // Note "Cat" !!!

但是在这里,类型参数基本上意味着:“这个参数(Cat)是这个类(Dog)的对象可以与之进行比较的类型”。因此,您明确地表明一个Dog应该可与Cat进行比较。即使是使用先进的语言和智能编译器,编程人员也有责任编写有意义的代码。
实际上,并不多见这些自引用的泛型类型的情况。其中一个例子在这个FAQ条目中进行了概述:它声明了一个节点结构(即树),其中类型参数可用于将树的定义与节点的实际类型解耦。
public class RecurringTest {
    public static void main(String[] args) {
        SpecialNode sa = new SpecialNode(null);
        SpecialNode sb = new SpecialNode(sa);
        SpecialNode s = sa.getChildren().get(0);
    }
}

abstract class Node<N extends Node<N>> {
    private final List<N> children = new ArrayList<N>();
    private final N parent;

    protected Node(N parent) {
        this.parent = parent;
        if (parent != null) {
            this.parent.getChildren().add(getThis());
        }
    }

    abstract N getThis();

    public N getParent() {
        return parent;
    }

    public List<N> getChildren() {
        return children;
    }
}

class SpecialNode extends Node<SpecialNode> {
    public SpecialNode(SpecialNode parent) {
        super(parent);
    }

    SpecialNode getThis() {
        return this;
    }

}

但是从我的个人经验来看,当你认为需要创建这样一种类型时,你应该仔细考虑其优缺点。后者主要指降低可读性。当你可以选择像

Node<? extends Node<? extends N, ? extends T>, T> doSomething(
    Node<? super Node<? extends N>, ? extends T> p, 
    Node<? extends Node<? super N>, ? super T> c) { ... }

这些方法是类型安全的,或者像

Node doSomething(Node parent, Node child) { ... }

如果存在不安全类型(因为使用了原始类型或者只是因为类型没有泛型化),那么我更喜欢后者。代码是供人阅读的。


1

区别在于 Some<E extends Some> 有两个含义:

  • E 是原始类型。简而言之,原始类型从类中剥离了所有泛型信息,这可能会导致意外行为。
  • E 可以是 Some任何类!

相反,使用 Some<E extends Some<E>> 表示:

  • E 是有类型的
  • E 必须与它声明所在的类相同。

虽然原始类型是一个问题,但更大的影响在于类型边界。该代码演示了区别,其中“B”类使用(或尝试使用)“A”类作为泛型类型。

// The raw version
interface Some<E extends Some> {
    E get();
}

class SomeA implements Some<SomeA> {
    public  SomeA get() {
         return new SomeA();
    }
}

// Compiles OK
class SomeB implements Some<SomeA> {
   public SomeA get() {
         return new SomeA();
    }
}

// The typed version
interface SomeT<E extends SomeT<E>> {
    E get();
}

class SomeTA implements Some<SomeTA> {
    public  SomeTA get() {
         return new SomeTA();
    }
}

// Compile error 
class SomeTB implements SomeT<SomeA> {
   public SomeA get() { 
         return new SomeTA();
   }
}

SomeB编译正常 - E的类型仅绑定到Some,因此任何Some类都可以,但是类SomeTB会抛出编译错误:

类型参数SomeTA不在类型变量E的范围内

E的类型必须与包含类完全相同。

如果您希望参数和返回类型与类本身相同(这通常是有类型类所需的),则这一点非常重要。


原始类型所限定的类型不是很重要,因为它仍然是Some,但可能存在某些情况下会有所区别(目前我想不出任何例子)。


0

看起来Some<E extends Some>目前没有比Some<E extends Some<E>>更有优势

根据与Java Champions和Java Generics FAQ的创建者一起撰写的article:

文章承认了一些可能性/假设:

  • 由于Enum是一个泛型类,我们应该只在类型的情况下使用它
  • Java可能会变得更加严格,原始类型可能会变得非法
我认为枚举已经足够了。 然而,正如Philip Wadler对我指出的那样,由于Enum是一个泛型类,我们应该只在类型的情况下使用它。将来,Java可能会变得更加严格,原始类型可能会变成非法的。因此,我们可以选择编写Enum<E extends Enum<E>>Enum<E extends Enum<?>>,其中第一个选项更准确。即使编译器目前没有显示任何差异,但我将来可能会看到警告,所以我也遵循这个习惯用法在我的类中。

1
真正的答案是什么?public class Some<E extends Some> 会产生“Some is a raw type. References to generic type Some<E> should be parameterized”的编译器警告,而 public class Some<E extends Some<E>> 则不会。 - Powerlord
这个在文章中没有提到。所以真正的答案是它只能防止编译器警告。 - Kinga Odecka
只有编译器可以验证泛型类型,在运行时,JVM看不到原始类型或定义类型之间的区别。 - Tomas Smagurauskas
编译器警告不是玩笑,它们通常有存在的理由。 - EpicPandaForce

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