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

С учетом следующего примера (с использованием JUnit с сопоставителями Hamcrest):

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

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

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 вызывает метод match, который не типизирован никаким универсальным, и просто выглядит как попытка заставить безопасность типов, которая ничего не делает, как 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());
    }
}

Ответы [ 7 ]

126 голосов
/ 22 мая 2009

Сначала - я должен направить вас к 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? Карта от String до Class<Date> в целом не выглядит ужасно полезной (все, что она может содержать, это Date.class в качестве значений, а не экземпляры Date)

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

24 голосов
/ 24 мая 2009

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

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

14 голосов
/ 22 мая 2009

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

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, поскольку нет гарантии, что «неизвестный» Класс был Дата. Это не типично безопасно, поэтому компилятор запрещает это.

Однако, если мы представим какой-нибудь другой объект, скажем 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).

8 голосов
/ 22 мая 2009

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

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

7 голосов
/ 22 мая 2009

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

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

3 голосов
/ 23 января 2016

Я знаю, что это старый вопрос, но я хочу поделиться примером, который, я думаю, довольно хорошо объясняет ограниченные подстановочные знаки. 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. Поэтому он позволяет нам использовать Comparator T или любой суперкласс T, т.е. ? super T.

1 голос
/ 22 мая 2009

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

Map<String, ? extends Class<? extends Serializable>> expected = null;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...