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.

8
  • 1
    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. Commented Feb 22, 2016 at 22:50
  • 1
    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. Commented Feb 22, 2016 at 22:56
  • 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; }}); Commented Feb 23, 2016 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. Commented Feb 23, 2016 at 11:49
  • 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. Commented Feb 23, 2016 at 13:16

3 Answers 3

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.

Sign up to request clarification or add additional context in comments.

6 Comments

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.
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.
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 are you really interested in the solution, or is it just a flame discussion?
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.
|
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;
            }
        }

    }

}

Comments

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

3 Comments

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…
Right you are, meant the desired type, just missed when edited code here
That fixes the problem, but I’d prefer to use an explicit type together with a lambda expression, e.g. stream().<Class<? extends CharSequence>>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.

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.