Java: ограниченные символы подстановки или параметр ограниченного типа? - PullRequest
67 голосов
/ 15 августа 2010

Недавно я прочитал эту статью: 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);
    }
}

Какой способ использовать?Полезен ли подстановочный знак в этом случае?

Ответы [ 5 ]

110 голосов
/ 15 августа 2010

Это зависит от того, что вам нужно , чтобы сделать. Вам нужно использовать параметр ограниченного типа, если вы хотите сделать что-то вроде этого:

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

Здесь у нас есть List<T> shapes и T shape, поэтому мы можем безопасно shapes.add(shape). Если он был объявлен List<? extends Shape>, вы можете НЕ безопасно add к нему (потому что у вас может быть List<Square> и Circle).

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

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

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

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

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

Синтаксис

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

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

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

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

Цитаты из Effective Java 2nd Edition, Item 28. Использование ограниченных подстановочных знаков для повышения гибкости API :

Для максимальной гибкости используйте подстановочные типы для входных параметров, которые представляют производителей или потребителей. […] PECS обозначает производителя- extends, потребителя- super […]

Не используйте подстановочные типы в качестве типов возврата . Вместо того, чтобы предоставлять дополнительную гибкость вашим пользователям, это заставит их использовать подстановочные знаки в клиентском коде. При правильном использовании подстановочные типы почти невидимы для пользователей класса. Они заставляют методы принимать параметры, которые они должны принять, и отклоняют те, которые они должны отклонять. Если пользователь класса должен думать о типах подстановочных знаков, вероятно, что-то не так с API класса .

Применяя принцип PECS, теперь мы можем вернуться к нашему addIfPretty примеру и сделать его более гибким, написав следующее:

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

Теперь мы можем addIfPretty, скажем, от Circle до List<Object>. Это, очевидно, безопасно для типов, и все же наше первоначальное объявление не было достаточно гибким, чтобы это разрешить.

Похожие вопросы


Резюме

  • Используйте параметры / подстановочные знаки ограниченного типа, они повышают гибкость вашего API
  • Если для типа требуется несколько параметров, у вас нет другого выбора, кроме как использовать параметр ограниченного типа
  • если тип требует нижнего ограничения, у вас нет другого выбора, кроме как использовать ограниченный подстановочный знак
  • "Производители" имеют верхние границы, "потребители" имеют нижние границы
  • Не использовать подстановочный знак в типах возврата
5 голосов
/ 15 августа 2010

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

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

1 голос
/ 15 марта 2018

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

В указанной вами ссылке я прочитал (в разделе «Общие методы») следующие утверждения, которые намекают в этом направлении:

Универсальные методы позволяют использовать параметры типа для выражения зависимости между типами одного или нескольких аргументов метода и / или его тип возврата. Если нет такой зависимости, общий метод не должен использоваться.

[...]

Использование подстановочных знаков более понятно и кратко, чем объявление явного введите параметры, и поэтому должны быть предпочтительны, когда это возможно.

[...]

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

1 голос
/ 19 июля 2015

Рассмотрим следующий пример из «Программирование Java» Джеймса Гослинга, 4-е издание, ниже, где мы хотим объединить 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
}

Оба вышеприведенных метода имеют одинаковую функциональность.Так что предпочтительнее?Ответ 2-й.По словам автора:

"Общее правило заключается в использовании подстановочных знаков, когда это возможно, поскольку код с подстановочными знаками обычно более читабелен, чем код с несколькими параметрами типа. При принятии решения о необходимости использования переменной типаспросите себя, используется ли эта переменная типа для связи двух или более параметров или для связи типа параметра с типом возвращаемого значения. Если ответ «нет», то подстановочного знака должно быть достаточно. »

Примечание:В книге дан только второй метод и имя параметра типа S вместо «T».Первого метода нет в книге.

0 голосов
/ 15 августа 2010

Второй способ немного более многословен, но он позволяет ссылаться на T внутри него:

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

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

...