Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 542
user949300 Avatar asked Dec 05 '13 01:12

user949300


People also ask

Can we use Varargs in abstract method?

A method with a varargs annotation produces a forwarder method with the same signature (args: Array[String])Unit as an existing method. And of course putting the annotation only on the bar in Baz means we can't use the forwarder from a Bar instance.

Is it good to use varargs in Java?

Varargs are useful for any method that needs to deal with an indeterminate number of objects. One good example is String. format . The format string can accept any number of parameters, so you need a mechanism to pass in any number of objects.

What is the rule for using varargs in Java?

Rules to follow while using varargs in JavaWe can have only one variable argument per method. If you try to use more than one variable arguments a compile time error is generated.

Can we pass parameter in abstract method in Java?

Core Java bootcamp program with Hands on practiceYes, we can define a parameterized constructor in an abstract class.


1 Answers

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> 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));
}
like image 136
Paul Bellora Avatar answered Sep 16 '22 20:09

Paul Bellora