10

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.

2
  • 2
    Probably, you meant entry.getValue().equals(myValue), not entry.getValue() == myValue Commented Aug 8, 2022 at 17:14
  • Yes, thanks for noticing, I'll update post Commented Aug 8, 2022 at 17:18

1 Answer 1

14

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 {}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks, I didn't know Type Witnesses even existed. I noted that you modified the predicate in the filter(). Is it for any particular reason?
Note: "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" – Javadoc
@Slaw Right, and in Java generics get erased to Object type. So erasure type Xin the quote is nothing else but Object. And ? extends Object ( ? extends X ) effectively is the same as unknown type ?.
@ralos I've modified it when I was reading your initial version (which used identity comparison ==). You can use either equals(), or Objects.equals(). The latter has a small advantage, it's not hostile to null.
@AlexanderIvanchenko Type erasure is not involved here, as we're talking about compile-time, when generics still exist. One would expect Interface::getClass to return a Class<? extends Interface>. From there, it would make sense to think you'd get a Stream<Class<? extends Interface>> from the map(Interface::getClass) call. That's apparently not the case but it's an understandable expectation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.