类.as子类签名

2022-09-02 21:28:55

我的问题很理论化...这是 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>

布洛赫在他的著作《有效的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 示例完全兼容,并且不会限制客户端代码,强制它使用毫无意义的通配符泛型。

编辑2:我认为我的一般问题已经解决;谢谢。如果有人有关于asSubclass签名的正确性的其他好例子,请将它们添加到讨论中,我希望看到一个完整的示例,使用asSubclass和我的签名,显然不起作用。


答案 1

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

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

现在编译器允许我们做:

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

这是错误的。因为在运行时,将是 而不是 。两者都不能返回,因为是抽象列表类型<>而不是数组列表ls.getClassArrayList.classAbstractList.classls.getClassClass<ArrayList>ls

所以编译器,现在说 - 好吧!我不能回来,也不能回来。但是因为我知道肯定是一个 AbstractList,所以实际的类对象只能是 AbstractList 的子类型。所以这是一个非常安全的赌注。由于通配符:您不能执行以下操作:Class<ArrayList>Class<AbstractList>lsClass<? 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

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



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

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

对我可以传递的参数进行编译时限制更有意义。但我认为它不受欢迎的原因是 - 这将破坏java5之前代码的向后兼容性。例如,如果像上面所示声明的那样,使用 Java5 之前的代码编译的代码将不再编译。asSubClass

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


答案 2

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

类与类型

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

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

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

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

有趣的是,如果我们问这些参考文献中的每个人他们的班级名称,他们都会用相同的答案回答:“老虎”。

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

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

另一方面,引用可能具有不同的类型,与实际的对象类(即在这种情况下的老虎)多态兼容。

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

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

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

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

在我上面的代码中,被引用的对象是一个类对象(我暂时将其保留为空),并且此处使用的这些引用具有不同的类型来访问该假设对象。

所以,你看,这里的“class”这个词是用来命名一个“引用类型”的,指向一个实际的类对象,其类型与任何这些引用兼容。

在上面的示例中,由于我将原始变量初始化为null,因此我无法定义此类类对象。这是有意为之,一会儿我们就会明白为什么。

关于在引用上调用 getClassgetSubclass

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

Mammal animal = new Tiger();

现在我们知道,无论我们的引用是类型,对象的实际类是,并且永远是(即类)。animalMammalTiger

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

? type = mammal.getClass()

应该是 a 还是 a ?typeClass<Mammal>Class<Tiger>

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

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

在这一点上,你可能会看到在这里使用单词类的微妙细微差别。看起来,我们实际上从该方法中得到的是某种类型引用,我们用它来指向原始对象的实际类,正如我们之前已经解释过的那样。getClass()

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

Mammal tiger = new Tiger();

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

当我们调用时,我们得到的是对引用所指向的实际类类型的引用,但是编译器无法再确定该实际性质应该是什么,因此您会得到一个更宽松的引用,例如.这是编译器可以假设的关于对象原始性质的最多,这就是为什么我们得到的就是这个。asSubclassClass<? extends Feline>

那么新的Tiger().gerClass()呢?

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

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

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

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

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

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

如果更改工厂方法的运算符,这将变得更加清晰。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
   }
}

我希望这能以某种方式促进讨论。


推荐