Когда использовать универсальные методы, а когда использовать подстановочную карту?
Я читаю об универсальных методах из 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!
}[…]
Это говорит нам о том, что аргумент типа используется для полиморфизма;
его единственный эффект заключается в том, что допускается использование множества реальных типов аргументов
на разных сайтах вызова. Если это так, то следует
использовать подстановочные знаки. Подстановочные знаки предназначены для поддержки гибкого подтипирования,
что мы и пытаемся здесь выразить.
Не кажется ли нам, что подстановочная карта, подобная (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>
and 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
Рассмотрим следующий пример из "Программирования на 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'. Первого метода в книге нет.
Ответ 3
В вашем первом вопросе: это означает, что если существует связь между типом параметра и типом возвращаемого метода, тогда используйте универсальный.
Например:
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. */
Ответ 4
Метод подстановочного знака также является универсальным - вы могли бы вызывать его с некоторым диапазоном типов.
В <T>
синтаксисе определяется имя переменной типа. Если переменная типа имеет какое-либо применение (например, в реализации метода или в качестве ограничения для другого типа), то имеет смысл дать ей имя, в противном случае вы могли бы использовать ?
, как анонимную переменную. Итак, похоже, это просто короткий путь.
Более того, ?
синтаксиса невозможно избежать при объявлении поля:
class NumberContainer
{
Set<? extends Number> numbers;
}