14

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.

Community
  • 1
  • 1
Chris Kerekes
  • 1,116
  • 8
  • 27
  • 1
    Huh. At first I was thinking this was due to [method chaining](http://stackoverflow.com/q/24794924/1743880) 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 Feb 22 '16 at 22:50
  • 1
    I can get it to compile with the call to `filter` with an explicit type argument to `map`: `.>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 Feb 22 '16 at 22:56
  • It is no problem with javac 1.8.0_31. –  Feb 22 '16 at 23:46
  • 1
    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 Feb 23 '16 at 11:21
  • 1
    @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 Feb 23 '16 at 11:49
  • @Holger I didn't close that one, JarrodRoberson did. – Tunaki Feb 23 '16 at 11:51
  • @JarrodRoberson it looks like this isn't a duplicate of the linked question, as per Holger's comment. – Tunaki Feb 23 '16 at 11:54
  • 1
    @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 Feb 23 '16 at 13:16

3 Answers3

1

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.

pdem
  • 3,880
  • 1
  • 24
  • 38
  • Your answer is completely missing the question. The question is about a problem that is caused by the `filter(…)` operation which you don’t use in any of your code examples. The fact that `null` can be assigned to every type or that the compilation will fail, when you replace the correct `Class extends CharSequence>` with an unmatching `Class>` is not helpful in any way. Keeping the questioner’s original code is simpler than your “solutions” as the questioner already creates a stream of the *correct* type which already works when not chaining a `filter(…)` operation. – Holger Feb 25 '16 at 10:48
  • The problem is not caused by the filter in particular, but by any operation, it could be map(), filter() or whatever (). and I let the filter in my code, it can be uncommented to get the original code. – pdem Feb 25 '16 at 11:02
  • The OPs code compiles without error without the `filter` operation and anything you do is only complicating the OPs code without solving anything. When you add the `filter` operation, the same error shows up. – Holger Feb 25 '16 at 11:07
  • @Holger are you really interested in the solution, or is it just a flame discussion? – pdem Feb 25 '16 at 11:13
  • I don’t see a flame discussion. You don’t have a solution, you are not even addressing the problem. You say you can uncomment the filter, but obviously you never tried it. *try it*. – Holger Feb 25 '16 at 11:17
  • I think it works, if not : Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/104526/discussion-between-pdem-and-holger). – pdem Feb 25 '16 at 11:28
0

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

    }

}
Community
  • 1
  • 1
FaNaJ
  • 1,329
  • 1
  • 16
  • 39
0

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

Vasily Liaskovsky
  • 2,248
  • 1
  • 17
  • 32
  • Eh, when you change the return type to `Stream>`, you don’t need to change anything in the original code, that’s a cheap solution, if we want to call it a solution… – Holger Feb 25 '16 at 11:10
  • Right you are, meant the desired type, just missed when edited code here – Vasily Liaskovsky Feb 25 '16 at 11:39
  • 1
    That fixes the problem, but I’d prefer to use an explicit type together with a lambda expression, e.g. `stream().>map(name -> { try { return Class.forName(name).asSubclass(CharSequence.class); } catch (Exception e) {} return null; }).filter(…)` but note that an explanation about *why* is not sufficient when it includes Eclipse’s behavior. Eclipse is … very special, when it comes to type inference. For `javac`, appending a `.map(x->x)` at the end also solves the problem, clearly showing that it never inferred `? extends Object` here. It’s more complicated. – Holger Feb 25 '16 at 11:50