1

In this answer I attempted to create a static utility method to make a List into a 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()));
}

It works just fine. However, I found that the method cannot be used in all the same contexts as the list.stream().collect(...) expression. The method isn't as flexible.

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!

In this example, Student is a subtype of Person and has a getId method that returns a Long.

The last statement fails with incompatible types: inference variable T has incompatible bounds ... (JDK 1.8.0_25). Is there a way to define the type parameters so that the static method will work in the same contexts as the expression it contains?

Community
  • 1
  • 1
glts
  • 21,808
  • 12
  • 73
  • 94
  • [is-listdog-a-subclass-of-listanimal-why-arent-javas-generics-implicitly-polymorphic?](http://stackoverflow.com/questions/2745265/is-listdog-a-subclass-of-listanimal-why-arent-javas-generics-implicitly-p). `Map` can't be reference to returned `Map`. Maybe consider using `Map` as reference. – Pshemo Nov 01 '14 at 18:05
  • If you could use `Map` reference for instance of `Map` it would mean that via such reference you could put to this map any kind of person, not only Student (in other words generics are not covariant). – Pshemo Nov 01 '14 at 18:15

3 Answers3

6

You could add a type parameter for the values of the map so they can be different from 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()));
}
Alex - GlassEditor.com
  • 14,957
  • 5
  • 49
  • 49
1

Your last line calls the method toMapBy in which the compiler infers the type Student for T. So it obviously returns a List<Long, Student>.

But generics aren't covariant!

That means, you cannot assign a List<Long, Student> to a variable of type List<Long, Person>, because they are not in a subtype relationship.

The solution is to use the subtype form:

Map<Long, ? extends Person> peopleById2 = toMapBy(students, Student::getId); // no compiler error
Community
  • 1
  • 1
Seelenvirtuose
  • 20,273
  • 6
  • 37
  • 66
  • 1
    True, but the question is whether it's possible to define `toMapBy`'s type parameters so that it will work in the same contexts as the stream expression. In other words, why can `Map` be the assignment target in the second to last line but not in the last? – glts Nov 01 '14 at 18:15
  • Interesting. But look as @Alex' answer. This is _the_ solution. – Seelenvirtuose Nov 01 '14 at 18:29
0

With this bit:

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

Notice that you do not provide arguments to Function.identity(). The compiler is free to infer it as Function.<Person>identity() to resolve the difference imposed by the return value assignment.

This should be good enough for your purpose:

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

Now the elements of the List can be a subtype of the Map values. Or you can define a third parameter like @Alex has suggested.

Radiodef
  • 37,180
  • 14
  • 90
  • 125