Reputation: 1116
While manipulating Java 8 streams I've encountered an error where the compiler seems to 'forget' the type my generic parameters.
The following snippet creates a stream of class names and attempts to map the stream to a stream of Class<? extends CharSequence>
.
public static Stream<Class<? extends CharSequence>> getClasses() {
return Arrays.asList("java.lang.String", "java.lang.StringBuilder", "Kaboom!")
.stream()
.map(x -> {
try {
Class<?> result = Class.forName(x);
return result == null ? null : result.asSubclass(CharSequence.class);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
})
//.filter(x -> x != null)
;
}
When I uncomment the filter to remove the null entries from the stream I get a compile error
Type mismatch: cannot convert from Class<capture#15-of ? extends CharSequence> to Class<Object>
Can someone please explain to me why adding the filter causes this error?
PS: The code here is somewhat arbitrary and it's easy enough to make the error go away: Assign the mapped stream to a temporary variable before applying the filter. What I'm interested in is why the above code snippet generates a compile time error.
Edit: As @Holger pointed out the this question is not an exact duplicate of Java 8 Streams: why does Collectors.toMap behave differently for generics with wildcards? because the problematic snippet there currently compiles without issues while the snippet here does not.
Upvotes: 14
Views: 3533
Reputation: 2538
Because return type of your lambda function cannot be determined (or compiler just doesn't try to do so) correctly. Using explicit anonymous Function
object with correct type parameters completely removes problems with type inference:
public static Stream<Class<? extends CharSequence>> getClasses() {
return Arrays.asList("java.lang.String",
"java.lang.StringBuilder",
"Kaboom!")
.stream().map(
new Function<String, Class<? extends CharSequence>>() {
public Class<? extends CharSequence> apply(String name) {
try {
return Class.forName(name).asSubclass(CharSequence.class);
} catch (Exception e) {
}
return null;
}
}
).filter(Objects::nonNull);
}
To see, what actual return type of lambda function is resolved by compiler, try asking Eclipse to assign the expression ...stream().map(<your initial lambda>)
to local variable (press Ctrl+2
, then L
with cursor standing just before the expression). It is Stream<Class<? extends Object>>
return type resolved by compiler, not expected Stream<Class<? extends CharSequence>>
.
Upvotes: 0
Reputation: 4076
This is because of type inference:
The type is "guessed" from it's target: we know that map(anything) must return a
"Stream<Class<? extends CharSequence>>"
because it is the return type of the function. If you chain that return to another operation, a filter or a map for example, we loose this type inference (it can't go "through" chainings)
The type inference has his limits, and you find it.
The solution is simple: has you said, if you use a variable, you can specify the target then help the type inference.
This compile:
public static Stream<Class<? extends CharSequence>> getClasses() {
Stream<Class<? extends CharSequence>> map1 = Arrays.asList ("java.lang.String", "java.lang.StringBuilder", "Kaboom!").stream ().map (x -> {
try {
Class<?> result = Class.forName (x);
return result == null ? null : result.asSubclass(CharSequence.class);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace ();
}
return null;
});
return map1.filter(x -> x != null);
Note that i modified the code to return always null to show that infered type doesn't come from lambda return type.
And we see that the type of map1 is infered by the variable declaration, its target. If we return it, it is equivalent, the target is the return type, but if we chain it:
This doesn't compile:
public static Stream<Class<? extends CharSequence>> getClasses () {
return Arrays.asList ("java.lang.String", "java.lang.StringBuilder", "Kaboom!").stream ().map (x -> {
try {
Class<?> result = Class.forName (x);
return result == null ? null : result.asSubclass(CharSequence.class);
} catch (Exception e) {
e.printStackTrace ();
}
return null;
}).filter(x -> x != null);
The first map declaration has no target, so the infered type is defined by default: Stream<Object>
Edit
Another way to make it work would be to make the type inference work with Lambda return value (instead of target), you need to specify the return type with cast for example. This will compile:
public static Stream<Class<? extends CharSequence>> getClasses2 () {
return Arrays.asList ("java.lang.String", "java.lang.StringBuilder", "Kaboom!").stream ().map (x -> {
try {
Class<?> result = Class.forName (x);
return (Class<? extends CharSequence>)( result == null ? null : result.asSubclass(CharSequence.class));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace ();
}
return (Class<? extends CharSequence>)null;
}).filter(x -> x != null);
}
Note that this is because of operation chaining, you could replace .filter(x -> x != null) with map(x->x) you would have the same problem.
Edit: modify examples to match exactly the question.
Upvotes: 1
Reputation: 1359
In addition to @pdem's answer, also this works for you :
public class Test {
public static void main(String[] args) {
getAsSubclasses(CharSequence.class, "java.lang.String", "java.lang.StringBuilder", "Kaboom!")
.forEach(System.out::println);
}
public static <C> Stream<Class<? extends C>> getAsSubclasses(Class<C> type, String... classNames) {
return Arrays.stream(classNames)
.map(new ToSubclass<>(type))
.filter(c -> c != null);
}
static final class ToSubclass<C> implements Function<String, Class<? extends C>> {
final Class<C> type;
ToSubclass(Class<C> type) {
this.type = type;
}
@Override
public Class<? extends C> apply(String s) {
try {
return Class.forName(s).asSubclass(type);
} catch (Exception e) {
return null;
}
}
}
}
Upvotes: 0