14
votes

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.

3
Huh. At first I was thinking this was due to method chaining but I'm not sure actually. asSubclass really returns a Class<? extends CharSequence>. It doesn't compile with javac also, at least 1.8.0_74. - Tunaki
I can get it to compile with the call to filter with an explicit type argument to map: .<Class<? extends CharSequence>>map(. Or I can get it to compile with return CharSequence.class instead of return null after the catch block. It looks like a problem with type inference. - rgettman
Just a few side-notes: use Stream.of(…) instead of Arrays.asList(…).stream(), further Class.forName never returns null so the conditional is obsolete. And you can always merge a .map(…).filter(…) using flatMap(…) when the filter is merely handling the map’s error condition (i.e. a null test). Putting it all together, you can solve your task as return Stream.of("java.lang.String", "java.lang.StringBuilder", "Kaboom!") .flatMap(x -> { try { return Stream.of(Class.forName(x).asSubclass(CharSequence.class)); } catch(Exception e) { e.printStackTrace(); return null; }}); - Holger
@Tunaki: Unfortunately, it’s not a duplicate as the issue of the linked question has been fixed while the problem of this question arises with all versions, including the most recent one. - Holger
@Tunaki: indeed, I don’t know where I copied your name from. What’s really baffling me is that simply appending .map(Function.identity()) after the rejected .filter(…) makes the error disappear. Chaining another .filter(x->true) or even a simple .unordered() makes it reappear and adding another map(x->x) or .flatMap(Stream::of) will fix it again. You can go on with that…the only thing that matters is the last operation of the chain. - Holger

3 Answers

1
votes

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.

0
votes

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;
            }
        }

    }

}
0
votes

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>>.