为什么在这种情况下擦除通用类型会阻止覆盖?

Why erasure of generic types prevents overriding in this case?

以下代码无法在 OpenjDK 11 上编译。 在我看来,B 中的 test1 应该覆盖 A 中的 test1,因为:

  1. 方法同名。
  2. 这些方法具有相同的参数列表。
  3. 这些方法具有相同的可见性。
  4. 这些方法实际上不会抛出可能不兼容的已检查异常。
  5. 它们的 return 类型是协变的。

我拿了一个反编译器,分别反编译了每个class。编译后的代码实际上如我所料地工作。它将 U extends Number 替换为 Number,T extends Integer 替换为 Integer 等等。但是,当我尝试将两个 class 一起编译时,我在第二个 class 上遇到错误,表明第二个 class 中的测试不会覆盖第一个中的方法。

我在这里遗漏了或大或小的东西。它可能与 5 有关。也许类型不是协变的。你能帮帮我吗?

class A {
    <U extends Number, T extends Number> U test(T test) {
        System.out.println("In A.test(T test)");
        return null;
    }
    //Decompiler shows that the above method erases to 
    // Number test(Number test)
    // Just like the method below.
        
    Number test2(Number test) {
        return null;
    }
}

class B extends A {
    //Unsuccessful override. Compiler error.
    @Override  
    <U extends Integer, T extends Number> U test(T test) {
        System.out.println("In B.test(T tesT)");
        return null;
    }
    //Decompiler shows that the above method erases to 
    //Integer test(Number test)
    //Just like the method bellow.
    
    
    //Successful override
    @Override
    Integer test2(Number test) {
        return null;
    }
}

您的 test 方法产生错误的原因是它们具有完全不相关的不同签名。注意方法的signature由方法名、参数列表、和类型参数

组成

引自 Java 语言规范:

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 type parameters of M, the same formal parameter types.

至关重要的是,您的两个 test 方法没有 same type parameters,因为 A.test 中的 U 具有不同的 bound 来自 UB.test.

Two methods or constructors M and N have the same type parameters if both of the following are true:

  • M and N have same number of type parameters (possibly zero).

  • Where A1, ..., An are the type parameters of M and B1, ..., Bn are the type parameters of N, let θ=[B1:=A1, ..., Bn:=An]. Then, for all i (1 ≤ i ≤ n), the bound of Ai is the same type as θ applied to the bound of Bi.

想想如果 B.test 实际上覆盖了 A.test 会发生什么。您可以将类型传递给超出其范围的类型参数 U

A a = new B();
// This will call B.test, and U is Double, T is Integer
// but U should extends Integer!
Double x = a.test((Integer)0); 

有关详细信息,请在 JLS 中 here are the precise rules for when overriding happens. Note that criteria #4 and #5 on your list are not actually considered. They are just additional requirements that make your code not compile if you break them. One method is still "defined" to override another even if you break those requirements. They are listed here

鉴于我发现相关的 JLS 规则(在上面的 Sweeper 的回答中引用)非常混乱,提及由 Enthuware (https://enthuware.com/) 的伟人制定的规则很有用。 他们没有提到可见性和例外规则,但他们详细介绍了泛型。他们似乎解释了我在上面评论中提到的两个案例。我在这里再说一遍案例:

class A {
    public <T,U> U test(T test) {
        return null;
    }
}

class B extends A {
    //Successful override even though type parameters do not match
    //This method even has none.
    @Override
    public Number test(Object test) {
        return null;
    }
}

class C {
    public Object test(Object test) {
        return null;
    }
}

class D extends C {
    //Same situation as A and B, but with places being exchanged.
    //Now the generic method overrides non-generic. 
    //And we have error. 
    @Override
    public <T,U> U test(T test) {
        return null;
    }
}

现在是 Enthuware 制定的步骤:

检查有效覆盖的步骤:

首先,检查方法签名(即方法名和参数列表)。如果subclass中方法的签名与superclass中方法的签名匹配,那么它可能是一个有效的覆盖,否则它只是一个重载方法。 请注意,签名不包括参数名称和参数的泛型类型规范。

注意:我认为他们的意思是我们用它的绑定替换类型参数,然后比较两种方法。

其次,如果是潜在重写,检查参数的泛型类型说明。 如果覆盖方法没有对参数类型使用泛型类型规范,那么它是有效的。反过来是无效的,即允许覆盖方法擦除泛型类型规范,但如果被覆盖的方法没有它,则不允许添加泛型类型规范。如果两个方法都具有泛型类型规范,则规范必须完全匹配。 例如,如果重写方法具有 Set,则重写方法可以使用 Set 或 Set。但是如果覆盖方法有 Set,那么覆盖方法也必须有 Set 才能有效覆盖。

Third,如果是潜在的覆盖,检查return类型。 Java 允许“协变”returns,这意味着覆盖方法的 return 类型必须相同或者是覆盖方法中提到的 return 类型的子类型方法。检查没有通用类型规范的两个 return 类型。如果覆盖方法的 return 类型相对于覆盖方法的 return 类型是协变的(例如,ArrayList 与 List 是协变的),则执行相同的检查,包括通用类型规范(对于例如,ArrayList 与 List 是协变的)。不要被代码中的 搞糊涂了。相同的覆盖规则仍然适用。 中的 T 称为“类型”参数。它用作调用方法时实际使用的任何类型的占位符。例如,如果您使用 List 调用方法 List transform(List list),T 将被类型化为 String。因此,它将 return List。如果在另一个地方,您使用 Integer 调用相同的方法,T 将被键入为 Integer,因此,该调用的方法的 return 类型将为 List