Общий статический метод слишком сильно ограничивает типы

В этом ответе Я попытался создать статический метод утилиты, чтобы сделать List в Map:

public static <K, T> Map<K, T> toMapBy(List<T> list,
        Function<? super T, ? extends K> mapper) {
    return list.stream().collect(Collectors.toMap(mapper, Function.identity()));
}

Это прекрасно работает. Однако я обнаружил, что метод не может использоваться во всех тех же контекстах, что и выражение list.stream().collect(...). Метод не такой гибкий.

List<Student> students = Arrays.asList();

Map<Long, Student> studentsById1 = students.stream()
        .collect(Collectors.toMap(Student::getId, Function.identity()));
Map<Long, Student> studentsById2 = toMapBy(students, Student::getId);

Map<Long, Person> peopleById1 = students.stream()
        .collect(Collectors.toMap(Student::getId, Function.identity()));
Map<Long, Person> peopleById2 = toMapBy(students, Student::getId);  // compile error!

В этом примере Student является подтипом Person и имеет метод getId, который возвращает Long.

Последний оператор не работает с incompatible types: inference variable T has incompatible bounds ... (JDK 1.8.0_25). Есть ли способ определить параметры типа, чтобы статический метод работал в тех же контекстах, что и содержащееся выражение?

Ответ 1

Вы можете добавить параметр типа для значений карты, чтобы они отличались от T:

public static <K, V, T extends V> Map<K, V> toMapBy(List<T> list,
        Function<? super T, ? extends K> mapper) {
    return list.stream().collect(Collectors.toMap(mapper, Function.identity()));
}

Ответ 2

В последней строке вызывается метод toMapBy, в котором компилятор указывает тип Student для T. Поэтому он явно возвращает List<Long, Student>.

Но дженерики не ковариантны!

Это означает, что вы не можете назначить List<Long, Student> переменной типа List<Long, Person>, потому что они не находятся в отношении подтипа.

Решение заключается в использовании формы подтипа:

Map<Long, ? extends Person> peopleById2 = toMapBy(students, Student::getId); // no compiler error

Ответ 3

С этим битом:

Map<Long, Person> peopleById1 = students.stream()
        .collect(Collectors.toMap(Student::getId, Function.identity()));

Обратите внимание, что вы не предоставляете аргументы Function.identity(). Компилятор может вывести его как Function.<Person>identity() для устранения разницы, налагаемой присвоением возвращаемого значения.

Это должно быть достаточно хорошо для вашей цели:

public static <K, T> Map<K, T> toMapBy(
    List<? extends T> list, // <- note
    Function<? super T, ? extends K> mapper
) {
    ...
}

Теперь элементы списка могут быть подтипом значений карты. Или вы можете определить третий параметр, например @Alex.