Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics compilation error with ternary operator in Java 8, but not in Java 7

This class compiles ok in Java 7, but not in Java 8:

public class Foo {      public static void main(String[] args) throws Exception {         //compiles fine in Java 7 and Java 8:         Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;         CharSequence foo = foo(aClass);          //Inlining the variable, compiles in Java 7, but not in Java 8:         CharSequence foo2 = foo(true ? String.class : StringBuilder.class);      }      static <T> T foo(Class<T> clazz) throws Exception {         return clazz.newInstance();     } } 

Compilation error:

Error:(9, 29) java: method foo in class Foo cannot be applied to given types; required: java.lang.Class found: true ? Str[...]class
reason: inferred type does not conform to equality constraint(s) inferred: java.lang.StringBuilder equality constraints(s): java.lang.StringBuilder,java.lang.String

Why has this stopped working in Java 8? Is it intentional / a side effect of some other feature, or is it simply a compiler bug?

like image 896
Aleksander Blomskøld Avatar asked Mar 19 '14 14:03

Aleksander Blomskøld


People also ask

Does Java 7 have ternary operator?

As of Java 7, only one of the right-hand expressions of the ternary operator will be evaluated at runtime. In a manner similar to the short-circuit operators, if one of the two right-hand expressions in a ternary operator performs a side effect, then it may not be applied at runtime.

Does Java 8 have ternary operator?

Hence with a Java 8 class library the result type of the ternary expression is Executable rather than Member . Some (pre-release) versions of the Java 8 compiler seem to have produced an explicit reference to Executable inside generated code when compiling the ternary operator.

Can we use ternary operator in if condition in Java?

Java ternary operator is the only conditional operator that takes three operands. It's a one-liner replacement for the if-then-else statement and is used a lot in Java programming. We can use the ternary operator in place of if-else conditions or even switch conditions using nested ternary operators.


2 Answers

I'm going to go out on a limb and say that this error (while it may or may not conform to the updated JLS, which I admit I haven't read in detail) is due to an inconsistency in type handling by the JDK 8 compiler.

In general the ternary operator used the same type inference as if for a two-argument method which had the formal parameters both based on the same type parameter. For instance:

static <T> T foo(Class<? extends T> clazz, Class<? extends T> clazz2) { return null; }  public static void main(String[] args) {     CharSequence foo2 = foo(String.class, StringBuilder.class); } 

In this example, T can be inferred to be a capture of ? extends Object & Serializable & CharSequence. Now similarly, in JDK 7, if we go back to your original example:

CharSequence foo2 = foo(true ? String.class : StringBuilder.class); 

This does almost the exact same type inference as above, but in this case consider the ternary operator to be a method as such:

static <T> T ternary(boolean cond, T a, T b) {     if (cond) return a;     else return b; } 

So in this case, if you pass String.class and StringBuilder.class as the parameters, the inferred type of T is (roughly speaking) Class<? extends Object & Serializable & CharSequence>, which is what we wanted.

In fact you can replace the use of your ternary operator in the original snippet with this method, thus:

public class Foo {      public static void main(String[] args) throws Exception {         //compiles fine in Java 7 and Java 8:         Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;         CharSequence foo = foo(aClass);          //Inlining the variable using 'ternary' method:         CharSequence foo2 = foo(ternary(true, String.class, StringBuilder.class));      }      static <T> T foo(Class<T> clazz) throws Exception {         return clazz.newInstance();     }      static <T> T ternary(boolean cond, T a, T b) {         if (cond) return a;         else return b;     } } 

... And now it compiles in Java 7 and 8 (edit: actually it also fails with Java 8! edit again: it now works, Jdk 8u20). What gives? for some reason an equality constraint is now being imposed on T (in the foo method), rather than a lower-bounds constraint. The relevant section of the JLS for Java 7 is 15.12.2.7; for Java 8 there's a whole new chapter on type inference (chapter 18).

Note that explicitly typing T in the call to 'ternary' does allow compilation with Java 7 and 8, but this doesn't seem like it should be necessary. Java 7 does the right thing, where Java 8 gives an error even though there is an appropriate type that can be inferred for T.

like image 144
davmac Avatar answered Sep 21 '22 14:09

davmac


This is not a javac bug, according to the current spec. I wrote an answer here is SO for a similar issue. Here the problem is more or less the same.

On an assignment or invocation context reference conditional expressions are poly expressions. This means that the type of the expression is not the result of applying capture conversion to lub(T1, T2), see JSL-15.25.3 for a detailed definition of T1 and T2. Instead we have, also from this portion of the spec that:

Where a poly reference conditional expression appears in a context of a particular kind with target type T, its second and third operand expressions similarly appear in a context of the same kind with target type T.

The type of a poly reference conditional expression is the same as its target type.

So this means that the target type is pushed down to both operands of the reference conditional expression, and both operands are attributed against that target type. So the compiler ends up gathering constraints from both operands, leading to an unsolvable constraint set and thus an error.


OK, but why do we get equality bounds for T here?

Let's see in detail, from the call:

foo(true ? String.class : StringBuilder.class) 

where foo is:

static <T> T foo(Class<T> clazz) throws Exception {     return clazz.newInstance(); } 

We have that as we are invoking method foo() with the expression true ? String.class : StringBuilder.class. This reference conditional expression should be compatible in a loose invocation context with type Class<T>. This is represented as, see JLS-18.1.2:

true ? String.class : StringBuilder.class → Class<T> 

As follows from JLS-18.2.1 we have that:

A constraint formula of the form ‹Expression → T› is reduced as follows:

...

  • If the expression is a conditional expression of the form e1 ? e2 : e3, the constraint reduces to two constraint formulas, ‹e2 → T› and ‹e3 → T›.

This implies that we obtain the following constraint formulas:

String.class → Class<T> StringBuilder.class → Class<T> 

or:

Class<String> → Class<T> Class<StringBuilder> → Class<T> 

Later from JLS-18.2.2 we have that:

A constraint formula of the form ‹S → T› is reduced as follows:

...

  • Otherwise, the constraint reduces to ‹S <: T›.

I'm only including the related parts. So going on we have now:

Class<String> <: Class<T> Class<StringBuilder> <: Class<T> 

From JLS-18.2.3, we have:

A constraint formula of the form ‹S <: T› is reduced as follows:

...

  • Otherwise, the constraint is reduced according to the form of T:
    • If T is a parameterized class or interface type, or an inner class type of a parameterized class or interface type (directly or indirectly), let A1, ..., An be the type arguments of T. Among the supertypes of S, a corresponding class or interface type is identified, with type arguments B1, ..., Bn. If no such type exists, the constraint reduces to false. Otherwise, the constraint reduces to the following new constraints: for all i (1 ≤ i ≤ n), ‹Bi <= Ai›.

So as Class<T>, Class<String> and Class<StringBuilder> are parameterized classes, this implies that now our constraints reduces to:

String <= T StringBuilder <= T 

Also from JLS-18.2.3, we have:

A constraint formula of the form ‹S <= T›, where S and T are type arguments (§4.5.1), is reduced as follows:

...

  • If T is a type:
    • If S is a type, the constraint reduces to ‹S = T›.

Thus we end up with these constraints for T:

String = T StringBuilder = T 

Finally at JLS-18.2.4 we have that:

A constraint formula of the form ‹S = T›, where S and T are types, is reduced as follows:

...

  • Otherwise, if T is an inference variable, α, the constraint reduces to the bound S = α.

And there is no solution for type variable T with bounds T = String and T = StringBuilder. There is no type the compiler can substitute T for that satisfies both restrictions. For this reason the compiler displays the error message.


So javac is OK according to the current spec, but is the spec correct on this? Well there is a compatibility issue between 7 and 8 that should be investigated. For this reason I have filed JDK-8044053 so we can track this issue.

like image 24
Vicente Romero Avatar answered Sep 21 '22 14:09

Vicente Romero