5

The following code is working fine for m2() but is throwing a ClassCastException when I use m1().

The only difference between m1 and m2 is the number of arguments.

public class Test  {

  public static void m1() {
        m3(m4("1"));
    }

    public static void m2() {
        m3(m4("1"), m4("2"));
    }

    public static void m3(Object... str) {
        for (Object o : str) {
            System.out.println(o);
        }
    }

    public static <T> T m4(Object s) {
        return (T) s;
    }

    public static void main(String[] args) {
        m1();
   }
 }

My question is - Does varargs not work with a single argument when we use generics?

PS : This is not related to ClassCastException using Generics and Varargs

Sachin Sachdeva
  • 10,526
  • 37
  • 100

4 Answers4

10

Let's skip the fact that you ignored an unchecked cast warning for now and try to understand why this happened.

In this statement:

Test.m3(Test.m4("1"));

There is one inferred type, which is the return type of m4. If one is to use it outside the m3 invocation context, as in:

Test.m4("1"); // T is Object

T is inferred as Object. One can use a type witness to force the compiler to use a given type:

Test.<String>m4("1"); // T is String

...or by using the expression in an assignment context:

String resString = Test.m4("1"); // T is String
Integer resInt = Test.m4("1"); // T is Integer <-- see the problem?

... or in an invocation context:

Integer.parseInt(Test.m4("1")); // T is String
Long.toString(Test.m4("1")); // T is Long

Now, back to Test.m3(Test.m4("1"));: I couldn't find a reference for this, but I believe the compiler is forced to make T resolve to the parameter type of m3, which is Object[]. This means that T, which has to coincide with the parameter type of m3, is therefore resolved to Object[], and that makes it as though you specified generic types as:

Test.m3(Test.<Object[]>m4("1")); // this is what is happening

Now, because m4 is not returning an Object[], m3 is receiving a String, which leads to the inescapable ClassCastException.

How to solve it?

The first way to fix this is to specify a correct type argument for m4:

Test.m3(Test.<String>m4("1")); 

With this, String is the return type of m4, and m3 is called with a single String object (for the Object... var-arg), as if you had written:

String temp = m4("1");
m3(temp);

The second approach was suggested in @Ravindra Ranwala's deleted answer. In my opinion, this boils down to heeding compiler warnings:

public static <T> T m4(Object s) {
    return (T) s; // unchecked cast
} 

The unchecked cast warning simply tells you that the compiler (and the runtime) are not going to enforce type compatibility, simply because T is not known where you cast. The following version is type-safe, but it also makes the compiler use String as the return type of m4 as well as the type of the parameter to m3:

public static <T> T m4(T s) {
    return s;
}

With this, m3(m4("1")); still uses Object... as the parameter type of m3, while keeping String the return type of m4 (i.e., a string value is used as the first element of the Object array).

ernest_k
  • 39,584
  • 5
  • 45
  • 86
  • 9
    The declaration `public static T m4(Object s)` basically says “it will return whatever the caller wants”, which is, of course broken, unless the method always return `null`, throws an exception or never returns. So the only sensible fix is to change this method. See also https://stackoverflow.com/q/39021934/2711488 And yes, as a rule of thumb, the compiler will use a varargs invocation only when no other invocation is applicable. So when it can invoke the method by inferring an array type, it will do. – Holger Dec 18 '19 at 08:25
  • 1
    @Thanks earnest for digging so much into this – Sachin Sachdeva Dec 18 '19 at 14:52
  • Interesting that the target type for a single expression in argument position to a vararg method seems to be the array type and not the element type... – Lii Dec 25 '19 at 12:33
  • @Lii The parameter type in `m3(Object... str)` is `Object[]`. The inference has to resolve to the parameter type of the `m3` method, not to some element of the argument array (because that one type must coincide with the return type of `m4`, and there can only be one return type) – ernest_k Dec 25 '19 at 17:34
  • @user1729 That surely wouldn't compile. Thank you. Not sure how I'd missed it even after re-reading a few times. I've corrected. – ernest_k Jan 02 '20 at 16:54
  • @ernest_k no problem. Thank you for great input by sharing your knowledge in all your answers! – user1729 Jan 02 '20 at 16:55
4

Because in the method implementation the array is only read and nothing is stored in the array. However, if a method would store something in the array it could attempt to store an alien object in the array, like putting a HashMap<Long,Long> into a HashMap<String,String>[]. Neither the compiler nor the runtime system could prevent it.

Here is another example that illustrates the potential danger of ignoring the warning issued regarding array construction in conjunction with variable argument lists.

static <T> T[] method_1(T t1, T t2) { 
            return method_2(t1, t2);                       // unchecked warning 
        } 
        static <T> T[] method_2( T... args) { 
            return args; 
        } 
        public static void main(String... args) { 
            String[] strings = method_1("bad", "karma");     // ClassCastException 
        } 

warning: [unchecked] unchecked generic array creation of type T[] for varargs parameter

        return method_2(t1, t2); 

As in the previous example, the array's component type is non-reifiable and due to type erasure the compiler does not create a T[] , but an Object[] instead. Here is what the compiler generates:

Example (same a above, after translation by type erasure):

public final class Test {  
        static Object[] method_1( Object t1, Object t2) { 
            return method_2( new Object[] {t1, t2} );                   // unchecked warning 
        } 
        static Object[] method_2( Object[] args) { 
            return args; 
        } 
        public static void main(String[] args) { 
            String[] strings = (String[]) method_1("bad", "karma");       // ClassCastException 
        } 
}

The unchecked warning is issued to alert you to the potential risk of type safety violations and unexpected ClassCastExceptions

In the example, you would observe a ClassCastException in the main() method where two strings are passed to the first method. At runtime, the two strings are stuffed into an Object[]; note, not a String[] .

The second method accepts the Object[] as an argument, because after type erasure Object[] is its declared parameter type. Consequently, the second method returns an Object[] , not a String[] , which is passed along as the first method's return value. Eventually, the compiler-generated cast in the main() method fails, because the return value of the first method is an Object[] and no String[]

Conclusion

It is probably best to avoid providing objects of non-reifiable types where a variable argument list is expected. You will always receive an unchecked warning and unless you know exactly what the invoked method does you can never be sure that the invocation is type-safe.

Navin Gelot
  • 1,030
  • 2
  • 10
  • 26
4

You have to use a Class instance of T to cast since the generic type erasure during compilation

public class Test {

    public static void m1() {
        m3(m4("1", String.class));
    }

    public static void m2() {
        m3(m4("1", String.class), m4("2", String.class));
    }

    public static void m3(final Object... str) {
        for (Object o : str) {
            System.out.println(o);
        }
    }

    public static <T> T m4(final Object s, Class<T> clazz) {
        return clazz.cast(s);
    }

    public static void main(String[] args) {
        m1();
        m2();
    }
}
$java Test
1
1
2
Melan
  • 458
  • 2
  • 14
2

Varargs and Generics don't mix to well in Java. This is because

  • Varags implemented by having an array of the respective type at runtime (array of Object in your case)
  • Arrays and Generics are just incompatible. You can't have an Array of String-Lists.
Nicktar
  • 5,288
  • 1
  • 24
  • 40