Iain Toft Iain Toft - 1 month ago 8
Java Question

eclipse compiler compiles code that javac will not - code looks to be legal

The following code compiles in Eclipse Version: Mars.2 Release (4.5.2) Build id: 20160218-0600 on Windows 10 Version 10.0.14393. It does not compile on a range of oracle javac compilers. There are other questions concerning pieces of code that compile with the Eclipse JDT compiler, but not javac. I could not find one similar to this example. This is a piece of toy code, and it's only purpose is to demonstrate this curiosity.

Is the eclipse compiler correct to compile this?

Note: If the eclipse compiler generated .class is de-compiled it produces source which can be compiled with javac.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class Puzzle {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) {
List<List> outer = Arrays.asList(Arrays.asList(new Object(), new Object()),
Arrays.asList(),
Arrays.asList(new Object(), new Object()));

Stream<Boolean> bs1 = outer.stream().flatMap(inner -> inner.stream()).map(obj -> obj.hashCode() % 2 == 0);
boolean b0 = bs1.filter(b -> !b).findAny().isPresent();

boolean b2 = outer.stream().flatMap(inner->inner.stream())
.map(obj -> obj.hashCode() % 2 == 0)
.filter(b -> !b).findAny().isPresent();

System.out.printf("%s %s %s", outer, b0, b2);
}
}


Here is the compiler error, in several versions of the compiler:

C:\Users\tofti>C:\jdk1.8.0_121\bin\javac -version & C:\jdk1.8.0_121\bin\javac Puzzle.java
javac 1.8.0_121
Puzzle.java:23: error: bad operand type Object for unary operator '!'
.filter(b -> !b).findAny().isPresent();
^
1 error

C:\Users\tofti>C:\jdk1.8.0_112\bin\javac -version & C:\jdk1.8.0_112\bin\javac Puzzle.java
javac 1.8.0_112
Puzzle.java:23: error: bad operand type Object for unary operator '!'
.filter(b -> !b).findAny().isPresent();
^
1 error

C:\Users\tofti>C:\jdk1.8.0_141\bin\javac -version & C:\jdk1.8.0_141\bin\javac Puzzle.java
javac 1.8.0_141
Puzzle.java:23: error: bad operand type Object for unary operator '!'
.filter(b -> !b).findAny().isPresent();
^
1 error


And here is the code compiling, and executing in Eclipse:
Code compiling and running in eclipse

Answer Source

One of the suppressed "unchecked" warnings reveals the problem, in Eclipse 4.6.3:

Type safety: The expression of type Stream needs unchecked conversion to conform to Stream<Boolean>

And in javac 1.8.0_131:

Puzzle.java:12: warning: [unchecked] unchecked conversion
        Stream<Boolean> bs1 = outer.stream().flatMap(inner -> inner.stre
am()).map(obj -> obj.hashCode() % 2 == 0);

     ^
  required: Stream<Boolean>
  found:    Stream

Without any casting, here are the actual return types from each method call in the chain:

Stream<List> s1 = outer.stream();
Stream s2 = s1.flatMap(inner -> inner.stream());
Stream s3 = s2.map(obj -> obj.hashCode() % 2 == 0);
Stream s4 = s3.filter(b -> !b); // compiler error
Optional opt = s4.findAny();
boolean b = opt.isPresent();

If you add a wildcard to make it List<List<?>> outer, this is what you get instead:

Stream<List<?>> s1 = outer.stream();
Stream<?> s2 = s1.flatMap(inner -> inner.stream());
Stream<Boolean> s3 = s2.map(obj -> obj.hashCode() % 2 == 0);
Stream<Boolean> s4 = s3.filter(b -> !b); // compiles OK
Optional<Boolean> opt = s4.findAny();
boolean b = opt.isPresent();

So the issue is that the .map call on the raw Stream doesn't actually return Stream<Boolean>, you're just implicitly casting it when you assign bs1, and that fixes the raw types in the rest of the chain. In fact, you can add this cast to the one-line version and it compiles:

boolean b2 = ((Stream<Boolean>) outer.stream().flatMap(inner -> inner.stream())
        .map(obj -> obj.hashCode() % 2 == 0))
        .filter(b -> !b).findAny().isPresent();

Now, the raw .map call is missing the class type parameter <T>, but <R> is a method type parameter, so why doesn't it return Stream<R>? The raw types section in the Java language specification states that "a non-static type member of a raw type is considered raw". The example given is a generic inner class, but assuming it applies to generic methods as well, this should be the reason for the <R> parameter of .map getting dropped when it's called on a raw Stream, causing it to return another raw Stream.

EDIT: Found this in an Eclipse bug report:

It looks like javac infers the return type of the method invocation to be the raw type I when passed in arguments are raw types. This may be motivated by the almost last bit of §18.5.2 after bounds set 4 has been generated

[...]

If unchecked conversion was necessary for the method to be applicable during constraint set reduction in §18.5.1, then the parameter types of the invocation type of m are obtained by applying θ' to the parameter types of m's type, and the return type and thrown types of the invocation type of m are given by the erasure of the return type and thrown types of m's type.