The following program compiles in Java 7 and in Eclipse Mars RC2 for Java 8:
import java.util.List;
public class Test {
static final void a(Class<? extends List<?>> type) {
b(newList(type));
}
static final <T> List<T> b(List<T> list) {
return list;
}
static final <L extends List<?>> L newList(Class<L> type) {
try {
return type.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Using the javac 1.8.0_45 compiler, the following compilation error is reported:
Test.java:6: error: method b in class Test cannot be applied to given types;
b(newList(type));
^
required: List<T>
found: CAP#1
reason: inference variable L has incompatible bounds
equality constraints: CAP#2
upper bounds: List<CAP#3>,List<?>
where T,L are type-variables:
T extends Object declared in method <T>b(List<T>)
L extends List<?> declared in method <L>newList(Class<L>)
where CAP#1,CAP#2,CAP#3 are fresh type-variables:
CAP#1 extends List<?> from capture of ? extends List<?>
CAP#2 extends List<?> from capture of ? extends List<?>
CAP#3 extends Object from capture of ?
A workaround is to locally assign a variable:
import java.util.List;
public class Test {
static final void a(Class<? extends List<?>> type) {
// Workaround here
List<?> variable = newList(type);
b(variable);
}
static final <T> List<T> b(List<T> list) {
return list;
}
static final <L extends List<?>> L newList(Class<L> type) {
try {
return type.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
I know that type inference has changed a lot in Java 8 (e.g. due to JEP 101 "generalized target-type inference"). So, is this a bug or a new language "feature"?
EDIT: I have also reported this to Oracle as JI-9021550, but just in case this is a "feature" in Java 8, I've reported the issue also to Eclipse:
Disclaimer - I don't know enough about the subject, and the following is an informal reasoning of mine to try to justify javac's behavior.
We can reduce the problem to
<X extends List<?>> void a(Class<X> type) throws Exception
{
X instance = type.newInstance();
b(instance); // error
}
<T> List<T> b(List<T> list) { ... }
To infer T
, we have constraints
X <: List<?>
X <: List<T>
Essentially, this is unsolvable. For example, no T
exists if X=List<?>
.
Not sure how Java7 infers this case. But javac8 (and IntelliJ) behaves "reasonably", I'd say.
Now, how come this workaround works?
List<?> instance = type.newInstance();
b(instance); // ok!
It works due to wildcard capture, which introduces more type info, "narrowing" the type of instance
instance is List<?> => exist W, where instance is List<W> => T=W
Unfortunately, this is not done when instance
is X
, thus there is less type info to work with.
Conceivably, the language could be "improved" to do wildcard capture for X too:
instance is X, X is List<?> => exist W, where instance is List<W>
Thanks for the bug report, and thanks, Holger, for the example in your answer. These and several others finally made me question one small change made in the Eclipse compiler 11 years ago. The point was: Eclipse had illegally extended the capture algorithm to apply recursively to wildcard bounds.
There was one example where this illegal change perfectly aligned Eclipse behavior with javac. Generations of Eclipse developers have trusted this old decision more than what we could clearly see in JLS. Today I believe that previous deviation must have had a different reason.
Today I took the courage to align ecj with JLS in this regard and voila 5 bugs that appeared to be extremely hard to crack, have essentially been solved just like that (plus a little tweak here and there as compensation).
Ergo: Yes, Eclipse had a bug, but that bug has been fixed as of 4.7 milestone 2 :)
Here's what ecj will report henceforth:
The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)
It's the wildcard inside a capture bound that doesn't find a rule to detect compatibility. More precisely, some time during inference (incorporation to be precise) we encounter the following constraint (T#0 representing an inference variable):
⟨T#0 = ?⟩
Naively, we could just resolve the type variable to the wildcard, but -- presumably because wildcards are not considered types -- the reduction rules define the above as reducing to FALSE, thus letting inference fail.
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