Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java generics inconsistent behavior?

Tags:

java

generics

Why does the first method compile, and the second not? The generics for Set and ImmutableSet.Builder are the same, and the type signatures for their add methods are also the same.

import java.util.Set;
import java.util.HashSet;
import com.google.common.collect.ImmutableSet;

public class F {

    public static ImmutableSet<? extends Number> testImmutableSetBuilder() {
        ImmutableSet.Builder<? extends Number> builder = ImmutableSet.builder();
        Number n = Integer.valueOf(4);
        builder.add(n);
        return builder.build();
    }

    public static Set<? extends Number> testJavaSet() {
        Set<? extends Number> builder = new HashSet<Number>();
        Number n = Integer.valueOf(4);
        builder.add(n);
        return builder;
    }
}

I am using javac version 1.7.0_25 to build. I get the following error on the second method, but not on the first. I believe I should get the error in both cases, as it is not type correct to put a Number into a collection of ? extends Number.

error: no suitable method found for add(Number)
        builder.add(n);
               ^
    method Set.add(CAP#1) is not applicable
      (actual argument Number cannot be converted to CAP#1 by method invocation conversion)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
like image 338
sullivan- Avatar asked Oct 23 '13 15:10

sullivan-


2 Answers

Indeed add(E) is not appliccable, but the matter is less clear for the method add(E...):

The Java Language Specification defines:

The method m is an applicable variable-arity method if and only if all of the following conditions hold:

  • For 1 ≤ i < n, the type of ei, Ai, can be converted by method invocation conversion to Si.

  • If k ≥ n, then for n ≤ i ≤ k, the type of ei, Ai, can be converted by method invocation conversion to the component type of Sn.

  • ...

In our case Sn is capture-of-? extends Number[].

Now just what is the component type of Sn? Unfortunately, the JLS does not give a formal definition for that term. In particular, it not said explicitly whether the component type is a compile time type (which need not be reifiable) or a runtime type (which must be). In case of the former, the component type would be capture-or-? extends Number, which cannot accept a Number through method invocation conversion. In case of the latter, the component type would be Number, which obviously can.

It appears that the implementors of the eclipse compiler have used the former definition, while the implementors of javac have used the latter.

Two facts seem to imply that the component type is indeed a runtime type. First, the spec requires:

If the type of the value being assigned is not assignment-compatible (§5.2) with the component type, an ArrayStoreException is thrown.

If the component type of an array were not reifiable (§4.7), the Java Virtual Machine could not perform the store check described in the preceding paragraph. This is why an array creation expression with a non-reifiable element type is forbidden (§15.10). One may declare a variable of an array type whose element type is non-reifiable, but assignment of the result of an array creation expression to the variable will necessarily cause an unchecked warning (§5.1.9).

and second, the runtime evaluation of a method invocation expression targeting a variable arity method is defined as follows:

If m is being invoked with k ≠ n actual argument expressions, or, if m is being invoked with k = n actual argument expressions and the type of the k'th argument expression is not assignment compatible with T[], then the argument list (e1, ..., en-1, en, ..., ek) is evaluated as if it were written as (e1, ..., en-1, new |T[]| { en, ..., ek }), where |T[]| denotes the erasure (§4.6) of T[].

A casual reading of this paragraph might lead one to think that the expression is not just evaluated, but also type checked that way, but the specification doesn't quite say that.

On the other hand, it is quite an odd notion to use the erasure for compile time type checking (though the spec requires this in some other cases).

To summarize, the observed differences between the eclipse compiler and javac appear to stem from a slightly different interpretation of the type checking rules for non-reifiable variable-arity method arguments.

like image 164
meriton Avatar answered Sep 30 '22 03:09

meriton


I think I started to figure out the answer. ImmutableSet.Builder method add is overloaded, there is an alternate signature add(E... elements). I ran javap -v on the resulting .class file and I saw that this alternative method is the one that is actually getting called. The varargs elements is actually a Java array under the covers, and Java arrays are covariant. I.e., in regards to this specific example, we call

builder.add(n);

The Number n is converted into a single-element array of type Number[]. But I don't know how that array is legally converted into an array of <? extends Number>!

like image 29
sullivan- Avatar answered Sep 30 '22 04:09

sullivan-