Когда использовать общие методы и когда использовать wild-card?

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

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!
}

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

Разве мы не считаем, что такая дикая карта, как (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) {
    ...
}

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

Может ли кто-нибудь поставить свет на эту область.

Ответ 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)

он не будет работать должным образом. Во втором случае вы можете передать 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
    

Литература:

Ответ 2

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

Например:

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) {
    ...
}

constrain 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. */

Ответ 3

Подстановочный метод также является общим - вы можете называть его некоторым диапазоном типов.

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

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

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

Ответ 4

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

Разве мы не считаем, что такая дикая карта, как (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.

As S - это параметризованный тип, который требует привязки.

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

Ответ 5

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

Ответ 6

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

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