Java: обмежені символи підстановки або параметр обмеженого типу?


83

Нещодавно я прочитав цю статтю: http://download.oracle.com/javase/tutorial/extra/generics/wildcards.html

Моє питання полягає в тому, щоб замість створення такого методу:

public void drawAll(List<? extends Shape> shapes){
    for (Shape s: shapes) {
        s.draw(this);
    }
}

Я можу створити такий метод, і він чудово працює:

public <T extends Shape> void drawAll(List<T> shapes){
    for (Shape s: shapes) {
        s.draw(this);
    }
}

Який спосіб слід використовувати? Чи корисний підстановочний знак у цьому випадку?


? - це скорочене позначення; внутрішній компілятор все одно замінює його параметром типу; коли виникає помилка компілятора, ви побачите параметр сурогатного типу, а не?
беззаперечний

9
виправлення: мій попередній коментар неправильний. уайлдкард є вишуканішим, ніж я думав.
беззаперечний

@irreputable, хоча це справді складніше, ваша думка про заміну його параметром type цілком справедлива; це лише той, який ви не можете заявити.
Євген

якщо ми подивимося лише на ці два методи як вони є - різниці немає, а лише питання стилю; інакше (якщо ви додасте ще деякі загальні параметри), все зміниться
Євген

Відповіді:


134

Це залежить від того, що вам потрібно зробити. Вам потрібно використовувати параметр обмеженого типу, якщо ви хотіли зробити щось подібне:

public <T extends Shape> void addIfPretty(List<T> shapes, T shape) {
    if (shape.isPretty()) {
       shapes.add(shape);
    }
}

Тут ми маємо a List<T> shapesта a T shape, тому можемо спокійно shapes.add(shape). Якщо це було оголошено List<? extends Shape>, ви НЕ можете безпечно addдо нього (оскільки у вас можуть бути а List<Square>та а Circle).

Отже, надаючи ім’я параметру обмеженого типу, ми маємо можливість використовувати його в іншому місці нашого загального методу. Звичайно, ця інформація потрібна не завжди, тому, якщо вам не потрібно знати стільки про тип (наприклад, ваш drawAll), тоді достатньо лише символів підстановки.

Навіть якщо ви знову не звертаєтеся до параметра обмеженого типу, параметр обмеженого типу все одно необхідний, якщо у вас кілька меж. Ось цитата із поширених запитань про Java Generics Анжеліки Лангер

У чому різниця між зв’язаними символами підстановки та параметрами типу?

Узагальнюючий знак може мати лише одну межу, тоді як параметр типу може мати кілька меж. Узагальнюючий знак може мати нижню або верхню межу, тоді як для параметра типу не існує поняття нижньої межі.

Межі підстановки та типи параметрів часто плутають, оскільки вони обидва називаються межами і мають частково подібний синтаксис. […]

Синтаксис :

  type parameter bound     T extends Class & Interface1 & … & InterfaceN

  wildcard bound  
      upper bound          ? extends SuperType
      lower bound          ? super   SubType

Узагальнюючий знак може мати лише одну межу, або нижню, або верхню. Список обмежених знаків не допускається.

Параметр типу, у constrast, може мати кілька меж, але для поняття типу не існує такого поняття, як нижня межа.

Цитати з Effective Java 2nd Edition, пункт 28: Використовуйте обмежені символи підстановки для збільшення гнучкості API :

Для максимальної гнучкості використовуйте типи підстановок на вхідних параметрах, що представляють виробників або споживачів. […] PECS означає «виробник» extends, «споживач» super[…]

Не використовуйте типи підстановок як типи повернення . Замість того, щоб забезпечити додаткову гнучкість для ваших користувачів, це змусить їх використовувати підстановочні типи в коді клієнта. При правильному використанні типи підстановок майже не видно для користувачів класу. Вони змушують методи приймати параметри, які вони повинні прийняти, та відкидати ті, які вони повинні відкидати. Якщо користувачеві класу доводиться думати про типи підстановок, можливо, щось не так з API класу .

Застосовуючи принцип PECS, тепер ми можемо повернутися до нашого addIfPrettyприкладу та зробити його більш гнучким, написавши наступне:

public <T extends Shape> void addIfPretty(List<? super T> list, T shape) { … }

Тепер ми можемо addIfPretty, скажімо, a Circle, a List<Object>. Це, очевидно, безпечно для типу, і все ж наша оригінальна декларація була недостатньо гнучкою, щоб дозволити це.

Пов’язані запитання


Резюме

  • Використовуйте обмежені параметри типу / узагальнюючі символи, вони збільшують гнучкість вашого API
  • Якщо тип вимагає декількох параметрів, вам не залишається нічого іншого, як використовувати параметр обмеженого типу
  • якщо тип вимагає нижньої межі, вам не залишається нічого іншого, як використовувати обмежений підстановочний знак
  • "Виробники" мають нижчі межі, "споживачі" - нижчі
  • Не використовуйте символи підстановки у типах повернення

Щиро дякую, ваше пояснення дуже чітке та повчальне
Тоні Ле

1
@Tony: У книзі також є коротка дискусія про те, як вибрати між символом підстановки та типом, коли немає обмежень. По суті, якщо параметр типу з’являється лише один раз у декларації методу, використовуйте підстановочний знак. Див. Також reverse(List<?>)приклад із JLS java.sun.com/docs/books/jls/third_edition/html/…
polygenelubricants

Я отримую UnsupportedOperationException для наступного коду, <code> public static <T extends Number> void add (List <? Super T> list, T num) {list.add (num); } </code>
Самра

1
Крім того - ви не можете передавати ?як параметр методу, наприклад, doWork(? type)оскільки тоді ви не можете використовувати цей параметр у методі. Вам потрібно використовувати параметр типу.
Томаш Муларчик

1
@VivekVardhan, ви не можете створити такий метод, використовуючи символи підстановки, в цьому полягає суть. Вирішуючи, яким користуватися, це одне з найкращих пояснень.
Євген

6

У вашому прикладі вам насправді не потрібно використовувати T, оскільки ви більше ніде не використовуєте цей тип.

Але якщо ви зробили щось на зразок:

public <T extends Shape> T drawFirstAndReturnIt(List<T> shapes){
    T s = shapes.get(0);
    s.draw(this);
    return s;
}

або як сказали полігенмастильні матеріали, якщо ви хочете, щоб параметр типу у списку відповідав іншому параметру типу:

public <T extends Shape> void mergeThenDraw(List<T> shapes1, List<T> shapes2) {
    List<T> mergedList = new ArrayList<T>();
    mergedList.addAll(shapes1);
    mergedList.addAll(shapes2);
    for (Shape s: mergedList) {
        s.draw(this);
    }
}

У першому прикладі ви отримуєте трохи більшу безпеку типу, ніж повертаєте лише Shape, оскільки потім ви можете передати результат функції, яка може прийняти дочірній елемент Shape. Наприклад, ви можете передати a List<Square>моєму методу, а потім передати отриманий квадрат методу, який приймає лише квадрати. Якщо ви використовували "?" вам доведеться передати отриману фігуру в квадрат, що не буде безпечним для друку.

У другому прикладі ви гарантуєте, що обидва списки мають однаковий параметр типу (чого не можна робити з "?", Оскільки кожен "?" Відрізняється), так що ви можете створити список, що містить усі елементи з обох .


Я вважаю, що це Shape s = ...повинно бути T s = ..., інакше return s;не слід компілювати.
полігенмастильні матеріали

1

Розглянемо наступний приклад із програмування Java Джеймса Гослінга, четверте видання нижче, де ми хочемо об’єднати 2 SinglyLinkQueue:

public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

Обидва вищезазначені методи мають однакову функціональність. То що є кращим? Відповідь друга. За словами власного автора:

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

Примітка: У книзі подано лише другий метод, а ім'я параметра параметра - S замість 'T'. Першого методу в книзі немає.


1

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

У посиланні, яке ви вказуєте, я прочитав (у розділі "Загальні методи") такі твердження, які натякають у цьому напрямку:

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

[...]

Використання узагальнюючих знаків зрозуміліше і стисліше, ніж декларування явних параметрів типу, і тому слід надавати перевагу, коли це можливо.

[...]

Узагальнюючі символи також мають ту перевагу, що їх можна використовувати поза підписами методів, як типи полів, локальні змінні та масиви.


0

Другий спосіб - трохи більш детальний, але він дозволяє посилатися Tвсередині нього:

for (T shape : shapes) {
    ...
}

Це єдина різниця, наскільки я розумію.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.