Java 为什么这个程序需要编译一个通用接口方法?

Java 为什么这个程序需要编译一个通用接口方法?,java,generics,interface,classcastexception,Java,Generics,Interface,Classcastexception,下面的代码在运行时抛出一个ClassCastException,行public String foo(){return“bar”}生成一个警告“find'java.lang.String',required'T'”。我理解ClassCastException(调用接口方法时T等于Integer,但foo返回一个字符串),我理解警告(它正试图警告我们这个问题)。但我不明白的是为什么程序会编译。为什么允许返回字符串的方法重写返回T的方法 public class Main { interf

下面的代码在运行时抛出一个
ClassCastException
,行
public String foo(){return“bar”}
生成一个警告“find'java.lang.String',required'T'”。我理解
ClassCastException
(调用接口方法时
T
等于
Integer
,但
foo
返回一个
字符串
),我理解警告(它正试图警告我们这个问题)。但我不明白的是为什么程序会编译。为什么允许返回
字符串的方法重写返回
T
的方法

public class Main {

    interface MyInterface {

        <T> T foo();
    }

    static class MyClass implements MyInterface {

        @Override
        public String foo() { return "bar"; }
    }

    public static void main(String[] args) {
        int a = ((MyInterface) new MyClass()).<Integer>foo();
    }
}
公共类主{
接口MyInterface{
T foo();
}
静态类MyClass实现MyInterface{
@凌驾
公共字符串foo(){return“bar”;}
}
公共静态void main(字符串[]args){
int a=((MyInterface)new MyClass()).foo();
}
}

因为声明一个方法
T foo()
与声明它
Object foo()
基本相同。如果您在某个地方与type参数有其他连接(可能接口是在
T
上参数化的,而方法只是
T foo()
),那么可能会有某种链接被破坏。然而,在这种情况下,您只是回到了标准规则,即覆盖可以返回超类型返回的任何更具体的子类型。

由于类型擦除,T
部分只是字节码中的一个
对象。您可以返回更具体的类型,在本例中,当天真地声明
tfoo()时,返回
String

编译器将尝试从将被赋值的变量推断
foo
的结果类型。这就是为什么要编译它。它可以很容易地进行测试:

interface MyInterface {
    <T> T foo();
}

class MyClass implements MyInterface {
    @Override
    public String foo() { return "bar"; }
}

public class Main {
    public static void main(String[] args) {
        MyInterface myInterface = new MyClass();
        //the result of foo will be String
        String bar = myInterface.foo();
        System.out.println(bar); //prints "bar"
        try {
            //the result of foo at compile time will be Integer
            Integer fail = myInterface.foo();
            System.out.println(fail); //won't be executed
        } catch (ClassCastException e) {
            //for test purposes only. Exceptions should be managed better
            System.out.println(e.getMessage()); //prints "java.lang.String cannot be cast to java.lang.Integer"
        }
    }
}
  • 作为参数传递时,这使编译器能够推断结果类型,并在不满足此条件时引发适当的编译器错误:

    interface MyInterface {
        <T> T foo(Class<T> clazz);
    }
    
    class MyClass implements MyInterface {
        @Override
        public <T> T foo(Class<T> clazz) {
            try {
                return clazz.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace(System.out);
            } catch (IllegalAccessException e) {
                e.printStackTrace(System.out);
            }
            return null;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyInterface myInterface = new MyClass();
            //uncomment line below to see the compiler error
            //Integer bar = myInterface.foo(String.class);
    
            //line below compiles and runs with no problem
            String bar = myInterface.foo(String.class);
            System.out.println(bar);
        }
    }
    
    接口MyInterface{
    T-foo(课堂);
    }
    类MyClass实现MyInterface{
    @凌驾
    公共T-foo(课堂){
    试一试{
    返回clazz.newInstance();
    }捕获(实例化异常e){
    e、 printStackTrace(系统输出);
    }捕获(非法访问例外e){
    e、 printStackTrace(系统输出);
    }
    返回null;
    }
    }
    公共班机{
    公共静态void main(字符串[]args){
    MyInterface MyInterface=新的MyClass();
    //取消注释下面的行以查看编译器错误
    //整型条=myInterface.foo(String.class);
    //下一行编译并运行没有问题
    stringbar=myInterface.foo(String.class);
    系统输出打印项次(巴);
    }
    }
    

    • 这是一个令人惊讶的深刻问题。Java语言规范:

      在类
      C
      中声明或由类
      C
      继承的一个实例方法
      mC
      ,重写了类
      A
      中声明的另一个方法
      mA
      ,如果以下所有条件均为真:

      • mC
        的签名是
        mA
        签名的子签名(§8.4.2)
      :

      两种方法或构造函数,
      M
      N
      ,如果它们具有相同的名称、相同的类型参数(如有)(§8.4.4),则具有相同的签名,并且在将
      N
      的形式参数类型调整为
      M
      的类型参数后,具有相同的形式参数类型

      在我们的例子中显然不是这样,因为
      MyInterface.foo
      声明了一个类型参数,但
      MyClass.foo
      没有声明

      方法
      m1
      的签名是方法
      m2
      签名的子签名,如果:

      • m2
        具有与
        m1
        相同的签名,或
      • m1
        的签名与
        m2
        签名的擦除(§4.6)相同
      规范解释了第二个条件的必要性,如下所示:

      子签名的概念旨在表示两个方法之间的关系,这两个方法的签名不完全相同,但其中一个方法可以覆盖另一个方法。具体来说,它允许签名不使用泛型类型的方法重写该方法的任何泛型版本。这一点很重要,这样库设计人员就可以独立于定义库的子类或子接口的客户端自由地生成方法

      事实上,第二个条件在我们的例子中得到满足,因为
      MyClass.foo
      具有签名
      foo()
      ,这也是对
      MyInterface.foo
      签名的擦除

      这就剩下了不同返回类型的问题。规范:

      如果返回类型为R1的方法声明
      d1
      覆盖或隐藏另一个返回类型为R2的方法声明
      d2
      ,则
      d1
      必须是返回类型可替换(§8.4.5)的
      d2
      ,否则会发生编译时错误

      :

      如果满足以下任一条件,则返回类型为R1的方法声明d1为返回类型,可替换为另一个返回类型为R2的方法d2:

      • 如果R1是引用类型,则以下情况之一为真:

        • R1适用于d2的类型参数(§8.4.4),是R2的一个子类型

        • R1可以通过未经检查的转换(§5.1.9)转换为R2的子类型

        • d1与d2没有相同的签名(§8.4.2),R1=| R2 |

      在我们的例子中,R1=String和R2=T。因此,第一个条件为false,因为String不是T的子类型。但是,可以通过未选中的转换将String转换为T,从而使第二个条件为true

      规格说明
      interface MyInterface {
          <T> T foo(Class<T> clazz);
      }
      
      class MyClass implements MyInterface {
          @Override
          public <T> T foo(Class<T> clazz) {
              try {
                  return clazz.newInstance();
              } catch (InstantiationException e) {
                  e.printStackTrace(System.out);
              } catch (IllegalAccessException e) {
                  e.printStackTrace(System.out);
              }
              return null;
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              MyInterface myInterface = new MyClass();
              //uncomment line below to see the compiler error
              //Integer bar = myInterface.foo(String.class);
      
              //line below compiles and runs with no problem
              String bar = myInterface.foo(String.class);
              System.out.println(bar);
          }
      }