为什么不可能使用不同的类型参数多次实现通用接口?

Boz*_*zho 30 java generics

我有一个界面:

public static interface Consumer<T> {
    void consume(T t);
}
Run Code Online (Sandbox Code Playgroud)

我希望能够拥有:

public static class Foo implements Consumer<String>, Consumer<Integer> {
    public void consume(String t) {..}
    public void consume(Integer i) {..}
}
Run Code Online (Sandbox Code Playgroud)

它不起作用 - 编译器不允许您两次实现相同的接口.

问题是:为什么?

人们在这里问了类似的问题,但答案总是"类型擦除",即你不能这样做,因为类型在运行时被删除.

它们不是 - 某些类型在运行时保留.它们保留在这种特殊情况下:

public static void main(String[] args) throws Exception {
    ParameterizedType type = (ParameterizedType) Foo.class.getGenericInterfaces()[0];
    System.out.println(type.getActualTypeArguments()[0]);
}
Run Code Online (Sandbox Code Playgroud)

这打印class java.lang.String(如果我只保留Consumer<String>以便编译)

因此,擦除,在其最简单的解释中,不是原因,或者至少它需要详细说明 - 类型存在,而且,您不关心类型解析,因为您已经有两个具有不同签名的方法.或者至少看起来如此.

Boz*_*zho 27

答案仍然是"类型擦除",但并不是那么简单.关键字是:原始类型

想象一下:

Consumer c = new Foo();
c.consume(1);
Run Code Online (Sandbox Code Playgroud)

那会怎么样?它似乎consume(String s)实际上并不是consume(String s)- 它仍然是consume(Object o),即使它被定义为采取String.

因此,上面的代码含糊不清 - 运行时无法知道consume(..)要调用的两个方法中的哪一个.

一个有趣的后续示例是删除Consumer<Integer>,但保留consume(Integer i)方法.然后调用c.consume(1)原始的Consumer.throws ClassCastException- 无法从Integer转换为String.关于这个例外的奇怪之处在于它发生在第1行.

原因是使用桥接方法.编译器生成桥接方法:

public void consume(Object o) {
    consume((String) o);
}
Run Code Online (Sandbox Code Playgroud)

生成的字节码是:

public void consume(java.lang.String);

public void consume(java.lang.Integer);

public void consume(java.lang.Object);
  Code:
     0: aload_0
     1: aload_1
     2: checkcast     #39                 // class java/lang/String
     5: invokevirtual #41                 // Method consume:(Ljava/lang/String;)V
Run Code Online (Sandbox Code Playgroud)

因此,即使您定义的方法保留了它们的签名,每个方法都有一个相应的桥接方法,在使用该类时实际调用它(无论它是原始的还是参数化的).

  • 另一个答案是,即使原始类型被排除,支持这将完全打破现有的方法调用机制.`Consumer`接口有`consume(Object)`,实现类必须具有相同的功能.因此,保持向后兼容性的主要目标将被打败. (5认同)