8

If I understand it correctly, Integer[] is a subtype of Object[]. You can for instance do

Object[] objs = new Integer[] { 1, 2, 3 };

While playing around with var-args I realized, that it seems like the compiler "over approixmates" the array type for no obvious reason.

The program below for instance, prints 123 123. Wouldn't it make sense / be more precise if it printed 123 6?

class Test {

    public static Object combine(Object... objs) {

        if (objs instanceof Integer[]) {

            int sum = 0;
            for (Integer i : (Integer[]) objs)
                sum += i;
            return sum;

        } else {

            String concat = "";
            for (Object o : objs)
                concat += o;
            return concat;

        }
    }

    public static void main(String[] args) {
        System.out.println(combine("1", "2", "3"));  // prints 123
        System.out.println(combine(1, 2, 3));        // prints 123
    }
}

I guess my question could be summed up as: Would any contradiction / problem arise if the JLS was defined to pass T[] as argument, where T was the least upper bound of the types of all arguments given?


Edit: I realize that I, in this particular case, could overload the the combine method to take Integer[] as well (ideone demo). Still, the question remains of why this design was chosen.

aioobe
  • 413,195
  • 112
  • 811
  • 826
  • possible duplicate of [Java: overloaded method resolution and varargs -- confusing example](http://stackoverflow.com/questions/6032901/java-overloaded-method-resolution-and-varargs-confusing-example) – Buhake Sindi Jun 30 '11 at 13:36
  • I've seen that question (and upvoted it!) That question concerns overloading though, which is a completely different issue as I see it? – aioobe Jun 30 '11 at 13:37
  • 1
    Agreed, it's a different issue. This question is about the compiler's packaging of the varargs array, not about method resolution. – Jason S Jun 30 '11 at 13:43
  • Sorry, My fault. I don't unknow how to undo the closed vote. – Buhake Sindi Jun 30 '11 at 13:49

6 Answers6

6

As to this specific question:

Would any contradiction / problem arise if the JLS was defined to pass T[] as argument, where T was the least upper bound of the types of all arguments given?

Yes, because the array is not read-only; it's writable:

package com.example.test;

import java.util.Arrays;

public class Varargs3 {
    public static Object[] changeLastArgument(Object... objs) {
        if (objs.length > 0)
            objs[objs.length-1] = "Pow!";
        return objs;
    }

    public static void main(String[] args) {
        System.out.println(
            Arrays.toString(changeLastArgument(1,2,3))
        );
    }
}

which prints

[1, 2, Pow!]

If the JLS was defined the way you are asking (e.g. for foo(T... args), if you call foo(a,b,c) then the compiler constructs an array of the least upper bound of the types a,b,c), then this case would allow a runtime error: the invocation of changeLastArgument(1,2,3) would create an array of type Integer[], but the changeLastArgument() method would attempt to assign "Pow!" to the last element and you'd get a runtime error.

The declaration of changeLastArgument() is specifying its input types, and therefore it should be able to assume its input argument is truly an Object[] and not a subtype of Object[], so that it can safely modify the input arguments. (This is similar to the PECS principle -- in order for a List<T> to be both safely readable and writable, you can't use any wildcards like List<? extends T> -- which is safely readable but not safely writable -- or List<? super T> -- which is safely writable but not safely readable.)

Community
  • 1
  • 1
Jason S
  • 184,598
  • 164
  • 608
  • 970
  • Hah! I think you nailed it! Passing a `new Integer[] {1, 2, 3}` results in an `ArrayStoreException` as you point out. You deserve a Nice answer badge for this IMO :-) – aioobe Jun 30 '11 at 14:21
  • @aioobe this is not a killer argument. you can pass one yourself: `Arrays.toString(changeLastArgument(new Integer[]{1,2,3}))` and get ArrayStoreException. – irreputable Jun 30 '11 at 22:11
  • Sure, but then it's at least "my fault". It would be *far worse* if this was the default behavior when simply doing `combine(1, 2, 3)`. – aioobe Jun 30 '11 at 22:12
3

To print 6 as an result, the compiler would have to be clever enough to realize, that all arguments can be boxed into a similar wrapper class.

I guess, this is just too much effort or too difficult to specify correctly for some very rare cases.


Besides the question, well, it looks like, the simple rule is: the array is always of type Object[] (if the varargs type is Object), here's some demonstration code:

public static void main (String[] args) {
    temp("1", "2", "3");
    temp(1,2,3);
    temp(String.class, Integer.class);
}

public static void temp(Object... objs) {
    System.out.println(objs.getClass());
}

Output:

class [Ljava.lang.Object;
class [Ljava.lang.Object;
class [Ljava.lang.Object;
aioobe
  • 413,195
  • 112
  • 811
  • 826
Andreas Dolk
  • 113,398
  • 19
  • 180
  • 268
  • Aha, that's a good theory, +1. An identical specification already exists though for overloaded methods, where "the most precise" method gets called. – aioobe Jun 30 '11 at 13:32
  • Regarding your edit: Yes, I know. The parameter type defines how the argument gets compiled. My question is, since `Integer[]` is a subtype of `Object[]` why doesn't the compiler compile the argument type to the most specific type, i.e., `Integer[]` instead of `Object[]`? – aioobe Jun 30 '11 at 13:36
  • But why should it be the most precise in this case? There's no requirement; it needs to match a method, and once it does that, there's no requirement to try to be clever and allocate Integer[] instead of Object[]. – Jason S Jun 30 '11 at 13:36
  • I thought the example in my question provided an example of why I might benefit from getting the most precise type. – aioobe Jun 30 '11 at 13:38
  • @aioobe (the `throws` was an ideone artifact ;) ) – Andreas Dolk Jun 30 '11 at 13:57
1

It looks to me like combine(1, 2, 3) will yield a int[] rather than Integer[]. Since an int[] array is not an instance of an Integer[] array, the first check fails, and you fall back to the concat block.

Ray
  • 4,829
  • 4
  • 28
  • 55
  • 1
    Uhm, no, since `int[]` is not a subtype of `Object[]`. You can't do `Object[] o = new int[] {1, 2, 3 };`. Put another way, there's no way to pass an `int[]` to a method accepting an `Object[]`. – aioobe Jun 30 '11 at 13:26
1

The JLS specifies this behavior (creation of an array of elements of the type that is the variable arity parameter type, i.e. if the vararg method is foo(Bar bar, Baz baz, T...) then the array created on method invocation is of type T[]), if you find the right spot:

From JLS 8.4.1 (Oracle site having trouble at the moment, I had to use the Internet Archive):

If the last formal parameter is a variable arity parameter of type T, it is considered to define a formal parameter of type T[]. The method is then a variable arity method. Otherwise, it is a fixed arity method. Invocations of a variable arity method may contain more actual argument expressions than formal parameters. All the actual argument expressions that do not correspond to the formal parameters preceding the variable arity parameter will be evaluated and the results stored into an array that will be passed to the method invocation (§15.12.4.2).

From JLS 15.12.4.2:

15.12.4.2 Evaluate Arguments The process of evaluating of 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 (§8.4.1) 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 kth 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}).

The argument expressions (possibly rewritten as described above) are now evaluated to yield argument values. Each argument value corresponds to exactly one of the method's n formal parameters.

The argument expressions, if any, are evaluated in order, from left to right. If the evaluation of any argument expression completes abruptly, then no part of any argument expression to its right appears to have been evaluated, and the method invocation completes abruptly for the same reason.The result of evaluating the jth argument expression is the jth argument value, for 1 <= j <= n. Evaluation then continues, using the argument values, as described below.

So I maintain my original answer (see below).


I believe the answer is in the declaration:

public static Object combine(Object... objs)

The compiler matches this method, and therefore for varargs it allocates an Object[]. There is no reason for it to allocate an Integer[].


trial test:

package com.example.test;
public class Varargs1 {
    public static void varargs(Object... objs) {
        System.out.println(objs.getClass());
    }

    public static void main(String[] args) {
        varargs("1", "2", "3");
        varargs(1, 2, 3);

        Integer[] ints = {1,2,3};
        varargs(ints); // Eclipse yields the following warning:
        /* 
         * The argument of type Integer[] should explicitly be 
         * cast to Object[] for the invocation of the varargs 
         * method varargs(Object...) from type Varargs1. 
         * It could alternatively be cast to Object for a 
         * varargs invocation
         */
    }
}

which prints:

class [Ljava.lang.Object;
class [Ljava.lang.Object;
class [Ljava.lang.Integer;

Finally, if you want the compiler to be more specific, use generic methods:

package com.example.test;

public class Varargs2 {
    public static <T> void varargs(T... objs) {
        System.out.println(objs.getClass());
    }

    public static void main(String[] args) {
        varargs("1", "2", "3");
        varargs(1, 2, 3);
        varargs(1, "2", 3); // warning from Eclipse:
        /*
         * Type safety : A generic array of 
         * Object&Comparable<?>&Serializable
         * is created for a varargs parameter
         */
    }
}

which prints:

class [Ljava.lang.String;
class [Ljava.lang.Integer;
class [Ljava.lang.Comparable;
Jason S
  • 184,598
  • 164
  • 608
  • 970
  • Yes, I know. I did the `getClass` check myself. My question is *why* doesn't the compiler / JLS allocate an `Integer[]` instead? It would be more precise, no? – aioobe Jun 30 '11 at 13:30
  • The JLS says exactly what the JLS says, regardless of whether something else would be better. Although there's probably a symmetry here: from the method resolution side of varargs, it finds the most specific method; from the consumer (calling) side of varargs, it finds the *least* specific type (which is the type of the declaring method's variable arity parameter) – Jason S Jun 30 '11 at 13:56
  • My question is not about how it works, or what the JLS says. My question is, as stated from the beginning, *Would any contradiction / problem arise if the JLS was defined to pass T[] as argument, where T was the least upper bound of the types of all arguments given?* – aioobe Jun 30 '11 at 14:04
  • Ah... ok, somehow I missed that part. – Jason S Jun 30 '11 at 14:09
  • Added a new answer which answers that question. – Jason S Jun 30 '11 at 14:15
0

Well you are not sending a Integer[] into combine function. That's why it's not working as you expect.

Use

System.out.println(combine(new Integer[] {1, 2, 3}));

to make it work.

styken
  • 74
  • 6
  • Ah, good point. However, why don't the compiler / JLS do exactly this for me if I provide it with the arguments `1, 2, 3` ? – aioobe Jun 30 '11 at 13:29
0

My best guess is that you haven't specified a type that the varargs should incur.

The following shows what I mean:

/**
 * @author The Elite Gentleman.
 *
 */
public class Test {

    public static Object combine(Object... objs) {
        System.out.println("combine()");
        System.out.println(objs.getClass().getName());

        if (objs instanceof Integer[]) {

            int sum = 0;
            for (Integer i : (Integer[]) objs)
            sum += i;
            return sum;

        } else {

            String concat = "";
            for (Object o : objs) {
            System.out.println(o.getClass().getName());
            concat += o;
            }
            return concat;

        }
    }


    public static void main(String[] args) {
        System.out.println("1");
        System.out.println(combine(new String[] {"1", "2", "3"}));
        System.out.println(combine(new Integer[] {1, 2, 3}));

        System.out.println("2");
        System.out.println(combine("1", "2", "3"));
        System.out.println(combine(1, 2, 3));
    }
}

Output:

1
combine()
[Ljava.lang.String;
java.lang.String
java.lang.String
java.lang.String
123
combine()
[Ljava.lang.Integer;
6
2
combine()
[Ljava.lang.Object;
java.lang.String
java.lang.String
java.lang.String
123
combine()
[Ljava.lang.Object;
java.lang.Integer
java.lang.Integer
java.lang.Integer
123

It's clear that by not passing an "untyped" array, the JVM converts it to an Object[] that is passed to the combine() method.

PS, I couldn't find the JLS as the Oracle Server is down.

Buhake Sindi
  • 87,898
  • 29
  • 167
  • 228
  • I don't understand your sentence about "generics". Care to elaborate? – aioobe Jun 30 '11 at 13:58
  • Right. Still, that wasn't my question :P My question was why JLS is designed to "over approximate" the type, and why it's not defined to go for the most precise type. – aioobe Jun 30 '11 at 14:06
  • I guess I over-read the question....lol...I need to carefully read the JLS to understand (*cough* Oracle) when the server comes back up – Buhake Sindi Jun 30 '11 at 14:11