Подтвердить что ты не робот

Подпись 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):

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

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

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

MAYBE он был добавлен во избежание неконтролируемого кастинга в конкретной ситуации: когда вы передаете результат метода 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 с моей подписью и, по-видимому, не работать.

4b9b3361

Ответ 1

В случае типа возврата 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. Итак, 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)

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

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

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

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

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

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

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

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

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

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

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

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

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

Mammal animal = new Tiger();

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

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

? type = mammal.getClass()

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

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

Итак, когда мы запрашиваем его класс, мы не получаем Class<Mammal>, потому что компилятор не может быть уверен. Вместо этого мы получаем a Class<? extends 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>. Это самый компилятор, который может предположить об исходном характере объекта, и почему это все, что мы получаем.

Что нового Tiger(). gerClass()?

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

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

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

Вот почему даже этот код создавал бы Class<? extend Tiger>

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

То есть, компилятор не дает никаких гарантий относительно того, что оператор new может вернуться сюда. Для всего, что имеет значение, он может вернуть объект, совместимый с Tiger, но не необходимый, класс которого является самим тигром.

Это становится яснее, если вы изменили оператор new для метода factory.

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

И наш тигр factory:

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

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

Ответ 3

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