Generics are tricky.
And looks like they are treated differently in different versions of Java.
This code successfully compiles in Java 7 and fails to compile with Java 8.
import java.util.EnumSet;
public class Main {
public static void main(String[] args) {
Enum foo = null;
tryCompile(EnumSet.of(foo));
}
static <C extends Enum<C> & Another> void tryCompile(Iterable<C> i) {}
static interface Another {}
}
Here is an error message from Java 8. I used this one to compile it: http://www.compilejava.net/
/tmp/java_A7GNRg/Main.java:6: error: method tryCompile in class Main cannot be applied to given types;
tryCompile(EnumSet.of(foo));
^
required: Iterable<C>
found: EnumSet
reason: inferred type does not conform to upper bound(s)
inferred: Enum
upper bound(s): Enum<Enum>,Another
where C is a type-variable:
C extends Enum<C>,Another declared in method <C>tryCompile(Iterable<C>)
/tmp/java_A7GNRg/Main.java:6: warning: [unchecked] unchecked method invocation: method of in class EnumSet is applied to given types
tryCompile(EnumSet.of(foo));
^
required: E
found: Enum
where E is a type-variable:
E extends Enum<E> declared in method <E>of(E)
1 error
1 warning
The question is about the difference between versions of Java compiler.
The main difference between Java 7 and Java 8 is the target type inference. While Java 7 only considers the parameters of a method invocation to determine the type arguments, Java 8 will use the target type of an expression, i.e. the parameter type in case of a nested method invocation, the type of the variable that is initialized or assigned to, or the method’s return type in case of a return
statement.
E.g. when writing, List<Number> list=Arrays.asList(1, 2, 3, 4);
, Java 7 will infer the type List<Integer>
for the right hand side by looking at the method’s arguments and generate an error while Java 8 will use the target type List<Number>
to infer the constraint that the method arguments must be instances of Number
which is the case. Therefore, it is legal in Java 8.
If you are interested in the formal details, you may study the “Java Language Specification, Chapter 18. Type Inference”, especially §18.5.2. Invocation Type Inference, however, that’s not easy reading…
So what happens when you say Enum foo = null; tryCompile(EnumSet.of(foo));
?
In Java 7 the type of the expression EnumSet.of(foo)
will be inferred by looking at the type of the argument, foo
which is the raw type Enum
, hence an unchecked operation will be performed and the result type is the raw type EnumSet
. This type implements the raw type Iterable
and hence can be passed to tryCompile
forming another unchecked operation.
In Java 8 the target type of EnumSet.of(foo)
is the type of the first parameter of tryCompile
which is Iterable<C extends Enum<C> & Another>
, so without going too much into details, in Java 7 EnumSet.of
will be treated as raw type invocation because it has a raw type argument, in Java 8 it will be treated as generic invocation because it has a generic target type. By treating it as as a generic invocation, the compiler will conclude that the type found (Enum
) is not compatible to the required type C extends Enum<C> & Another
. While you could get away with assigning the raw type Enum
to C extends Enum<C>
with an unchecked warning, it will considered to be incompatible with Another
(without a type-cast).
You can indeed insert such a cast:
Enum foo = null;
tryCompile(EnumSet.of((Enum&Another)foo));
This compiles, of course not without an unchecked warning due to the assignment of Enum
to C extends Enum<C>
.
You can also dissolve the target type relationship so that the same steps as in Java 7 are performed:
Enum foo = null;
EnumSet set = EnumSet.of(foo);
tryCompile(set);
Here, raw types are used throughout the three lines so this compiles with unchecked warnings and the same ignorance about the implements Another
constraint as in Java 7.
The type inference engine in Java 8 has been improved, and (I assume) is now able to determine that the C
type does not extend Another
.
In Java 7 the type inference system wasn't able to, or didn't bother determining that the Another
type was missing, and gave the programmer the benefit of the doubt (at compile time).
You will still pay for the transgression at runtime if you call methods on the Another
interface at runtime in Java 7.
For example, this code:
import java.util.EnumSet;
public class Main {
static enum Foo {
BAR
}
public static void main(String[] args) {
Enum foo = Foo.BAR;
tryCompile(EnumSet.of(foo));
}
static <C extends Enum<C> & Another> void tryCompile(Iterable<C> i) {
i.iterator().next().doSomething();
}
static interface Another {
void doSomething();
}
}
Will produce this error at runtime:
Exception in thread "main" java.lang.ClassCastException: Main$Foo cannot be cast to Main$Another
at Main.tryCompile(Main.java:16)
at Main.main(Main.java:12)
Even though the Java 7 compiler will compile the code, it still gives warnings about raw types and unchecked invocations which should alert you to something being amiss.
Here is a pretty straightforward example not using an enum, but modeled on the definition of Enum
that exhibits the same problem. Compiles with warnings in Java 7, but not in Java 8:
import java.util.Collections;
import java.util.List;
public class Main {
static class Foo<T extends Foo<T>> {
}
static class FooA extends Foo<FooA> {
}
public static <T extends Foo<T>> List<T> fooList(T e) {
return Collections.singletonList(e);
}
public static void main(String[] args) {
Foo foo = new FooA();
tryCompile(fooList(foo));
}
static <C extends Enum<C> & Another> void tryCompile(Iterable<C> i) {
i.iterator().next().doSomething();
}
static interface Another {
void doSomething();
}
}
So it isn't an Enum
specific issue, but it may be because of the recursive types involved.
Looks like a correct error to me:
reason: inferred type does not conform to upper bound(s)
inferred: Enum
upper bound(s): Enum<Enum>,Another
EnumSet.of(foo)
will have type EnumSet<Enum>
, which is not compatible with C extends Enum<C> & Another
, for the same reason that Set<Enum>
is not compatible with Set<? extends Enum>
, since Java generics are invariant.
this compiles fine for me in Eclipse Standard/SDK Version: Luna Release (4.4.0) Build id: 20140612-0600 with the Eclipse JDT (Java Development Tools) Patch with Java 8 support (for Kepler SR2) 1.0.0.v20140317-1956 org.eclipse.jdt.java8patch.feature.group Eclipse.org installed.
i get a few warnings (raw type on foo and Unchecked invocation on tryCompile.