Когда для дженериков Java требуется <? расширяет T> вместо <T> и есть ли недостатки в переключении?


205

Приведенный ниже пример (с использованием JUnit с сопоставителями Hamcrest):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

Это не компилируется с assertThatсигнатурой метода JUnit :

public static <T> void assertThat(T actual, Matcher<T> matcher)

Сообщение об ошибке компилятора:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

Однако, если я изменю assertThatсигнатуру метода на:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Тогда компиляция работает.

Итак, три вопроса:

  1. Почему именно текущая версия не компилируется? Хотя я смутно понимаю проблемы ковариации здесь, я, конечно, не мог бы объяснить это, если бы мне пришлось.
  2. Есть ли недостаток в изменении assertThatметода на Matcher<? extends T>? Есть ли другие случаи, которые сломались бы, если бы вы сделали это?
  3. Есть ли смысл обобщать assertThatметод в JUnit? MatcherКласс , кажется, не требует, поскольку JUnit вызывает метод спичек, который не напечатанный с любыми родовыми, и просто выглядит как попытка заставить типа безопасности , который ничего не делает, так как Matcherпросто не будет на самом деле совпадение, и тест не пройден независимо. Небезопасные операции не выполняются (или так кажется).

Для справки, вот реализация JUnit assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}

эта ссылка очень полезна (дженерики, наследование и подтип): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Дариуш Джафари

Ответы:


145

Во-первых, я должен направить вас на http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - она ​​делает потрясающую работу.

Основная идея заключается в том, что вы используете

<T extends SomeClass>

когда фактический параметр может быть SomeClassили любой его подтип.

В вашем примере

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

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

Когда вы передаете в результате вы устанавливаете Tточно Mapиз Stringк Dateобъектам класса, которые не соответствуют Mapот Stringни к чему , что это Serializable.

Одно нужно проверить - ты уверен, что хочешь, Class<Date>а нет Date? Карта Stringto Class<Date>не звучит ужасно полезной в целом (все, что она может содержать, это Date.classзначения, а не экземпляры Date)

Что касается обобщения assertThat, идея заключается в том, что метод может гарантировать, Matcherчто передается a, который соответствует типу результата.


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

6
Я думаю, что мой мозг застрял на том, почему карта, содержащая классы типа Date, не просто хорошо вписывается в тип карт, содержащих классы типа Serializable. Конечно, классы типа Serializable могут быть и другими классами, но он, безусловно, включает тип Date.
Ишай

На assertT, который гарантирует, что приведение выполняется для вас, метод matcher.matches () не заботится, поэтому, так как T никогда не используется, зачем его включать? (метод возврата типа void)
Ишай

Аааа - это то, что я получаю за то, что не прочитал определение утверждения достаточно близко. Похоже, это только для того, чтобы убедиться, что подходящий Matcher передан в ...
Скотт Стэнчфилд

28

Спасибо всем, кто ответил на вопрос, это действительно помогло мне прояснить ситуацию. В конце концов ответ Скотта Стэнчфилда ближе всего подошел к тому, как я это понял, но так как я не понял его, когда он впервые написал это, я пытаюсь сформулировать проблему так, чтобы, надеюсь, кто-то другой выиграл.

Я собираюсь переформулировать вопрос в терминах List, поскольку он имеет только один общий параметр, и это облегчит его понимание.

Назначение параметризованного класса (такого как List <Date>или Map, <K, V>как в примере) состоит в том, чтобы вызвать downcast и иметь гарантию компилятора, что это безопасно (без исключений времени выполнения).

Рассмотрим случай List. Суть моего вопроса заключается в том, почему метод, который принимает тип T и List, не будет принимать список чего-то еще ниже по цепочке наследования, чем T. Рассмотрим этот надуманный пример:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

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

То же самое относится и к карте. <String, Class<? extends Serializable>>Это не то же самое, что карта <String, Class<java.util.Date>>. Они не являются ковариантными, поэтому, если я хотел взять значение из карты, содержащей классы дат, и поместить его в карту, содержащую сериализуемые элементы, это нормально, но сигнатура метода, которая говорит:

private <T> void genericAdd(T value, List<T> list)

Хочет иметь возможность сделать оба:

T x = list.get(0);

и

list.add(value);

В этом случае, даже если метод junit на самом деле не заботится об этих вещах, сигнатура метода требует ковариации, которую он не получает, поэтому он не компилируется.

По второму вопросу

Matcher<? extends T>

Недостатком было бы действительно принимать что-либо, когда T - это объект, который не является целью API. Цель состоит в том, чтобы статически убедиться, что сопоставитель соответствует фактическому объекту, и нет никакого способа исключить Object из этого вычисления.

Ответ на третий вопрос заключается в том, что ничего не будет потеряно с точки зрения неконтролируемой функциональности (не было бы небезопасного встраивания типов в JUnit API, если бы этот метод не был обобщен), но они пытаются выполнить что-то другое - статически убедитесь, что два параметра могут совпадать.

РЕДАКТИРОВАТЬ (после дальнейшего созерцания и опыта):

Одна из больших проблем с сигнатурой метода assertThat - попытки приравнять переменную T к универсальному параметру T. Это не работает, потому что они не являются ковариантными. Так, например, у вас может быть буква T, которая представляет собой, List<String>но затем передает совпадение, с которым работает компилятор Matcher<ArrayList<T>>. Теперь, если бы это не был параметр типа, все было бы хорошо, потому что List и ArrayList ковариантны, но, поскольку Generics, с точки зрения компилятора, требует ArrayList, он не может допустить List по причинам, которые, я надеюсь, понятны из вышесказанного.


Я до сих пор не понимаю, почему я не могу выскочить. Почему я не могу превратить список дат в список сериализуемых?
Томас Ахл

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

Я вижу, но что, если я каким-то образом избавился от старой ссылки, как если бы я возвратил метод List<Date>from с типом List<Object>? Это должно быть правильно, даже если это не разрешено Java.
Томас Ахл

14

Это сводится к:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

Вы можете видеть, что ссылка на класс c1 может содержать экземпляр Long (поскольку базовый объект в данное время мог бы быть List<Long>), но, очевидно, не может быть приведена к Date, поскольку нет никакой гарантии, что «неизвестным» классом был Date. Это не типично безопасно, поэтому компилятор запрещает это.

Однако, если мы представим какой-нибудь другой объект, скажем List (в вашем примере этот объект является Matcher), то следующее становится истинным:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... Однако, если тип списка становится? расширяет T вместо T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

Я думаю, что изменяя Matcher<T> to Matcher<? extends T>, вы в основном вводите сценарий, похожий на назначение l1 = l2;

Все еще очень запутанно иметь вложенные подстановочные знаки, но, надеюсь, это имеет смысл, поскольку это помогает понять обобщенные элементы, глядя на то, как вы можете назначать обобщенные ссылки друг на друга. Это также еще более сбивает с толку, поскольку компилятор выводит тип T при выполнении вызова функции (вы явно не говорите, что это был T).


9

Причина, по которой исходный код не компилируется, заключается в том, что <? extends Serializable>это не означает «любой класс, расширяющий Serializable», но «некоторый неизвестный, но определенный класс, расширяющий Serializable».

Например, если код , как написано, это вполне допустимо назначать new TreeMap<String, Long.class>()>в expected. Если бы компилятор позволил скомпилировать код, assertThat()он, вероятно, сломался бы, потому что ожидал бы Dateобъекты вместо Longобъектов, найденных на карте.


1
Я не совсем следую - когда вы говорите «не значит ... но ...»: какая разница? (например, что может быть примером «известного, но неспецифического» класса, который соответствует первому определению, но не последнему?)
poundifdef

Да, это немного неловко; не знаю, как это лучше выразить ... имеет ли смысл говорить «?» это тип, который неизвестен, а не тип, который соответствует чему-либо? "
Эриксон

1
Это может помочь объяснить, что для «любого класса, расширяющего Serializable», вы можете просто использовать<Serializable>
c0der

8

Один из способов понять подстановочные знаки - это подумать, что подстановочный знак не указывает тип возможных объектов, которые может иметь данная ссылка, а тип других общих ссылок, с которыми он совместим (это может показаться странным ...) Таким образом, первый ответ вводит в заблуждение.

Другими словами, List<? extends Serializable>означает, что вы можете назначить эту ссылку другим спискам, где типом является некоторый неизвестный тип, который является или подклассом Serializable. НЕ думайте об этом с точки зрения того, что ЕДИНЫЙ СПИСОК может содержать подклассы Serializable (потому что это неверная семантика и приводит к неправильному пониманию Generics).


Это, безусловно, помогает, но слово «может показаться странным» заменено словами «звучит странно». В качестве продолжения, почему, согласно этому объяснению, метод с Matcher <? extends T>компилируется?
Ишай

если мы определим как List <Serializable>, он делает то же самое, верно? Я говорю о вашем втором пункте. т.е. полиморфизм справился бы с этим?
Супун Вийератне

3

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

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

Если у нас есть список T, список, конечно, может содержать экземпляры типов, которые расширяются T. Если список содержит животных, список может содержать как собак, так и кошек (оба животных). Собаки имеют свойство "woofVolume", а кошки имеют свойство "meowVolume". В то время как мы могли бы сортировать, основываясь на этих свойствах, относящихся к подклассам T, как мы можем ожидать, что этот метод сделает это? Ограничением Comparator является то, что он может сравнивать только две вещи только одного типа ( T). Таким образом, требование просто Comparator<T>сделало бы этот метод пригодным для использования. Но создатель этого метода признал, что если что-то является T, то это также экземпляр суперклассов T. Таким образом, он позволяет использовать компаратор Tили любой суперкласс T, то есть ? super T.


1

Что делать, если вы используете

Map<String, ? extends Class<? extends Serializable>> expected = null;

Да, это как раз то, к чему стремился мой ответ выше.
GreenieMeanie

Нет, это не поможет ситуации, по крайней мере, так, как я пытался.
Ишай
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.