Когда использовать общие методы, а когда использовать подстановочные знаки?


122

Я читаю об общих методах из OracleDocGenericMethod . Меня очень смущает сравнение, когда в нем говорится, когда использовать подстановочные знаки, а когда - общие методы. Цитата из документа.

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

Вместо этого мы могли бы использовать общие методы:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

[…] Это говорит нам, что аргумент типа используется для полиморфизма; его единственный эффект - позволить использовать множество фактических типов аргументов на разных сайтах вызова. В таком случае следует использовать подстановочные знаки. Подстановочные знаки предназначены для поддержки гибкого выделения подтипов, что мы и пытаемся здесь выразить.

Разве мы не думаем, что wild card вроде (Collection<? extends E> c);тоже поддерживает своего рода полиморфизм? Тогда почему использование универсальных методов в этом случае считается нецелесообразным?

Далее говорится:

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

Что это значит?

Они представили пример

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

[...]

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

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

Документ не одобряет второе объявление и способствует использованию первого синтаксиса? В чем разница между первым и вторым объявлением? Кажется, оба делают одно и то же?

Может кто-нибудь зажечь эту зону.

Ответы:


173

Есть определенные места, где подстановочные знаки и параметры типа делают то же самое. Но есть также определенные места, где вы должны использовать параметры типа.

  1. Если вы хотите установить какое-либо отношение к различным типам аргументов метода, вы не можете сделать это с помощью подстановочных знаков, вы должны использовать параметры типа.

Берем метод в качестве примера, предположим , что вы хотите , чтобы убедиться , что srcи destсписок передается copy()методу должны быть одного параметризованного типа, вы можете сделать это с параметрами типа как так:

public static <T extends Number> void copy(List<T> dest, List<T> src)

Здесь вы можете быть уверены, что оба destи srcимеют одинаковый параметризованный тип для List. Таким образом, можно безопасно копировать элементы из srcв dest.

Но, если вы продолжите изменять метод для использования подстановочного знака:

public static void copy(List<? extends Number> dest, List<? extends Number> src)

это не будет работать должным образом. Во 2-м случае можно передать List<Integer>и List<Float>как, destи src. Таким образом, перемещение элементов из srcв destбольше не будет безопасным по типу. Если вам не нужны такие отношения, вы можете вообще не использовать параметры типа.

Еще одно различие между использованием подстановочных знаков и параметров типа:

  • Если у вас есть только один аргумент параметризованного типа, вы можете использовать подстановочный знак, хотя параметр типа также будет работать.
  • Параметры типа поддерживают несколько границ, а подстановочные знаки - нет.
  • Подстановочные знаки поддерживают как верхнюю, так и нижнюю границы, параметры типа поддерживают только верхнюю границу. Итак, если вы хотите определить метод, который принимает Listтип Integerили суперкласс, вы можете:

    public void print(List<? super Integer> list)  // OK

    но вы не можете использовать параметр типа:

     public <T super Integer> void print(List<T> list)  // Won't compile

Ссылки:


1
Странный ответ. Это не объясняет, зачем вам вообще это нужно ?. Вы можете переписать его как `public static <T1 extends Number, T2 extends Number> void copy (List <T1> dest, List <T2> src), и в этом случае станет очевидно, что происходит.
kan

@kan. Что ж, это настоящая проблема. Вы можете использовать параметр типа для принудительного применения того же типа, но вы не можете сделать это с помощью подстановочных знаков. Использование двух разных типов для параметра типа - это другое дело.
Рохит Джайн,

1
@benz. Вы не можете определить нижнюю границу в Listпараметре типа using. List<T super Integer>недействителен и не будет компилироваться.
Рохит Джайн

2
@benz. Пожалуйста :) Я настоятельно рекомендую вам пройти по ссылке, которую я разместил в конце. Это лучший ресурс по Generics, который вы могли бы получить.
Рохит Джайн

3
@ jorgen.ringen <T extends X & Y>-> несколько границ.
Rohit Jain

12

Рассмотрим следующий пример из 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». Первого метода в книге нет.


Я проголосовал за цитату книги, она прямая и лаконичная
Курапика

9

В вашем первом вопросе: это означает, что если существует связь между типом параметра и типом возвращаемого значения, используйте общий.

Например:

public <T> T giveMeMaximum(Collection<T> items);
public <T> Collection<T> applyFilter(Collection<T> items);

Здесь вы извлекаете некоторые из T по определенным критериям. Если T - Longваши методы вернут Longи Collection<Long>; фактический тип возвращаемого значения зависит от типа параметра, поэтому полезно и рекомендуется использовать универсальные типы.

Если это не так, вы можете использовать типы подстановочных знаков:

public int count(Collection<?> items);
public boolean containsDuplicate(Collection<?> items);

В этих двух примерах, независимо от типа элементов в коллекциях, будут возвращаться типы intи boolean.

В ваших примерах:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

эти две функции будут возвращать логическое значение независимо от типа элементов в коллекциях. Во втором случае это ограничено экземплярами подкласса E.

Второй вопрос:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

Этот первый код позволяет передавать List<? extends T> srcв качестве параметра неоднородный . Этот список может содержать несколько элементов разных классов, если все они расширяют базовый класс T.

если у тебя есть:

interface Fruit{}

и

class Apple implements Fruit{}
class Pear implements Fruit{}
class Tomato implements Fruit{}

ты мог бы сделать

List<? extends Fruit> basket = new ArrayList<? extends Fruit>();
basket.add(new Apple());
basket.add(new Pear());
basket.add(new Tomato());
List<Fruit> fridge = new ArrayList<Fruit>(); 

Collections.copy(fridge, basket);// works 

С другой стороны

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

ограничиваться List<S> srcодним конкретным классом S, который является подклассом T. Список может содержать только элементы одного класса (в этом экземпляре S) и никакого другого класса, даже если они также реализуют T. Вы не сможете использовать мой предыдущий пример, но можете:

List<Apple> basket = new ArrayList<Apple>();
basket.add(new Apple());
basket.add(new Apple());
basket.add(new Apple());
List<Fruit> fridge = new ArrayList<Fruit>();

Collections.copy(fridge, basket); /* works since the basket is defined as a List of apples and not a list of some fruits. */

1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();Это недопустимый синтаксис. Вы должны создать экземпляр ArrayList без ограничений.
Арнольд Писториус

Невозможно добавить яблоко в корзину в приведенном выше примере, так как корзина может быть списком груш. Некорректный пример AFAIK. И тоже не компилируется.
Khanna111 03

1
@ArnoldPistorius Это меня смутило. Я проверил документацию API ArrayList, и у нее есть подписанный конструктор ArrayList(Collection<? extends E> c). Вы можете мне объяснить, почему вы так сказали?
Kurapika

@Kurapika Может быть, я использовал старую версию Java? Комментарий был размещен почти 3 года назад.
Арнольд Писториус

2

Метод подстановочных знаков также является универсальным - вы можете вызывать его с некоторым диапазоном типов.

<T>Синтаксис определяет имя переменной типа. Если переменная типа имеет какое-либо использование (например, в реализации метода или в качестве ограничения для другого типа), тогда имеет смысл назвать ее, иначе вы можете использовать ее ?как анонимную переменную. Итак, похоже, это просто сокращение.

Более того, ?при объявлении поля нельзя избежать синтаксиса:

class NumberContainer
{
 Set<? extends Number> numbers;
}

3
Разве это не должно быть комментарием?
Buhake Sindi

@BuhakeSindi Извините, что неясно? Почему -1? Думаю, это ответ на вопрос.
kan

2

Я постараюсь ответить на ваш вопрос один за другим.

Разве мы не думаем, что wild card вроде (Collection<? extends E> c);тоже поддерживает своего рода полиморфизм?

Нет. Причина в том, что ограниченный подстановочный знак не имеет определенного типа параметра. Это неизвестно. Все, что он «знает», - это то, что «сдерживание» относится к определенному типу E(независимо от того, что определено). Таким образом, он не может проверить и обосновать, соответствует ли предоставленное значение ограниченному типу.

Таким образом, нет смысла иметь полиморфное поведение для подстановочных знаков.

Документ не одобряет второе объявление и способствует использованию первого синтаксиса? В чем разница между первым и вторым объявлением? Кажется, оба делают одно и то же?

Первый вариант в этом случае лучше, поскольку Tон всегда ограничен и sourceопределенно будет иметь значения (неизвестных), которые являются подклассами T.

Итак, предположим, что вы хотите скопировать весь список чисел, первый вариант будет

Collections.copy(List<Number> dest, List<? extends Number> src);

src, По существу, может принять List<Double>, List<Float>и т.д. , как есть верхняя граница к параметризованному типу найден в dest.

Второй вариант заставит вас выполнить привязку Sдля каждого типа, который вы хотите скопировать, например

//For double 
Collections.copy(List<Number> dest, List<Double> src); //Double extends Number.

//For int
Collections.copy(List<Number> dest, List<Integer> src); //Integer extends Number.

Как Sпараметризованный тип, требующий привязки.

Надеюсь, это поможет.


Можете ли вы объяснить, что вы имеете в виду в своем последнем абзаце
benz

Тот, который заявляет 2-й вариант, заставит вас связать его ... не могли бы вы уточнить его?
Бенц

<S extends T>утверждает, что Sэто параметризованный тип, являющийся подклассом T, поэтому для него требуется параметризованный тип (без подстановочных знаков), являющийся подклассом T.
Buhake Sindi,

2

Еще одно отличие, которое здесь не указано.

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // correct
    }
}

Но следующее приведет к ошибке времени компиляции.

static <T> void fromArrayToCollection(T[] a, Collection<?> c) {
    for (T o : a) {
        c.add(o); // compile time error
    }
}

0

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

Помимо этого, однако, подстановочные знаки служат для написания более сжатого кода, как описано следующими утверждениями в упомянутом вами документе:

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

[...]

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

[...]

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


0

В основном -> Подстановочные знаки применяют универсальные шаблоны на уровне параметра / аргумента не-универсального метода. Заметка. Это также можно выполнить в genericMethod по умолчанию, но здесь вместо? мы можем использовать сам T.

дженерики пакетов;

public class DemoWildCard {


    public static void main(String[] args) {
        DemoWildCard obj = new DemoWildCard();

        obj.display(new Person<Integer>());
        obj.display(new Person<String>());

    }

    void display(Person<?> person) {
        //allows person of Integer,String or anything
        //This cannnot be done if we use T, because in that case we have to make this method itself generic
        System.out.println(person);
    }

}

class Person<T>{

}

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.