32

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?

Aleksander Blomskøld
  • 18,374
  • 9
  • 76
  • 82

3 Answers3

17

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.

davmac
  • 20,150
  • 1
  • 40
  • 68
  • 1
    I was writing up the same answer. I kind of agree with you. – Sotirios Delimanolis Mar 19 '14 at 16:27
  • 1
    Ahem, I tried it just with Netbeans 7.4 and `javac` of the RC and it produces the very same error message for the `ternary(…,…,…)` method as for the ternary operator `…?…:…`. – Holger Mar 20 '14 at 11:53
  • @Holger, you are correct, my mistake. I have edited the answer accordingly. – davmac Mar 20 '14 at 13:33
  • @davmac IMO this answer is incorrect, please check my answer in this thread – Vicente Romero May 27 '14 at 02:11
  • @vrz see my comment on your answer. – davmac May 27 '14 at 11:25
  • @davmac. I think that your answer needs some editing. My answer in this thread proves, by following the spec closely, that the compiler is doing exactly what the spec says. So we are in the worst case talking about a spec bug. If this is a spec bug or not this is something that spec experts have to figure out. But I think that you should be strict in your answer. – Vicente Romero Jun 05 '14 at 15:23
  • @VicenteRomero I have already, long before you made this comment, edited my answer to include reference to the problematic section of the spec. I think my answer is quite clear as it stands. – davmac Jun 09 '14 at 15:39
16

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.

Community
  • 1
  • 1
Vicente Romero
  • 1,460
  • 13
  • 16
  • This may be worth examining very closely. What you're suggesting is that there was an error with the way that the JLS was implemented in Java 7 as opposed to Java 8. I don't have the time to pour over the changelog between the versions, but it could *seem* like that's the case, given your explanation. – Makoto May 27 '14 at 04:31
  • Although you've followed the JLS closely and may have identified why it comes to an error (I don't have time to double-check right now), I don't see why the JLS-18.2.4 requirement is necessary. Similar problems have come up elsewhere, see https://bugs.openjdk.java.net/browse/JDK-8043980 - here, it seems to have been recognised as a compiler bug. It may be that the JLS is wrong - this wouldn't be the first time that's happened either; see http://stackoverflow.com/questions/5385743/java-casting-is-the-compiler-wrong-or-is-the-language-spec-wrong-or-am-i-wron for example. – davmac May 27 '14 at 11:23
  • @Makoto, no what I'm saying is that given the current state of the spec javac is giving the right answer. I have edited my answer to stress this point. I have also added a reference to a spec bug I have just filed to investigate this issue and check if there is a spec bug or not. – Vicente Romero May 27 '14 at 19:57
  • @davmac [JDK-8043980](https://bugs.openjdk.java.net/browse/JDK-8043980) can't be reproduced and thus has been closed. I will check later the question in SO that you are referring to. My answer is just trying to prove that according to the current spec javac is right. It has to be analyzed if there is a spec bug here but right not we can only speculate about this. – Vicente Romero May 27 '14 at 20:01
  • @vrz please read the comments: "Cannot reproduce in latest langtools repo. It seems like this has already been addressed." - it was reproducable, just not with the latest (repo) javac. However, it may refer to a different issue. I'll be interested to see the response to the bug you filed. Thanks – davmac May 28 '14 at 13:03
  • @davmac yes if a bug is not reproducible anymore then there is nothing else you can do. In principle the fix will be available at some point. Regarding your other comment about why JLS-18.2.4 is necessary. I don't think that this is the root problem here this is at the end of the reductions and there is not much you can do at this point. I think that the main issue is at the start point when the same target is used to gather restrictions from both operands in the ternary operator. Is this better than typing the whole expression using the lub as for JDK7? This is the key point IMO. – Vicente Romero May 29 '14 at 22:11
  • @vrz the target type being included in the constraint development is the crux of the type inference improvement in Java 8. Your statement in the answer that "So the compiler ends up gathering constraints from both operands, leading to an unsolvable constraint set" seems incorrect to me; in Java 7 both operands generate constraints in the same way. The problem is that in Java 8, according to your reading, 18.2.3 turns a "contains" constraint into an "equals" constraint for no good reason. However I'm not even sure that your reading is correct since it requires that `T` is a type. – davmac May 30 '14 at 12:26
  • @davmac, no you are looking at the wrong place, you can't modify 18.2.3 for ternary operators only. If a spec modification is done for ternary operators then IMO what you need to modify is JLS-18.2.1. You can't modify something that is working for a lot of cases not only for ternary operators. Clarification: I'm not saying that the spec should be modify. I don't decide on that. I'm just saying: in the hypothetical case that the spec is modified. – Vicente Romero Jun 05 '14 at 15:15
1

The problem appears in the context of parameters and assignments only. I.e.

CharSequence cs1=(true? String.class: StringBuilder.class).newInstance();

works. Unlike the other answer claims, using a generic <T> T ternary(boolean cond, T a, T b) method does not work. This still fails when the invocation is passed to a generic method like <T> T foo(Class<T> clazz) so that the actual type of <T> is searched. However it works in the assignment example

Class<? extends CharSequence> aClass = true ? String.class : StringBuilder.class;

as the resulting type is already specified explicitly. Of course, a type cast to Class<? extends CharSequence> would always fix that for the other use cases too.

The problem is that the algorithm is specified as finding the “least upper bound” of the second and third operand first and apply “capture conversion” afterwards. The first step already fails for the two types Class<String> and Class<StringBuilder> so the second step which would consider the context is not even attempted.

This is not a satisfying situation, however, in this special case there is an elegant alternative:

public static void main(String... arg) {
  CharSequence cs=foo(true? String::new: StringBuilder::new);
}

static <T> T foo(Supplier<T> s) { return s.get(); }
Holger
  • 285,553
  • 42
  • 434
  • 765
  • 3
    "The problem is that the algorithm is specified as finding the “least upper bound” of the second and third operand first and apply “capture conversion” afterwards. The first step already fails for the two types Class and Class" - but *why* does it fail? The least upper bound should be `Class extends Object & CharSequence & Serializable>`. – davmac Mar 20 '14 at 13:43
  • @davmac: I didn’t say that it is correct, I just nailed it down to this point but like you I couldn’t identity the relevant part for that “identity constraint” in the new spec (yet). Maybe I get through it when I have more time but I wanted to tell what I have found so far. But in my opinion a specification so hard to read *must* yield to problems in the implementation… – Holger Mar 21 '14 at 08:55
  • @Holger, good observation. If the reference conditional expression doesn't appear in an assignment or invocation context, then it's considered a standalone expression. As it's not a poly expression then the javac 8 machinery is not applied to it and it's typing is equivalent to the one obtained by javac 7. – Vicente Romero May 27 '14 at 04:20