JDK-8177946 : Strange compilation error on calling map and get on Optional of raw type parameter
  • Type: Bug
  • Component: tools
  • Sub-Component: javac
  • Affected Version: 8u121
  • Priority: P4
  • Status: Closed
  • Resolution: Won't Fix
  • OS: linux
  • CPU: x86_64
  • Submitted: 2017-04-02
  • Updated: 2017-04-03
  • Resolved: 2017-04-03
Related Reports
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux 4.4.0-66-generic #87-Ubuntu SMP Fri Mar 3 15:29:05 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux


A DESCRIPTION OF THE PROBLEM :
The following code does not compile:

import java.util.Optional;

class B<T> {}

public class Test {
	static B<Integer> fn(B<Object> a) { return null; }
	
	public static void main(String[] args) {
		
                Optional<B> opt = Optional.empty(); // note raw type parameter B
		
                B<Integer> res = opt.map(Test::fn).get(); // COMPILE ERROR: incompatible types: Object cannot be converted to B<Integer>

	}
}

It looks like javac infers the result of opt.map(Test::fn) to be Optional<Object> instead of Optional<B<Integer>>. I expected it to be inferred as Optional<B<Integer>> because fn returns B<Integer>.

Also, it's curious that the program does compile if we explicitly cast the result of opt.map as follows:

B<Integer> res = ((Optional<B<Integer>>) opt.map(Test::fn)).get();

It also compiles successfully if we make fn take a raw B, instead of B<Object>.

If the signature of fn is erased because of unchecked conversion, shouldn't the erasure return a raw B? Does the compiler erase the return to Object, or why does the compilation fail?


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Compile the program shown below

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Expected program to compile successfully
ACTUAL -
Compilation error

ERROR MESSAGES/STACK TRACES THAT OCCUR :
Compilation error: incompatible types: Object cannot be converted to B<Integer>

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.util.Optional;

class B<T> {}

public class Test {
	static B<Integer> fn(B<Object> a) { return null; }
	
	public static void main(String[] args) {
		
                Optional<B> opt = Optional.empty(); // note raw type parameter B
		
                B<Integer> res = opt.map(Test::fn).get(); // COMPILE ERROR: incompatible types: Object cannot be converted to B<Integer>

	}
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Multiple workarounds:

0) Don't use raw types in the first place

1) declare fn() as: static B<Integer> fn(B a) { return null; } // NOTE: parameter is raw B instead of B<Object>

2) explicit cast: B<Integer> res = ((Optional<B<Integer>>) opt.map(Test::fn)).get();



Comments
The behavior can probably be understood better by rewriting the method reference as a lambda: B<Integer> res = opt.map(b -> Test.fn(b)).get() In the above, Test.fn(b) is unchecked, as the type of the actual 'b' is B and the type of the formal is B<Object>. Therefore, the return type of Test.fn(b) is erased from B<Integer> to B, and B is the type inferred for the inference variable U in Optional.map. So we have an Optional<B> and we're calling get(), and we obtain a B, and then we need unchecked conversion to go to B<Integer>.
03-04-2017

In the code above, the method reference expression deserves an unchecked warning, as per 15.13. When determining the compile-time declaration for the method reference, the compiler does a method lookup in Test with argument types B (the type parameter of the functional interface Mapper<? super B, ? extends U>). Since B is convertible to B<Object> with unchecked conversion, the method reference expression gets an unchecked warning, and the signature of the corresponding compile-time declaration is to be erased for the purpose of determining the method reference type (see 15.13.2). This means that the inference variable U will get a constraint not from B<Integer> (the return type of fn), but just from B (the _erased_ return type of fn). That said, the code should still compile, as map.get would return B, and B is still convertible to B<Integer>. Unfortunately JDK 8 has a bug in that unchecked conversions occurring in nested scopes are erroneously applied to outer scopes, resulting in too much erasure being applied. This behavior has been fixed as part as JDK-8147493, but there were many follow up fixes in JDK 9 to make sure that other valid code kept compiling correctly, so I don't think this is reasonable to backport.
03-04-2017

Issue is fixed in 9 ea b103, 9 ea b102 - Fail 9 ea b103 - Pass 9 ea b163 - Pass
03-04-2017

Issue is reproducible in 8u121, 8u131, test passes in 9 == /java_re/jdk/8u121/fcs/b13/binaries/linux-x64/bin/javac Test.java Test.java:12: error: incompatible types: Object cannot be converted to B<Integer> B<Integer> res = opt.map(Test::fn).get(); // COMPILE ERROR: incompatible types: Object cannot be converted to B<Integer> ==
03-04-2017