Класс.подпись asSubclass

мой вопрос вполне теоретический... Это подпись класса.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):

Не используйте типы подстановочных знаков как возвращаемые типы. Вместо обеспечения дополнительной гибкости для пользователей это заставит их использовать подстановочные типы в клиентском коде.

какова причина этого выбора? Чего мне не хватает? Заранее большое спасибо.

Edit: Как предполагает @egelev, я действительно мог сформулировать свой вопрос по-другому... на самом деле возврат входного параметра "как есть" будет бессмысленным. Так что настоящая проблема в том,: Что такое реальная полезность класса.метод asSubclass по сравнению с обычным литьем? Оба будут бросать ClassCastException в случае проблем с приведением.

возможно, он был добавлен, чтобы избежать непроверенного кастинга в конкретной ситуации: когда вы передаете результат метода asSubclass напрямую к другому методу, запрашивающему параметр ограниченного типа, как здесь (взято из эффективной Java, стр. 146):

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

подпись вышеуказанного метода есть:

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

мне кажется, что метод asSubclass-это только способ сделать (на самом деле!) unchecked cast без надлежащего предупреждения компилятора...

и это в конце концов повторите мой бывший вопрос: подпись

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

было бы одинаково эффективно (даже если странно, я признаю это)! Он будет полностью совместим с примером getAnnotation и не будет ограничивать клиентский код, заставляя его бессмысленно использовать подстановочный знак дженерик.

Edit2: Я думаю, что мой общий вопрос решен; большое спасибо. Если у кого-то есть другие хорошие примеры правильности подписи asSubclass, пожалуйста, добавьте их в обсуждение, я хотел бы видеть полный пример использования asSubclass с моей подписью и очевидно не работает.

3 ответов


в случае возврата типа 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.class. И не может!--11--> возвращение Class<ArrayList>, потому что lsимеет тип AbstractList а не Arraylist

Итак, компилятор теперь говорит-Ок! Я не могу вернуться Class<ArrayList> я не могу вернуться Class<AbstractList>. Но потому что я знаю!--16--> является AbstractList наверняка, поэтому фактический объект класса может быть только подтипом AbstractList. Так что Class<? extends AbstractList> - это очень безопасная ставка. Из-за wild card: Вы не может do:

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)

имеет смысл иметь ограничение времени компиляции для аргументов, которые я могу передать. Но причина, по которой я думаю, что это не было предпочтительным, была - это нарушило бы обратную совместимость кода pre-java5. Например, код, такой как ниже, который скомпилирован с pre-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;

выше всегда будет успешным, потому что дженерики стираются. Так его класса в класс. Но!--27--> дал бы false. Так что определенно бросок неправильный. Если вам нужна безопасность типа, вы можете использовать asSubClaz выше


я думаю, что для того, чтобы это понять, надо сначала понять тонкие различия между типами и классами: объекты классов, ссылки типа.

классы против типа

я думаю, что пример-это хороший способ объяснить, что я имею в виду. Допустим существование следующей иерархии классов:

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;

по моему выше объекта код класс объекта (который я оставил null на время), и эти ссылки, используемые здесь, имеют разные типы для достижения этого гипотетического объекта.

Итак, вы видите, что слово " класс "здесь используется для имени" типа ссылки", указывающего на фактический объект класса, Тип которого совместим с любой из этих ссылок.

в моем примере выше мне не удалось определить такой объект класса, так как я инициализировал исходную переменную в null. Это намеренно, и через минуту мы поймем почему.

О Вызове getClass и getSubclass ссылки

следуя примеру @Jatin, который рассматривает getClass метод, теперь рассмотрим следующий фрагмент полиморфного кода:

Mammal animal = new Tiger();

теперь мы знаем, что независимо от того, что наши!--14--> ссылка типа Mammal, фактический класс объекта, и всегда будет Tiger (т. е. класса).

что мы получим, если сделаем это?

? type = mammal.getClass()

должны type быть Class<Mammal> или Class<Tiger>?

Ну, проблема в том, что когда компилятор видит ссылка типа Mammal, и он не может сказать, что такое фактический класс объекта, на который указывает эта ссылка. Это можно было определить только на время выполнения, верно?. На самом деле это может быть млекопитающее, но это может быть любой из его подклассов, как тигр, да?

Итак, когда мы просим его класс, мы не получаем Class<Mammal> потому что компилятор не может быть уверен. Вместо этого мы получаем Class<? extends Mammal>, и это имеет гораздо больше смысла, потому что, в конце концов, компилятор знает, что на основе правил полиморфизма подтипов данная ссылка может указывать на млекопитающее или любой из его подтипов.

на данный момент, Вы, вероятно, может см. тонкие нюансы использования класса word здесь. Казалось бы, что мы на самом деле получаем из getClass() метод - это своего рода ссылка на тип, которую мы используем, чтобы указать на фактический класс исходного объекта, как мы уже объясняли ранее.

Ну, то же самое можно сказать и о asSubclass метод. Например:

Mammal tiger = new Tiger();

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

когда мы вызываем asSubclass мы получаем ссылку на тип класса, на который указывает наш ссылка, но компилятор больше не может быть уверен в том, какой должна быть эта фактическая природа, и поэтому вы получаете более слабую ссылку, такую как Class<? extends Feline>. Это самое большее, что компилятор может предположить об исходной природе объекта, и именно поэтому это все, что мы получаем.

как насчет нового Тигра ().геркласс()?

мы могли бы ожидать, что единственный способ получить Class<Tiger> (без wirldcards) должен быть доступ к исходному объекту, верно? Например:

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

забавно, однако, мы всегда достигаем объекта тигра через ссылочный тип, верно?. В Java мы не имеем прямого доступа к объектам. Поскольку мы всегда достигаем объекта через их ссылки, компилятор не может делать предположений о фактическом типе возвращаемой ссылки.

вот почему даже этот код будет производить Class<? extend Tiger>

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

то есть, компилятор не дает никаких гарантий о том, что 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
   }
}

я надеюсь, что это как-то способствует дискуссии.


идея этого метода заключается именно в том, чтобы добавить этот подстановочный знак. Насколько я понимаю, цель этого метода-бросить this объект класса объекту класса, представляющему любой подкласс параметра clazz. Именно поэтому результат объявляется как Class<? extends U> (класс чего-то, что расширяет U). В противном случае это не имело бы смысла, потому что его тип возврата будет точно таким же, как тип его единственного параметра, т. е. он не будет делать ничего, кроме return clazz;. Это делает проверку типа времени выполнения и приводит параметр к Class<? extends U> (конечно, он будет бросать ClassCastException, если цель вызова, т. е. this, не представляет класс производной U). Это лучший подход, чем простой (Class<? extends U>) тип кастинга, потому что последний заставляет компилятор выдавать предупреждение.