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:
Iterable<? extends CharSequence>
.() -> Iterator<? extends CharSequence>
in my case.() -> Iterator<String>
.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.
The question now is: Why can't javac infer the correct type for this lambda?
One of the main reasons for making String implement Iterable is to enable the simple for(each) loop, as mentioned above. So, a reason for not making String implement Iterable could be the inherent inefficiency of a naïve implementation, since it requires boxing the result.
A String is an immutable sequence of bytes. Strings are iterable; iteration over a string yields each of its 1-byte substrings in order. But String doesn't implement Iterable 's Iterate method.
Both Iterator and Iterable are interfaces in Java that look very similar and are often confusing for beginners, but both are two different things. In short, if any class implements the Iterable interface, it gains the ability to iterate over an object of that class using an Iterator.
We can iterate the elements of Java Iterable by obtaining the Iterator from it using the iterator() method. The methods used while traversing the collections using Iterator to perform the operations are: hasNext(): It returns false if we have reached the end of the collection, otherwise returns true.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With