8

Background: I recently wrote an answer where I suggested writing the following code:

Files.write(Paths.get("PostgradStudent.csv"),
        Arrays.stream(PGstudentArray).map(Object::toString).collect(Collectors.toList()),
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

After some thoughts, I went: "I don't actually need a list here, I just need an Iterable<? extends CharSequence>". As Stream<T> has a method Iterator<T> iterator(), I then thought, well, that's easy:

Iterable<? extends CharSequence> iterable = () -> Arrays.stream(arr).map(Object::toString).iterator();

(I extracted it to a local variable for this question, I would like to do it inline at the end.) Unfortunately this does not compile without additional type hints:

error: incompatible types: bad return type in lambda expression
Iterable<? extends CharSequence> iterable = () -> Arrays.stream(arr).map(Object::toString).iterator();
                                                                                                   ^
    Iterator<String> cannot be converted to Iterator<CharSequence>

Of course adding some type hints will make this work:

Iterable<? extends CharSequence> iterable2 = (Iterable<String>) () -> Arrays.stream(arr).map(Object::toString).iterator();
Iterable<? extends CharSequence> iterable3 = () -> Arrays.stream(arr).<CharSequence>map(Object::toString).iterator();

In my understanding, the Java compiler does the following things:

  1. It looks at the target type of the expression, which is Iterable<? extends CharSequence>.
  2. It then determines the function type of this interface, which is () -> Iterator<? extends CharSequence> in my case.
  3. It then looks at the lambda and checks whether it is compatible. In my case, the lambda has a type of () -> Iterator<String>. Which is compatible with the function type determined in step 2.

Interestingly, if I change the target of the lambda to Supplier:

Supplier<Iterator<? extends CharSequence>> supplier = () -> Arrays.stream(arr)
    .map(Object::toString)
    .iterator();

it will compile fine.

Why can't javac infer the correct type for this lambda?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Johannes Kuhn
  • 14,778
  • 4
  • 49
  • 73
  • It should be `Iterable extends String>` Not `? extends CharSequence` – Ravindra Ranwala Oct 24 '19 at 09:30
  • @RavindraRanwala I don't understand your comment. Look at the [Javadoc for Files.write](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#write-java.nio.file.Path-java.lang.Iterable-java.nio.file.OpenOption...-). This is not something I can change. And because `String` is final, it would be equal to `Iterable`. – Johannes Kuhn Oct 24 '19 at 09:35
  • Might just be related to the [Capture Conversion#jls-5.1.10](https://docs.oracle.com/javase/specs/jls/se13/html/jls-5.html#jls-5.1.10) – Naman Oct 24 '19 at 10:14
  • 1
    @Naman not at all. It doesn't matter if I use it as part of an assignment or if I use it in a call to `Files.write`. Assignment is just an easy way to provide a target for the lambda. – Johannes Kuhn Oct 24 '19 at 10:43

2 Answers2

3

You can find some explanation here:

Wildcard-parameterized functional interface types must be turned into a function type (method signature) before checking for compatibility ... This works as follows:

Iterable<? extends CharSequence> becomes () -> Iterator<CharSequence>

So, if the lambda expression is implicitly-typed, the LHS becomes Iterator<CharSequence> while the RHS is Iterator<String>. Hence, an error:

Iterator<String> cannot be converted to Iterator<CharSequence>

This behavior is also explained in the JLS §18.5.3.

Oleksandr Pyrohov
  • 14,685
  • 6
  • 61
  • 90
  • Interesting fact is that `Supplier>` as target works just fine. What is the difference here? – Johannes Kuhn Oct 24 '19 at 11:03
  • @JohannesKuhn You added one more level of nesting which doesn't have a wildcard. – Oleksandr Pyrohov Oct 24 '19 at 11:20
  • So basically, because the parameter `Iterator extends CharSequence>` is not a wildcard, it doesn't do that? Then why not do that one level above as well? – Johannes Kuhn Oct 24 '19 at 11:22
  • @JohannesKuhn That's a question for the language designers. – Oleksandr Pyrohov Oct 24 '19 at 11:24
  • So, to summarize, to avoid a hard technical problem (type inference is hard), they decided to cheat a bit (which is fine), but they did it in a way that does not avoid this problem in all cases (which is stupid)? – Johannes Kuhn Oct 24 '19 at 11:26
  • @JohannesKuhn If you'd like to dive deeper into this topic - check the JLS section which describes how does the [**function type**](https://docs.oracle.com/javase/specs/jls/se12/html/jls-9.html#jls-9.9) is defined. – Oleksandr Pyrohov Oct 24 '19 at 12:00
  • 1
    @OleksandrPyrohov this... makes no sense to me. `LHS` becomes `Iterator`, let's suppose this is true; then how come a cast of the `RHS` (`(Iterable)`...) makes it work? It should generate the same exact error. – Eugene Oct 24 '19 at 13:44
  • @Eugene Citation from the linked bug: *We make some extra effort to choose a better function type when an explicitly-typed lambda expression is being used ...* - in case of explicitly-typed lambda expression the process is different. – Oleksandr Pyrohov Oct 24 '19 at 13:46
  • 1
    @OleksandrPyrohov right, I have read that. nevertheless, it does not make it less weird to me. your answer _is_ spot-on, no doubt, it's just me not completely satisfied. – Eugene Oct 24 '19 at 13:50
  • 1
    @Eugene Thank you, I completely understand you! I will also add a little clarification about the type of the lambda. – Oleksandr Pyrohov Oct 24 '19 at 13:51
2

After reading the other answer (which is absolutely correct) and some coffee, it seems the explanation in the bug is pretty logical.

There are two cases here: an explicit lambda type and an implicit lambda type. The explicit type is :

Iterable<String> one = () -> Arrays.stream(arr).map(Object::toString).iterator();
Iterable<? extends CharSequence> iterable = one;

or as in the OP's example:

Iterable<? extends CharSequence> iterable2 = (Iterable<String>) () -> Arrays.stream(arr).map(Object::toString).iterator();

We are directly telling to the compiler which type the lambda expression is : Iterable<String>.

In this case the compiler has one thing to do only: see if the target is assignable to that type; pretty easy to find out and not very much related to lambdas per-se.

The other type is an implicit type, when the compiler has to infer the type and things get a bit tricky here. The "tricky" part comes from the fact that the target uses wildcards, thus could match more than just one option. There could be an infinite number of ways (finite of course, but just to prove a point) that the lambda could be inferred as.

It could start with something like this for example:

Iterator<? extends Serializable> iter = Arrays.stream(arr).map(Object::toString).iterator();

No matter what further gets done, this will fail: CharSequence does not extend Serializable, but String does; we will not be able to assign Iterable<? extends CharSequence> iterable to "whatever-the-infered-type-with-Serializable-is".

Or it could start with:

Iterator<? extends Comparable<? extends CharSequence>> iter = Arrays.stream(arr).map(Object::toString).iterator();

So theoretically the compiler could just start to infer what that type could be and check one by one if a "certain" inferred type could match the target; but requires a lot of work, obviously; thus not done.

The other way is a lot easier, "cut" the target and thus drop the possibilities of inference to just one. Once the target is transformed to:

Iterable<CharSequence> iterable...

the work that a compiler has to do is far more simple.

This, btw, would not be the first time I see this implicit vs explicit types logic in lambdas.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • And how does this work with `Supplier>`? – Johannes Kuhn Oct 25 '19 at 10:09
  • 1
    @JohannesKuhn this has to do with _nested_ wildcards, I am still trying to figure out what is going on, but [here](https://stackoverflow.com/questions/27902219/nested-wildcards/58631449#58631449) is a recent answer I've posted related to this. – Eugene Oct 31 '19 at 03:01