Class.asSubclass签名

时间:2015-02-19 10:40:03

标签: java generics effective-java

我的问题很理论......这是Class.asSubclass(Javadoc)的签名:

public <U> Class<? extends U> asSubclass(Class<U> clazz)

为什么返回类型中使用了通配符泛型?根据我对泛型的理解,一个更好的签名可能是:

public <U> Class<U> asSubclass(Class<U> clazz)

因为你可以肯定演员

Class<? extends U>

更简单的

Class<U>
Bloch在他的书“Effective Java”中建议(第137页,第28项):

  

不要将通配符类型用作返回类型。它不会为您的用户提供额外的灵活性,而是迫使他们在客户端代码中使用通配符类型。

这种选择背后的原因是什么?我错过了什么? 非常感谢你提前。

修改 正如@egelev建议的那样,我确实可以用另一种方式表达我的问题......事实上,“按原样”返回输入参数将毫无意义。所以真正的问题是: 与普通的强制转换相比,Class.asSubclass方法的真正用处是什么?如果出现转换问题,两者都会抛出ClassCastException。

可能已添加它以避免在特定情况下未经检查的转换:当您将asSubclass方法的结果直接传递给另一个请求约束类型参数的方法时,如此处(取自Effective) Java,第146页):

AnnotatedElement element;
...
element.getAnnotation(annotationType.asSubclass(Annotation.class));

上述方法的签名是:

<T extends Annotation> T getAnnotation(Class<T> annotationClass);

在我看来,asSubclass方法只是一种在没有正确的编译器警告的情况下进行(事实上!)未经检查的强制转换的方法......

这最终重新提出我以前的问题:签名

public <U> Class<U> asSubclass(Class<U> clazz)

同样有效(即使很奇怪,我也承认)!它将与getAnnotation示例完全兼容,并且不会限制客户端代码强制它使用无意义的通配符泛型。

EDIT2: 我想我的一般问题已经解决了;非常感谢你。如果有人有关于asSubclass签名正确性的其他好例子,请将它们添加到讨论中,我希望看到一个完整的例子,使用asSubclass和我的签名,显然无效。

3 个答案:

答案 0 :(得分:13)

如果返回类型为Class<? extends U>。让我们首先尝试理解getClass签名:

AbstractList<String> ls = new ArrayList<>();
Class<? extends AbstractList> x = ls.getClass();

现在让编译器允许我们这样做:

Class<AbstractList> x = ls.getClass();

这可能是错的。因为在运行时,ls.getClass将是ArrayList.class而不是AbstractList.classls.getClass也不能返回Class<ArrayList>,因为ls的类型为AbstractList&lt;&gt;而不是Arraylist

所以编译器现在说 - 好吧!我无法退回Class<ArrayList>,也无法返回Class<AbstractList>。但是因为我知道ls是一个抽象列表,所以实际的类对象只能是AbstractList的子类型。所以Class<? extends AbstractList>是一个非常安全的赌注。因为外卡: 你不能做:

AbstractList<String> ls = new ArrayList<>();
Class<? extends AbstractList> x = ls.getClass();
Class<ArrayList<?>> xx = x;
Class<AbstractList<?>> xxx = x;

同样的逻辑适用于您的问题。假设它被宣布为:

 public <U> Class<U> asSubClass(Class<U> c)

以下内容将编译:

 List<String> ls = new ArrayList<>();
 Class<? extends List> x = ls.getClass();
 Class<AbstractList> aClass = x.asSubclass(AbstractList.class); //BIG ISSUE

运行时aClass以上Class<Arraylist>而不是Class<AbstractList>。所以这不应该被允许!! Class<? extends AbstractList>是最好的选择。



我在看问题时的第一个想法是,为什么它没有被宣布为:

 public <U extends T> Class<? extends U> asSubClass(Class<U> c)

对我可以传递的参数有一个编译时间限制更有意义。但我认为它不是首选的原因是 - 这会打破前java5代码的向后兼容性。例如,如果声明asSubClass如上所示,那么使用Java5之前编译的代码将不再编译。

Class x = List.class.asSubclass(String.class); //pre java5
// this would have not compiled if asSubclass was declared above like

快速检查:

    public static <T, U extends T> Class<? extends U> asSubClaz(Class<T> t, Class<U> c){
        return t.asSubClass(c);
    }
    public static <T, U> Class<? extends U> asSubClazOriginal(Class<T> t, Class<U> c){
        return t.asSubClass(c);
    }
    asSubClazOriginal(List.class, String.class);
    asSubClaz(List.class, String.class); //error. So would have broken legacy code

PS:对于编辑过的问题,关于为什么asSubClass而不是演员? - 因为演员是背叛。例如:

List<String> ls = new ArrayList<>();
Class<? extends List> x = ls.getClass();
Class<AbstractList> aClass = (Class<AbstractList>) x;

以上将永远成功,因为泛型被删除。所以它的类被投到了一个类。但是aClass.equals(ArrayList.class)会给出错误的。所以肯定演员是错的。如果您需要类型安全,可以使用上面的asSubClaz

答案 1 :(得分:7)

我认为,为了理解这一点,首先必须理解类型和类之间的细微差别:对象具有类,引用具有类型

类与类型

我认为一个例子是解释我的意思的好方法。假设存在以下类层次结构:

class Mammal {}
class Feline extends Mammal {}
class Lion extends Feline {}
class Tiger extends Feline {}

由于子类型多态性,我们可以声明对同一对象的多个引用。

Tiger tiger = new Tiger(); //!
Feline feline = tiger;
Mammal mammal = feline;

有趣的是,如果我们向这些参考文献的每个人询问他们班级的名字,他们都会回答相同的答案:&#34; Tiger&#34;。

System.out.println(tiger.getClass().getName()); //yields Tiger
System.out.println(feline.getClass().getName()); //yields Tiger
System.out.println(mammal.getClass().getName()); //yield Tiger

这意味着对象的实际类是固定的,它的类是我们在使用&#34; new&#34;时用来实例化它的类。我们上面的代码中的运算符。

另一方面,引用可能具有不同的类型,与实际的对象类(即本例中的老虎)在多态上兼容。

因此,对象具有固定类,而引用具有兼容类型。

这往往令人困惑,因为类名与我们用来命名引用类型的名称相同,但从语义上讲,存在一个细微的差别,正如我们在上面所看到的那样。

也许最令人困惑的部分是认识到类也是对象,因此它们可以拥有自己的兼容引用。例如:

Class<Tiger> tigerClass = null;
Class<? extends Tiger> someTiger = tigerClass;
Class<? extends Feline> someFeline = tigerClass;
Class<? extends Mammal> someMammal = tigerClass;

在上面的代码中,被引用的对象是类对象(我暂时将其保留为null),这里使用的这些引用具有不同的类型来到达该假设对象。

所以,你看,&#34; class&#34;这里用于命名&#34;类型的引用&#34;指向一个类型与这些引用中的任何一个兼容的实际类对象。

在上面的例子中,我无法定义这样的类对象,因为我将原始变量初始化为null。这是故意的,我们会在一瞬间看到原因。

关于在参考文献上调用getClassgetSubclass

关注考虑getClass方法的@ Jatin示例,现在让我们考虑下面的多态代码:

Mammal animal = new Tiger();

现在我们知道,无论我们的animal引用属于Mammal类型,对象的实际类是Tiger(即类)

如果我们这样做,我们应该得到什么?

? type = mammal.getClass()

type应该是Class<Mammal>还是Class<Tiger>

嗯,问题是当编译器看到类型为Mammal引用时,它无法分辨该引用所指向的对象的实际类。那只能在运行时确定,对吗?它实际上可能是一个哺乳动物,但它也可能是它的任何子类,如老虎,对吗?

因此,当我们要求它上课时,我们不会得到Class<Mammal>,因为编译器无法确定。相反,我们得到Class<? extends Mammal>,这更有意义,因为,毕竟,编译器知道基于子类型多态的规则,给定的引用可能指向哺乳动物或其任何子类型。

此时,你可能会看到在这里使用单词class的微妙细微差别。看起来我们实际上从getClass()方法中获得的是某种类型引用,我们用它来指向原始对象的实际类,如前所述。

嗯,关于asSubclass方法可以说同样的事情。例如:

Mammal tiger = new Tiger();

Class<? extends Mammal> tigerAsMammal = tiger.getClass();
Class<? extends Feline> tigerAsFeline = tigerAsMammal.asSubclass(Feline.class);

当我们调用asSubclass时,我们得到了引用指向的实际类类型的引用,但是编译器不再能确定实际应该是什么,因此你得到了一个像Class<? extends Feline>这样的宽松参考。这是编译器可以假设的关于对象的原始性质的最多,这就是我们得到的全部原因。

新Tiger()。gerClass()怎么样?

我们可能期望获得Class<Tiger>(没有wirldcards)的唯一方法应该是访问原始对象,对吧?像:

Class<Tiger> tigerClass = new Tiger().getClass()

有趣的是,我们总是通过参考类型到达老虎对象,对吗?在Java中,我们永远不能直接访问对象。由于我们总是通过引用到达对象,因此编译器无法对返回的引用的实际类型做出假设。

这就是为什么即使这段代码也会产生Class<? extend Tiger>

Class<? extends Tiger> tigerClass = new Tiger().getClass();

也就是说,编译器不保证new运算符可以返回此处。对于所有重要的事情,它可能会返回一个与Tiger兼容的对象,但不一定是其类是Tiger本身的对象。

如果您更改工厂方法的new运算符,这将变得更加清晰。

TigerFactory factory = new TigerFactory();
Class<? extends Tiger> tigerClass = tigerFactory.newTiger().getClass();

我们的老虎工厂:

class TigerFactory {
   public Tiger newTiger(){
     return new Tiger(){ } //notice this is an anonymous class
   }
}

我希望这在某种程度上有助于讨论。

答案 2 :(得分:2)

这种方法背后的想法正是添加这个通配符。据我所知,此方法的目的是将this类对象强制转换为表示参数clazz的任何子类的类对象。这就是为什么结果被声明为Class<? extends U>(扩展U的东西的类)。否则它没有意义,因为它的返回类型将与其唯一参数的类型完全相同,即除return clazz;之外什么也不做。它进行运行时类型检查并将参数强制转换为Class<? extends U>(当然,如果调用的目标(即this)不代表U派生类,它将抛出ClassCastException)。这是比简单(Class<? extends U>)类型的转换更好的方法,因为后者使编译器产生警告。