Джошуа Блох придумал PECS, в которой указано правило, когда использовать ? extends T и ? super T. Если вы думаете о PECS с точки зрения структуры коллекций, то все очень просто. Если вы добавляете значения в структуру данных, используйте ? super T. Если вы читаете из структуры данных, используйте ? extends T.
например:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}
Если я проверю подпись
public static <T> void sort(List<T> list, Comparator<? super T> c)
Я вижу, что Comparator использует ? super, поэтому он должен быть потребителем. Глядя на код, компаратор c используется только для создания материала, потому что ему задают логику сравнения.
С одной стороны, я понимаю, почему это супер, потому что как разработчик я хочу использовать компараторы класса T, а также компараторы суперкласса T, потому что объекты T также относятся к типу суперклассов T. Но когда я пытаюсь думать с точки зрения PECS, я ничего не понимаю.
Подходит ли PECS только для структуры коллекций? Если нет, может кто-нибудь объяснить мне, что потребляет компаратор в Collections.sort?




Ради этого ответа возьмем Comparator в качестве основного, руководящего примера.
Если вы хорошенько об этом подумаете, то увидите, что Comparator на самом деле получает двух аргументов типа T и возвращает результат их сравнения (представленный int). Другими словами, это потребляет два экземпляра типа T и производит значение int. Итак, согласно правилу PECS, это потребительT, отсюда и использование ? super T.
В более общем смысле следует рассматривать режиссер и потребитель с точки зрения основного типа в отношении типов каждого из его универсальных параметров. Если некоторые объекты Comparator типа потребляет типа T, правило PECS гласит, что пользователи такого Comparator<T> могут использовать его для сравнения объектов, тип которых является подтипом T.
В качестве конкретного примера, если у вас уже есть логика для сравнения двух общих экземпляров Number (независимо от того, каков их конкретный тип на самом деле), вы можете использовать ее, т. е. для сравнения экземпляров Double, потому что двойные значения, в конце концов, числа.
Рассмотрим следующий компаратор:
Comparator<Number> c = Comparator.comparingInt(Number::intValue);
Здесь компаратор c сравнивает экземпляры Number (номер любой), принимая во внимание только их интегральную часть.
Если у вас есть следующий список экземпляров Double:
List<Double> doubles = Arrays.asList(2.2, 2.1, 7.3, 0.2, 8.4, 9.5, 3.8);
И следующий метод sort:
static <T> void sort(List<T> list, Comparator<T> c) {
list.sort(c);
}
(Обратите внимание на отсутствие подстановочного знака ? super T в аргументе Comparator).
Затем, если вы хотите отсортировать список List<Double> doubles, сигнатура вышеуказанного метода sort потребует от вас передачи конкретного Comparator<Double>. Но что, если вы хотите использовать свой ранее определенный компаратор c для сортировки List<Double> doubles?
Поскольку тип этого компаратора — Comparator<Number>, а тип списка doubles — List<Double>, следующий код вызовет ошибку компиляции:
sort(doubles, c);
К счастью, поскольку Comparator является потребитель типа сравниваемых элементов, вы можете изменить сигнатуру метода sort на:
static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
И теперь этот код будет компилироваться:
sort(doubles, c);
@FatihArslan Да, это правильно. Например, если у вас есть метод, который получает Iterator<Number>, и вы пытаетесь передать Iterator<Integer>, он не скомпилируется. Но если вы измените метод так, чтобы теперь он получал Iterator<? extends Number>, вы сможете без проблем передать Iterator<Integer>. И подстановочные знаки тоже можно смешивать. Например, метод Stream.map получает Function<? super T, ? extends R>.
Большое спасибо за супер чистое объяснение. Я хочу привести еще один пример с Iterator<T>. Если я разработаю метод, который вызывает T next(), тогда я должен создать свою подпись с помощью ? расширяется, потому что итератор теперь производит T. Его можно обобщить, как если бы общий класс в методе потреблял T (как в случае компаратора), он должен быть супер, если он возвращает ссылку на T (как в случае итератора), тогда он должен быть продлен.