Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this compile in Java7 and does not in Java8?

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.

like image 302
Timothy Basanov Avatar asked Oct 09 '14 01:10

Timothy Basanov


2 Answers

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.

like image 95
Holger Avatar answered Nov 15 '22 20:11

Holger


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.

like image 37
clstrfsck Avatar answered Nov 15 '22 20:11

clstrfsck