Home > other >  Java Generics: Stream.map() returns "capture of ?" instead of "?"
Java Generics: Stream.map() returns "capture of ?" instead of "?"

Time:08-18

I'm trying to build a List of Classes that implement a certain interface called Interface:

List<Class<? extends Interface>> myList= myMap.entrySet().stream()
    .filter(entry -> entry.getValue().equals(myValue))
    .map(Map.Entry::getKey)   // Stream<Interface>
    .map(Interface::getClass) // Stream<Class<capture of ? extends Interface>>
    .distinct()
    .toList();

I added as comment the type of the elements in the Stream after map() is called.

The code iterates over all the entries in the map, and if their value is equal to myValue, then:

  • first, gets the instance of type Interface (which is the key of the entry)
  • then, gets the Class implementing the Interface.

myMap is defined as:

Map<Interface, Integer> myMap = new HashMap<>()

The error I'm getting :

Incompatible types.
Found: 'java.util.List<java.lang.Class<capture<? extends application.interfaces.Interface>>>',
required: 'java.util.List<java.lang.Class<? extends application.interfaces.Interface>>'

I am clearly missing something about how Generics work in Java, but I am at a loss here. I suppose it's something related to the fact that the compiler cannot correctly reify my ? wildcard.

CodePudding user response:

As @Slaw has pointed out in the comments, in this case getClass() is capable to provide the information about the generic type to the compiler.

According to the documentation:

The actual result type is Class<? extends |X|> where |X| is the erasure of the static type of the expression on which getClass is called.

Hence, at compile time, we would have a type ? extends Interface and the reason of the observed behavior is related solely to peculiarities of type inference in Java.

In this case, when we are chaining methods after map() operation, the compiler fails to infer the type of the method reference Interface::getClass correctly based on the resulting type returned by the stream.

If we substitute toList, which expects elements of type T and produces List<T>, with collect(Collectors.toList()), in which collector is of type Collector<? super T, A, R>, the compiler would be able to do its job (here's a proof):

List<Class<? extends Interface>> myList = myMap.entrySet().stream()
    .filter(entry -> Objects.equals(entry.getValue(), myValue))
    .map(Map.Entry::getKey)   // Stream<Interface>
    .map(Interface::getClass) // Stream<Class<? extends Interface>>
    .distinct()
    .collect(Collectors.toList());

But to make type inference working with toList() we need to provide the generic type explicitly.

For instance, this code would compile, because the type of Interface::getClass could be inferred from the assignment context (here there are no operations after map(), hence myStream directly says what should be the return type of map()):

Stream<Class<? extends Interface>> myStream = myMap.entrySet().stream()
    .filter(entry -> Objects.equals(entry.getValue(), myValue))
    .map(Map.Entry::getKey)
    .map(Interface::getClass);

List<Class<? extends Interface>> myList = myStream.distinct().toList();

A more handy way would be to use a so-called Type Witness:

Map<Interface, Integer> myMap = Map.of(new ClasA(), 1, new ClasB(), 1);
        
int myValue = 1;
        
List<Class<? extends Interface>> myList = myMap.entrySet().stream()
    .filter(entry -> Objects.equals(entry.getValue(), myValue))
    .map(Map.Entry::getKey)                               // Stream<Interface>
    .<Class<? extends Interface>>map(Interface::getClass) // Stream<Class<? extends Interface>>
    .distinct()
    .toList();
        
myList.forEach(c -> System.out.println(c.getSimpleName()));

Output:

ClasA
ClasB

Dummy classes:

interface Interface {}
class ClasA implements Interface {}
class ClasB implements Interface {}
  • Related