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, хотя это действительно более сложно, ваша точка зрения о замене его параметром типа полностью верна; это просто то, что вы не можете объявить.
Евгений
если мы посмотрим только на эти два метода такими, какие они есть - нет никакой разницы, это просто вопрос стиля; в противном случае (если вы добавите еще несколько общих параметров) все изменится
Евгений

Ответы:

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перейти к нему (потому что у вас могут быть a List<Square>и a Circle).

Таким образом, давая имя параметру ограниченного типа, у нас есть возможность использовать его в другом месте нашего универсального метода. Конечно, эта информация не всегда требуется, поэтому, если вам не нужно так много знать о типе (например, вашdrawAll ), тогда достаточно подстановочного знака.

Даже если вы снова не ссылаетесь на параметр ограниченного типа, параметр ограниченного типа по-прежнему требуется, если у вас есть несколько границ. Вот цитата из FAQs Анжелики Лангер по Java Generics

В чем разница между привязкой подстановочного знака и привязкой параметра типа?

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

Границы подстановочных знаков и границы параметров типа часто путают, потому что они оба называются границами и имеют частично похожий синтаксис. […]

Синтаксис :

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

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

Подстановочный знак может иметь только одну границу: нижнюю или верхнюю. Список границ подстановочных знаков не разрешен.

Параметр типа в качестве ограничения может иметь несколько границ, но не существует такой вещи, как нижняя граница для параметра типа.

Цитаты из 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)потому что тогда вы не можете использовать этот параметр в методе. Вам нужно использовать параметр типа.
Tomasz Mularczyk
1
@VivekVardhan, вы не можете создать такой метод с использованием подстановочных знаков, в этом вся суть. При выборе того, что использовать, это один из лучших источников, объясняющих.
Евгений
6

В вашем примере вам действительно не нужно использовать T, поскольку вы не используете этот тип где-либо еще.

Но если вы сделали что-то вроде:

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

или, как сказал polygenlubricants, если вы хотите сопоставить параметр типа в списке с другим параметром типа:

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. Например, вы можете передать List<Square>мой метод, а затем передать полученный Square методу, который принимает только Squares. Если вы использовали '?' вам придется преобразовать полученную форму в Square, что не будет типобезопасным.

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

Андрей Фирбинтяну
источник
Я считаю, что Shape s = ...должен быть T s = ..., иначе return s;не должен компилироваться.
polygenelubricants
1

Рассмотрим следующий пример из 4-го издания The Java Programming by James Gosling ниже, где мы хотим объединить 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) {
    ...
}

Это единственная разница, насколько я понимаю.

Никита Рыбак
источник