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

Учитывая следующий пример (используя 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)

Затем компиляция работает.

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

  • Почему точно не компилируется текущая версия? Хотя я смутно понимаю проблемы ковариации здесь, я, конечно, не мог объяснить это, если бы мне пришлось.
  • Есть ли недостаток в изменении метода assertThat на Matcher<? extends T>? Существуют ли другие случаи, которые могут сломаться, если вы это сделали?
  • Есть ли какая-либо точка для обобщения метода 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());
    }
}

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

То же самое относится к Map <String, Class<? extends Serializable>> Это не то же самое, что Map <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. Цель состоит в том, чтобы статически убедиться, что совпадение соответствует фактическому объекту, и нет способа исключить объект из этого вычисления.

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

EDIT (после дальнейшего созерцания и опыта):

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

Ответ 3

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

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

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

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

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).

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

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

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

Ответ 7

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

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