Коли для дженерики 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? Карта Stringдля Class<Date>загалом не здається страшно корисною (все, що вона вміє, - це Date.classяк значення, а не екземпляри Date)

Що стосується генеризації assertThat, то ідея полягає в тому, що метод може забезпечити передачу даних , Matcherщо відповідає типу результату.


У цьому випадку, так, я хочу карту занять. Приклад, який я наводив, призначений для використання стандартних класів JDK, а не моїх спеціальних класів, але в цьому випадку клас фактично створюється за допомогою відображення та використовується на основі ключа. (Розподілене додаток, де клієнт не має класів сервера, просто ключ, який клас використовувати для роботи на сервері).
Ісіхай

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

Що ж стосується того, щоб переконатися, що ролях виконується для вас, метод matcher.matches () не хвилює, тому оскільки T ніколи не використовується, навіщо його включати? (тип повернення методу недійсний)
Ісіхай

А-а-а - це те, що я отримую за те, що не читаю def твердження, що досить близько. Схоже, це лише для того, щоб придатний Матч був переданий у ...
Скотт Стенчфілд

28

Дякую всім, хто відповів на питання, це дійсно допомогло з’ясувати речі для мене. Врешті-решт відповідь Скотта Стенчфілда наблизилась до того, як я в кінцевому підсумку зрозумів це, але оскільки я не зрозумів його, коли він вперше написав це, я намагаюся відновити проблему, щоб, сподіваюся, хтось інший отримав користь.

Я збираюся повторно поставити запитання щодо списку, оскільки він має лише один загальний параметр, і це полегшить розуміння.

Мета параметризованого класу (наприклад, Список <Date>або Карта, <K, V>як у прикладі) - примусити знижувати обвал та надання компілятором гарантії, що це безпечно (без винятку з виконання).

Розглянемо випадок Списку. Суть мого питання полягає в тому, чому метод, який приймає тип T і List, не приймає Перелік чогось далі за ланцюгом успадкування, ніж Т. Розгляньте цей надуманий приклад:

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);
}

Це не буде компілюватися, оскільки параметр list - це список дат, а не список рядків. Дженерики були б не дуже корисними, якби це було складено.

Те саме стосується Карти. <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. Намір полягає в тому, щоб статично переконатися, що збірник відповідає фактичному об'єкту, і немає жодного способу виключити Об'єкт із цього обчислення.

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

EDIT (після подальшого розгляду та досвіду):

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


Я все ще не розумію, чому я не можу зітхнути. Чому я не можу перетворити список дат у перелік серіалізаційних?
Thomas Ahle

@ThomasAhle, оскільки тоді посилання, які вважають, що це список Дат, зіткнуться з помилками кастингу, коли вони знайдуть рядки або будь-які інші серіалізатори.
Yishai

Я бачу, але що робити, якщо я якось позбувся старого посилання, як би я повернув List<Date>метод із типом List<Object>? Це має бути безпечним правом, навіть якщо Java не дозволяє.
Thomas Ahle

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. Це не безпечно, тому компілятор відключає це.

Однак, якщо ми введемо якийсь інший об'єкт, скажімо, Список (у вашому прикладі цей об'єкт є Матчер), то наступне стане істинним:

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

Так, це трохи незручно; не знаєте, як це краще виразити ... чи має сенс сказати ""? " це невідомий тип, не тип, який нічого відповідає? "
erickson

1
Що може допомогти пояснити, це те, що для "будь-якого класу, що розширює Serializable", ви можете просто використовувати<Serializable>
c0der

8

Один із способів мені зрозуміти макіяж - це думати, що підстановочний код не вказує тип можливих об'єктів, на які може бути "загальна посилання", а тип інших загальних посилань, з якими він сумісний (це може здатися заплутаним) ...) Як така, перша відповідь є дуже оманливою в її формулюванні.

Іншими словами, List<? extends Serializable>означає, що ви можете призначити це посилання іншим Спискам, де тип - якийсь невідомий тип, який є або підкласом Serializable. НЕ думайте про це з точки зору того, що ЄДИННИЙ СПИСОК зможе містити підкласи Serializable (адже це неправильна семантика і призводить до нерозуміння Generics).


Це, безумовно, допомагає, але "може звучати заплутано" начебто замінюється на "звучить заплутано". Як підсумок, то чому, відповідно до цього пояснення, складається метод з Matcher <? extends T>?
Ісіхай

якщо ми визначимо як Список <Serializable>, він робить те саме, що правильно? Я говорю про ваш другий пункт. тобто поліморфізм би впорався з цим?
Supun Wijerathne

3

Я знаю, що це давнє запитання, але я хочу поділитися прикладом, який, на мою думку, досить добре пояснює обмежені символи. java.util.Collectionsпропонує цей метод:

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

Якщо у нас є Список T, Список, звичайно, може містити екземпляри типів, які розширюються T. Якщо у списку містяться тварини, у списку можуть бути як собаки, так і кішки (обидві тварини). Собаки мають властивість "woofVolume", а коти мають властивість "meowVolume". Хоча ми можемо хотіти сортувати на основі цих властивостей, зокрема, підкласи T, як ми можемо розраховувати, що цей спосіб це зробить? Обмеження компаратора в тому, що він може порівнювати лише дві речі лише одного типу ( 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.