为什么这个涉及通用接口方法的程序编译?

时间:2014-12-30 19:28:54

标签: java generics interface classcastexception

以下代码在运行时抛出ClassCastException,并且行public String foo() { return "bar"; }生成警告“找到'java.lang.String',需要'T'”。我理解ClassCastException(接口方法调用T等于Integerfoo返回String)并且我理解警告(它正在尝试关于这个问题的警告我们)。但我不明白的是程序编译的原因。为什么返回String的方法允许覆盖返回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();
    }
}

4 个答案:

答案 0 :(得分:4)

当天真地声明<T> T foo();时,编译器将尝试从将被分配的变量中推断出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"
        }
    }
}

编译时的结果不能是Object。如果它是对象,那么你将不得不添加一个手动类型转换,但事实并非如此。

简而言之,声明这样的方法是没用的,只会给程序员带来混乱和混乱。

此方法声明在以下某种情况下变得有用:

  • 在接口/类的顶级声明通用<T>时:

    interface MyInterface<T> {
        T foo();
    }
    
    class MyClass implements MyInterface<String> {
        @Override
        //can only return String here. Compiler can check this
        public String foo() { return "bar"; }
    }
    
  • 当传递Class<T>作为参数时,它使编译器能够在不满足此条件时推断结果类型并引发正确的编译器错误:

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

答案 1 :(得分:2)

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

答案 2 :(得分:2)

这是一个非常深刻的问题。 Java语言规范writes

在类mC中声明或继承的实例方法C,覆盖C在类mA中声明的另一个方法A,iff all the all以下是真实的:

  • mC的签名是mA签名的子签名(第8.4.2节)。
  • ...

and

  

两个方法或构造函数MN具有相同的签名,如果它们具有相同的名称,相同的类型参数(如果有的话)(第8.4.4节),并且在适应之后N的形式参数类型M的类型参数,相同的形式参数类型。

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

  

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

     
      
  • m2m1
  • 具有相同的签名   
  • m1的签名与m2签名的删除(§4.6)相同。
  •   

该规范解释了第二个条件的必要性如下:

  

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

实际上,在我们的情况下满足第二个条件,因为MyClass.foo具有签名foo(),这也是MyInterface.foo签名的擦除。

这留下了不同回报类型的问题。规范writes

  

如果返回类型为d1的方法声明R1覆盖或隐藏另一个返回类型为d2的方法R2的声明,则d1必须为d2的return-type-substitutable(第8.4.5节),或发生编译时错误。

and

  

返回类型为R1的方法声明d1是另一个方法d2的return-type-substitutable,返回类型为R2 iff以下任何一个为真:

     
      
  • ...

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

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

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

    •   
    • d1与d2(§8.4.2)的签名不同,R1 = | R2 |。

    •   
  •   

在我们的例子中,R1 = String,R2 = T.因此,第一个条件为false,因为String不是T的子类型。但是,String可以通过未经检查的转换转换为T,使第二个条件成立。

规范解释了对第二和第三个条件的需求如下:

  

定义中允许未经检查的转换,尽管不健全,作为允许从非泛型代码顺利迁移到通用代码的特殊容差。如果使用未经检查的转换来确定R1是R2的返回类型可替换,那么R1必然不是R2的子类型,并且覆盖规则(§8.4.8.3,§9.4.1)将需要编译时未经检查的警告。

也就是说,编译器接受了您的代码,因为您无意中使用了两个编译器功能,以便通过允许逐步生成现有代码来简化向泛型的转换。这些功能在编译时类型系统中打开了一个循环漏洞,这可能会导致堆污染和奇怪的ClassCastExceptions行甚至可能在源代码中没有强制转换。为了提醒您这种危险,编译器需要发出未经检查的警告。因此,这些功能仅应用于其预期目的(与非通用遗留代码的兼容性),否则应避免使用。

答案 3 :(得分:0)

由于类型擦除,T部分只是字节代码中的Object。您可以返回更具体的类型,在本例中为String