PECS для дженериков в неколлекциях

Джошуа Блох придумал 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?


person Fatih Arslan    schedule 07.02.2019    source источник


Ответы (1)


Ради этого ответа возьмем Comparator в качестве основного, руководящего примера.

Если вы хорошенько об этом подумаете, то увидите, что Comparator на самом деле получает два аргумента типа T и возвращает результат их сравнения (представленный int). Другими словами, он использует два экземпляра типа T и выдает значение int. Итак, согласно правилу PECS, это потребитель T, отсюда и использование ? super T.

В более общем смысле следует рассматривать producer и consumer с точки зрения основного типа в отношении типов каждого из его универсальных параметров. Если какой-либо тип 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>, а тип списка doublesList<Double>, следующий код вызовет ошибку компиляции:

sort(doubles, c);

К счастью, поскольку Comparator является потребителем типа сравниваемых элементов, вы можете изменить сигнатуру метода sort на:

static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

И теперь этот код будет компилироваться:

sort(doubles, c);
person fps    schedule 07.02.2019
comment
Большое спасибо за супер чистое объяснение. Я хочу привести еще один пример с Iterator‹T›. Если я разработаю метод, который вызывает T next(), тогда я должен создать свою подпись с помощью ? расширяется, потому что итератор теперь производит T. Его можно обобщить, как если бы общий класс в методе потреблял T (как в случае компаратора), он должен быть супер, если он возвращает ссылку на T (как в случае итератора), тогда он должен быть продлен. - person Fatih Arslan; 07.02.2019
comment
@FatihArslan Да, это правильно. Например, если у вас есть метод, который получает Iterator<Number>, а вы пытаетесь передать Iterator<Integer>, он не скомпилируется. Но если вы измените метод так, чтобы теперь он получал Iterator<? extends Number>, вы сможете пройти Iterator<Integer> без проблем. И подстановочные знаки тоже можно смешивать. Например, метод Stream.map получает Function<? super T, ? extends R>. - person fps; 08.02.2019