简体   繁体   中英

Issue with Java varargs and generics in abstract classes

I'm playing with some functional like programming. And having issues with some pretty deeply nested generics. Here's my SCCE that fails, with an abstract class involved:

public abstract class FooGen<IN, OUT> {

    OUT fn2(IN in1, IN in2) {  // clever? try at a lazy way, just call the varargs version
      return fnN(in1, in2);
   }

   abstract OUT fnN(IN...ins); // subclasses implement this

   public static void main(String[] args) {

      FooGen<Number, Number> foogen = new FooGen<Number, Number>() {   
         @Override Number fnN(Number... numbers) { 
            return numbers[0];
         }
      };

      System.out.println(foogen.fn2(1.2, 3.4));           
   }
}

This dies with a

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Number;

However, for a non-abstract FooGen, it works fine:

public class FooGen<IN, OUT> {

      OUT fn2(IN g1, IN g2) { 
         return fnN(g1, g2); 
      }

      OUT fnN(IN...gs) {
         return (OUT)gs[0];
      }

   public static void main(String[] args) {
      FooGen<Number,Number> foogen = new FooGen<Number,Number>();
      System.out.println(foogen.fn2(1.2, 3.4)); 
   }
}

This prints 1.2. Ideas? It seems like somewhere Java has lost track of the generics. This is pushing the limits of my generics knowledge. :-)

(Added in response to answers)

First, thanks for the upvotes, and to Paul and Daemon for their helpful answers.

Still wondering why it works as Numbers in the 2nd version, I had an insight. As a Thought Experiment, let's add a .doubleValue() somewhere. You can't. In the code itself the variables are INs, not Numbers. And in the main() it's merely declaring the type, FooGen<Number,Number> but there's no place there to add code.

In Version #2, it really isn't "working" as Numbers . Internally, with erasure, everything is Objects, as explained by Paul and Daemon, and, looking back sheepishly, well understood by myself. Basically, in this complex example, I got overexcited and mislead by the <Number> declaration.

Don't think I'll bother with a workaround. The whole idea was to be lazy. :-) For efficiency I created parallel interfaces and code that take primitive doubles (and ints), and there this trick works just fine.

Varargs parameters are first and foremost arrays. So without the syntactic sugar, your code would look like the following:

OUT fn2(IN in1, IN in2) {
    return fnN(new IN[] {in1, in2});
}

abstract OUT fnN(IN[] ins);

Except new IN[] would not be legal because arrays of type parameters cannot be instantiated , due to type erasure . An array needs to know its component type, but IN has been erased to its upper bound, Object , at runtime.

The varargs invocation hides this issue unfortunately, and at runtime you have the equivalent of fnN(new Object[] {in1, in2}) , whereas fnN has been overriden to take a Number[] .

However, for a non-abstract FooGen, it works fine

This is because by instantiating FooGen directly, you haven't overridden fnN . Thus it accepts an Object[] at runtime and no ClassCastException occurs.

For example, this will fail even if FooGen isn't abstract :

FooGen<Number, Number> foogen = new FooGen<Number, Number>() {
    @Override
    Number fnN(Number... gs) {
        return super.fnN(gs);
    }
};
System.out.println(foogen.fn2(1.2, 3.4));

So you can see that it really isn't related to the abstractness of FooGen , but to whether fnN gets overridden with a narrowed argument type.

SOLUTION

There are no easy workarounds. One idea is to have fnN take a List<? extends IN> List<? extends IN> instead:

OUT fn2(IN in1, IN in2) {
    //safe because the array won't be exposed outside the list
    @SuppressWarnings("unchecked")
    final List<IN> ins = Arrays.asList(in1, in2);
    return fnN(ins);
}

abstract OUT fnN(List<? extends IN> ins);

If you wanted to keep the varargs support, you could treat this method as an implementation detail and delegate to it:

abstract OUT fnNImpl(List<? extends IN> ins);

public final OUT fnN(IN... ins) {
    return fnNImpl(Arrays.asList(ins));
}

This ClassCastException occurs due to a feature of Java called "type erasure". Type erasure occurs when generics are compiled. Since the Java compiler cannot know the type of a generic class at run-time, it will instead compile the generic objects as instances of Object .

In your code, when FooGen is compiled, fnN(IN... ins) receives a parameter of type Object[] . The ClassCastException occurs when you then attempt to down-cast one of these Objects to your generic type OUT .

This isn't even mentioning the fact that creation of such "generic arrays" is prohibited in Java regardless.

Here is a quote from Angelika Langer's Java Generics FAQ :

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

Example (of a varargs method and its invocation):

public final class Test {  
        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);
                           ^

In this example the first method calls a second method and the second method takes a variable argument list. In order to invoke the varargs method the compiler creates an array and passes it to the method. In this example the array to be created is an array of type T[] , that is, an array whose component type is a type parameter. Creation of such arrays is prohibited in Java and you would receive an error message if you tried to create such an array yourself.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM