Home > Mobile >  Var-Arg method infers Generic Type to be both Object & primitive[] simultaneously
Var-Arg method infers Generic Type to be both Object & primitive[] simultaneously

Time:08-21

Unanticipated behavior for Generics and Var-Arg

   public static <T> void method(T singleVal, T ... vals) {
      System.out.println(singleVal);
      for(T val : vals) {
        System.out.println(val);
      }
   }

called via:

   method(0, new int[]{1,2,3});
   //method('a', new char[] {'b','c','d'});  //same behavior

Prints:

   0
   [I@7699a589   //<--- an array at element 0

It happens only when passing it a primitive[].

With int[] in, T becomes both Integer and int [] simultaneously?

debugEditorVals

When passing it an Integer[] instead of int [], it behaves as anticipated:

method(0, new Integer[]{1,2,3});
//method('a', new Character[] {'b','c','d'});  //same behavior

0
1
2
3

CodePudding user response:

I believe the answer comes from JLS §15.12.4.2. Evaluate Arguments. We've already done overload resolution trivially (there's only one overload in this example), so we know the target method. It's just a matter of evaluating the arguments and making the call.

The process of evaluating the argument list differs, depending on whether the method being invoked is a fixed arity method or a variable arity method (§8.4.1).

If the method being invoked is a variable arity method m, it necessarily has n > 0 formal parameters. The final formal parameter of m necessarily has type T[] for some T, and m is necessarily being invoked with k ≥ 0 actual argument expressions.

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[].

Now let's see how this word soup applies to your example. The method in question is

public static <T> void method(T singleVal, T ... vals)

And the argument list is

method(0, new int[]{1,2,3});

The method m has two formal arguments, of types T and T[]. Since the final argument is of array type and is marked with the variable-arity designator ..., this is a variable arity method. We're calling it with two arguments, so k = n in the above quote. Now, the first argument is of type int, which is not valid as a generic argument, so autoboxing takes that to Integer. That means T is Integer. The second argument is int[]. Since k = n, we're going to wrap this in an array if (and only if) our second argument is not assignment compatible with Integer[]. And the second argument is int[], not Integer[]. Autoboxing won't convert a whole array, so we assume we need to wrap this.

Now, you reasonably ask why this function call can happen at all. After all, we already decided T was Integer, and now we're saying T is int[]? Well, I lied. Or, rather, I oversimplified. We didn't decide T was Integer. We decided T was compatible with Integer. Basically, we decided that T was some supertype of Integer. In the absence of any additional information, Java would conclude that T should be Integer, but it is also happy to widen that during type inference if needed.

In your case, we've already decided that int[] is not compatible with Integer[] (or with U[] for any supertype U of Integer, to be precise), since int is a primitive and we can't autobox on the inside of an array. So we've already made the decision that this is a variable argument call that will be wrapped in an array. That is, the call is getting converted to

method(new Integer(0), new int[][]{new int[]{1, 2, 3}});

It's just a matter of finding a T that works with the method signature

// Note: No T... now, we've already made that decision, so it's T[] now.
public static <T> void method(T singleVal, T[] vals)

That is, we need a type T that is a common supertype of both Integer and int[]. Integer has several supertypes: Number, Comparable<Integer>, Serializable, etc. But only one type is a supertype of both Integer and an array type: Object. That is, you've made a call with T = Object.

The lesson here, I think, is that generics and varargs don't play nice, an when autoboxing gets in the mix, they definitely don't play nice. Personally, I only use variable arguments with concrete types (so int... or String... are fair game, but I'd never write T...), just to avoid messy business like this.

  • Related