Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ClassCastException in varargs while using Java-8

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

like image 709
Sachin Sachdeva Avatar asked Dec 18 '19 06:12

Sachin Sachdeva


4 Answers

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).

like image 124
ernest_k Avatar answered Nov 15 '22 06:11

ernest_k


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.

like image 38
Navin Gelot Avatar answered Nov 15 '22 04:11

Navin Gelot


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
like image 21
Melan Avatar answered Nov 15 '22 04:11

Melan


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.
like image 41
Nicktar Avatar answered Nov 15 '22 05:11

Nicktar