抽象类中的Java varargs和泛型问题

时间:2013-12-05 01:48:24

标签: java generics variadic-functions

我正在玩一些像编程一样的功能。和一些非常深层嵌套的泛型有问题。这是我的SCCE失败,涉及一个抽象类:

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));           
   }
}

这死了

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

但是,对于非抽象 FooGen,它可以正常工作:

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)); 
   }
}

这打印1.2。想法?似乎某些地方Java已经失去了对泛型的追踪。这推动了我的泛型知识的极限。 : - )

(在回答时添加)

首先,感谢你的赞成,以及Paul和Daemon的回答。

仍然想知道为什么它在第二版中作为Numbers工作,我有一个洞察力。作为思想实验,让我们在某处添加.doubleValue()你不能。在代码本身中,变量是IN,而不是Numbers。在main()中,它只是声明了类型FooGen<Number,Number>,但没有地方可以添加代码。

在版本#2中,它确实不像 Numbers 那样“正常工作”。在内部,通过擦除,一切都是对象,正如Paul和Daemon所解释的那样,并且回想起来,我自己很好理解。基本上,在这个复杂的例子中,我被<Number>声明过度兴奋和误导。

不要以为我会打扰一个变通方法。整个想法是懒惰的。 :-)为了提高效率,我创建了并行接口和采用原始双精度(和整数)的代码,这个技巧就可以了。

2 个答案:

答案 0 :(得分:5)

Varargs参数是第一个也是最重要的数组。因此,如果没有语法糖,您的代码将如下所示:

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

abstract OUT fnN(IN[] ins);

由于arrays of type parameters cannot be instantiatednew IN[]不合法,因为type erasure。数组需要知道其组件类型,但IN已在运行时擦除到其上限Object

不幸的是,varargs调用隐藏了这个问题,并且在运行时,您具有等效的fnN(new Object[] {in1, in2}),而fnN已被覆盖以获取Number[]

  

但是,对于非抽象 FooGen,它可以正常工作

这是因为直接实例化FooGen,您没有覆盖fnN。因此,它在运行时接受Object[],并且不会发生ClassCastException

例如,即使FooGen不是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));

所以你可以看到它与FooGen的抽象性无关,而是与fnN是否被缩小的参数类型重写。

<强>解

没有简单的解决方法。一个想法是让fnN改为List<? extends IN>

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

如果您想保持varargs支持,可以将此方法视为实现细节并委托给它:

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

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

答案 1 :(得分:2)

由于称为“类型擦除”的Java的特性而发生此ClassCastException。编译泛型时会发生类型擦除。由于Java编译器在运行时无法知道泛型类的类型,因此它会将通用对象编译为Object的实例。

在您的代码中,编译FooGen时,fnN(IN... ins)会收到Object[]类型的参数。当您尝试将其中一个对象向下转换为通用类型ClassCastException时,会出现OUT

这甚至没有提到在Java中禁止创建这样的“通用数组”的事实。

以下是Angelika Langer's Java Generics FAQ的引用:

  

这是另一个说明潜在危险的例子   忽略关于阵列构造的警告   与变量参数列表结合。

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);
                           ^
  

在此示例中,第一个方法调用第二个方法,第二个方法调用第二个方法   method采用变量参数列表。为了调用varargs   编译器创建数组并将其传递给方法的方法。在   这个例子,要创建的数组是T []类型的数组   是一个数组,其组件类型是一个类型参数。的制作   Java中禁止使用此类数组,您将收到错误消息   如果你试图自己创建这样的数组,请留言。