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 {}
entry.getValue().equals(myValue), notentry.getValue() == myValue