【问题标题】:Java automatic return type covariance with generic subclassingJava 自动返回类型协方差与泛型子类化
【发布时间】:2016-07-07 16:53:29
【问题描述】:

我有两个类似这样的界面:

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

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

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

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

这是有道理的。

如果我有一个原始的Child 对象,我希望调用foo() 会以相同的逻辑返回一个Integer。但是,编译器声称它返回一个Number

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

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

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

为什么会这样?有没有办法让Child.foo() 默认返回Integer 而不会覆盖Parent.foo()

编辑:假装Integer 不是最终的。我只是选择了NumberInteger 作为示例,但显然它们不是最佳选择。 :S

【问题讨论】:

  • 我认为这是因为没有孩子的覆盖,孩子继承了父母的foo方法,并且根据父母的说法,TNumber。要让父级中的foo 返回Integer,它需要知道有关其子类的信息,这会破坏层次结构...
  • 试试interface Child&lt;T extends Integer&gt; extends Parent&lt;T extends Integer&gt;
  • @fukanchik 这甚至不是有效的 Java。
  • @AdamGent 谢谢你是对的,它无法编译。我查看了 JLS Chapter 4. Types, Values, and Variables 并找不到它说这里不允许上限的确切点。
  • @fukanchik 这是type argument

标签: java generics polymorphism subclass covariance


【解决方案1】:
  1. 这是基于@AdamGent 的想法。
  2. 很遗憾,我对 JLS 不够熟练,无法从规范中证明以下内容。

想象public interface Parent&lt;T extends Number&gt; 是在一个不同的编译单元 中定义的——在一个单独的文件Parent.java 中。

然后,在编译Childmain 时,编译器会将方法foo 视为Number 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 确实类型擦除并且无法在结果 .class 文件中保留 T extends,因为方法 foo() 仅在 Parent 中定义。要在子编译器中更改结果类型,需要将 stub Integer foo() 方法插入到 Child.class 字节码中。这是因为编译后没有关于泛型类型的信息。

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

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

例如将自己的foo() 添加到Child 中,编译器将在.class 文件中创建Child 自己的方法副本,其具有不同但仍兼容的原型Integer foo()。现在输出是:

class java.lang.Integer

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

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

【讨论】:

  • 我删除了我的答案以支持你的答案。我不确定为什么我的投票被否决(可能是因为我很困惑地认为 OP 想知道为什么 Java 选择以它的方式实现泛型)。如果反对者刚刚添加了评论,那将会很有帮助。但你的答案和@Scottb 更好。
【解决方案2】:

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

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

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

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

interface Parent<<T2 extends Integer> extends Number>

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

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

换句话说,签名没有改变。在Parent 接口中声明的方法foo() 继续返回Number 作为原始类型。

【讨论】:

  • 签名没有改变,但有些语言会进行通用扩展(即它将复制/创建子类中的方法)。它必须执行 Java 如何实现泛型。
  • 在 Java 语言中实现泛型类型的方式肯定有一些特殊性(Java 在这方面并不是唯一的),但通常可以预见并使用这些。
  • 我同意,但我可以理解 OP 的困惑。
猜你喜欢
  • 2012-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-02
  • 2011-02-09
  • 1970-01-01
  • 2013-08-04
相关资源
最近更新 更多