具有泛型子类化的 Java 自动返回类型协方差

我有两个看起来像这样的接口:

interface Parent<T extends Number> {
    T foo();
}

interface Child<T extends Integer> extends Parent<T> {
}

如果我有一个原始对象,则调用默认为返回 a,因为没有类型参数。Parentfoo()Number

Parent parent = getRawParent();
Number result = parent.foo(); // the compiler knows this returns a Number

这是有道理的。

如果我有一个原始对象,我希望调用将按相同的逻辑返回 a。但是,编译器声称它返回一个 .Childfoo()IntegerNumber

Child child = getRawChild();
Integer result = child.foo(); // compiler error; foo() returns a Number, not an Integer

我可以覆盖 in 来解决此问题,如下所示:Parent.foo()Child

interface Child<T extends Integer> extends Parent<T> {
    @Override
    T foo(); // compiler would now default to returning an Integer
}

为什么会发生这种情况?有没有办法让默认返回 不覆盖 ?Child.foo()IntegerParent.foo()

编辑:假装不是最终的。我只是选择并作为例子,但显然它们不是最好的选择。:SIntegerNumberInteger


答案 1
  1. 这是基于@AdamGent的想法。
  2. 不幸的是,我对JLS的熟悉程度不足以从规范中证明以下内容。

Imagine是在不同的编译单元中定义的 - 在单独的文件中。public interface Parent<T extends Number>Parent.java

然后,在编译 和 时,编译器会将方法视为 。证明:ChildmainfooNumber foo()

import java.lang.reflect.Method;
interface Parent<T extends Number> {
    T foo();
}

interface Child<R extends Integer> extends Parent<R> {
}

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(Child.class.getMethod("foo").getReturnType());
    }
}

指纹:

class java.lang.Number

此输出是合理的,因为java会进行类型擦除,并且无法保留在结果文件中,因为方法仅在 中定义。若要更改子编译器中的结果类型,需要在字节码中插入一个存根方法。这是因为编译后仍然没有关于泛型类型的信息。T extends.classfoo()ParentInteger foo()Child.class

现在,如果您将孩子修改为:

interface Child<R extends Integer> extends Parent<R> {
    @Override R foo();
}

例如,将 own 添加到编译器中将在文件中创建自己的方法副本,其中包含不同但仍然兼容的原型。现在输出为:foo()ChildChild.classInteger foo()

class java.lang.Integer

这当然令人困惑,因为人们会期望“词法可见性”而不是“字节码可见性”。

另一种方法是编译器在两种情况下以不同的方式编译它:接口在同一“词法范围”中,当编译器只能看到字节码时,编译器可以看到源代码和不同编译单元中的接口。我不认为这是一个好的选择。


答案 2

它们并不完全相同。想象一下,接口是这样定义的:T

interface Parent<T1 extends Number> {
    T1 foo();
}

interface Child<T2 extends Integer> extends Parent<T2> {
}

接口扩展了接口,因此我们可以将形式类型参数 T1“替换为”实际“类型参数,我们可以说是:ChildParent"T2 extends Integer"

interface Parent<<T2 extends Integer> extends Number>

这只允许,因为 Integer 是 Number 的子类型。因此,接口中的 签名(在接口中扩展后)简化为:foo()ParentChild

interface Parent<T2 extends Number> {
    T2 foo();
}

换句话说,签名不会更改。接口中声明的方法继续作为原始类型返回。foo()ParentNumber


推荐