有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

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

下面的代码在运行时抛出一个ClassCastException,行public String foo() { return "bar"; }生成一个警告“find'java.lang.String',required'T”。我理解ClassCastException(调用接口方法时T等于Integer,但foo返回一个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) 个答案

  1. # 1 楼答案

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

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

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

    and

    Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the the type parameters of M, the same formal parameter types.

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

    The signature of a method m1 is a subsignature of the signature of a method m2 if either:

    • m2 has the same signature as m1, or
    • the signature of m1 is the same as the erasure (§4.6) of the signature of m2.

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

    The notion of subsignature is designed to express a relationship between two methods whose signatures are not identical, but in which one may override the other. Specifically, it allows a method whose signature does not use generic types to override any generified version of that method. This is important so that library designers may freely generify methods independently of clients that define subclasses or subinterfaces of the library.

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

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

    If a method declaration d1 with return type R1 overrides or hides the declaration of another method d2 with return type R2, then d1 must be return-type-substitutable (§8.4.5) for d2, or a compile-time error occurs.

    and

    A method declaration d1 with return type R1 is return-type-substitutable for another method d2 with return type R2 iff any of the following is true:

    • ...

    • If R1 is a reference type then one of the following is true:

      • R1, adapted to the type parameters of d2 (§8.4.4), is a subtype of R2.

      • R1 can be converted to a subtype of R2 by unchecked conversion (§5.1.9).

      • d1 does not have the same signature as d2 (§8.4.2), and R1 = |R2|.

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

    规范解释了第二和第三个条件的必要性,如下所示:

    An unchecked conversion is allowed in the definition, despite being unsound, as a special allowance to allow smooth migration from non-generic to generic code. If an unchecked conversion is used to determine that R1 is return-type-substitutable for R2, then R1 is necessarily not a subtype of R2 and the rules for overriding (§8.4.8.3, §9.4.1) will require a compile-time unchecked warning.

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

  2. # 2 楼答案

    当幼稚地声明<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);
          }
      }
      
  3. # 3 楼答案

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

  4. # 4 楼答案

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