Подпись Class.asSubclass

мой вопрос довольно теоретический ... Это подпись 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 напрямую другому методу, запрашивающему параметр ограниченного типа, как здесь (взято из Эффективной 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 с моей подписью, который очевидно не работает.


person CptWasp    schedule 19.02.2015    source источник
comment
Вопрос аналогичный, но не дубликат! stackoverflow.com/ questions / 20661835 /   -  person christopher    schedule 19.02.2015


Ответы (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. Также нельзя ls.getClass вернуть Class<ArrayList>, потому что ls имеет тип AbstractList ‹>, а не Arraylist.

Итак, компилятор теперь говорит - Хорошо! Я не могу ни вернуть Class<ArrayList>, ни Class<AbstractList>. Но поскольку я точно знаю, что ls является AbstractList, поэтому фактический объект класса может быть только подтипом 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. Например, приведенный ниже код, скомпилированный с использованием до-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, а не cast? - Потому что кастинг - это предательство. Например:

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

Вышеупомянутое всегда будет успешным, потому что дженерики стираются. Итак, его класс преобразован в класс. Но aClass.equals(ArrayList.class) даст ложь. Так что бросок определенно неправильный. Если вам нужна безопасность типов, вы можете использовать asSubClaz выше

person Jatin    schedule 19.02.2015
comment
@CptWasp Проверьте PS, почему asSubClass. Я надеюсь, что ответ прояснил ваши вопросы. - person Jatin; 19.02.2015
comment
Я до сих пор не понимаю, почему это действительно безопасно. На практике вы можете получить ClassCastException точно так же, как при ручном приведении ... Я понимаю, что вы получаете <? extends U>, но для меня это то же самое, что ‹U› для клиентского кода, и полученный код не загроможден подстановочный знак. - person CptWasp; 19.02.2015
comment
Мне очень жаль, но вы все еще не получили моего ответа. Возвращаться небезопасно ‹U›. Следовательно, компилятор никоим образом не может безопасно вернуть просто ‹U›. Для обеспечения безопасности типов прочитайте вторую часть моего ответа, это не полностью типизировано по причинам обратной совместимости. Но достаточно, чтобы убрать половину ошибок. - person Jatin; 19.02.2015
comment
Его нельзя сделать типизированным по причине обратной совместимости ** - person Jatin; 19.02.2015
comment
Я понимаю, что он имитирует метод getClass (и я понимаю особую причину подписи getClass); и я следую вашим рассуждениям до комментария BIG ISSUE. Метод getClass по контракту должен возвращать тип среды выполнения объекта, и его поведение является наиболее точным в данной ситуации; но какова реальная цель сужения типа при присвоении его новой переменной? Я всегда могу безопасно использовать: Class<AbstractList> bClass= (Class<AbstractList>)aClass; ... он не отмечен, но всегда должен работать. Я не прав? - person CptWasp; 19.02.2015
comment
Следующий код работает: List<String> ls = new ArrayList<String>(); Class<? extends List> x = ls.getClass(); @SuppressWarnings("unchecked") Class<AbstractList> y= (Class<AbstractList>)x; System.out.println("OK"); Я понимаю, что упускаю вашу точку зрения ... помогите, пожалуйста :) - person CptWasp; 19.02.2015
comment
Позвольте нам продолжить это обсуждение в чате. - person Jatin; 20.02.2015
comment
Метод asSubclass (...) был добавлен в Java 5. Не должно быть никакого кода до Java 5, которому нужен упрощенный вариант ‹U› вместо ‹U extends T› по причинам обратной совместимости. Должна быть другая причина. - person bjmi; 07.07.2018

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

Классы и типы

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

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

Это означает, что фактический класс объекта фиксирован, это тот класс, который мы использовали для его создания, когда использовали оператор «новый» в нашем коде выше.

С другой стороны, ссылки могут иметь разные типы, полиморфно совместимые с фактическим классом объекта (то есть с тигром в данном случае).

Итак, у объектов есть фиксированный класс, тогда как у ссылок есть совместимые типы.

Это может сбивать с толку, поскольку имена классов - это то же самое, что мы используем для именования типов наших ссылок, но семантически говоря, есть тонкая разница, как мы можем видеть выше.

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

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

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

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

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

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

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

Mammal animal = new Tiger();

Теперь мы знаем, что независимо от того, что наша ссылка animal имеет тип Mammal, фактическим классом объекта является и всегда будет Tiger (т. Е. Класс).

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

? type = mammal.getClass()

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

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

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

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

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

Mammal tiger = new Tiger();

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

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

А как насчет new 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();

И наша фабрика Tiger:

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

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

person Edwin Dalorzo    schedule 19.02.2015
comment
Спасибо, @Edwin Dalorzo, очень ясно. Мне теперь совершенно ясно, что является причиной возвращаемого типа части метода asSubclass. Теперь мы получаем Class<? extends Tiger>. Мы точно можем преобразовать его в Class<Tiger>, верно? Что ж, я не понимаю, почему компилятор не может распознать этот простой шаблон и вместо этого продолжает давать непроверенное предупреждение. Существуют ли ситуации, когда мое общее предположение неверно? - person CptWasp; 20.02.2015

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

person egelev    schedule 19.02.2015
comment
Спасибо, я согласен с тем, что на самом деле, используя мои рассуждения, вы могли бы просто проигнорировать метод asSubclass и использовать обычное ручное (не отмеченное) приведение для преобразования Class ‹?› В определенный тип ... Вам не нужен метод для Сделай так. Но какова должна быть цель сужения типа до «Class‹ »? расширяет U ›'? Я этого не понимаю. - person CptWasp; 19.02.2015