为什么Java允许子类重新定义方法的返回类型?

为什么Java允许子类重新定义方法的返回类型?,java,Java,这是有效的Java: public interface Bar { Number getFoo(); } public class MyBar implements Bar { @Override public Integer getFoo() { return Integer.MAX_VALUE; } } 我知道为什么它是有效的Java,因为Integer是数字的一个子类。但是我想知道为什么语言允许子类重新定义返回类型有什么好的理由吗?在某些

这是有效的Java:

public interface Bar {
    Number getFoo();
}

public class MyBar implements Bar {
    @Override
    public Integer getFoo() {
        return Integer.MAX_VALUE;
    }
}
我知道为什么它是有效的Java,因为Integer是数字的一个子类。但是我想知道为什么语言允许子类重新定义返回类型有什么好的理由吗?在某些地方这是一件有用的事情吗?最佳实践不是规定这应该是:

public class MyBar implements Bar {
    @Override
    public Number getFoo() {
        return Integer.MAX_VALUE;
    }
}

由于
Integer
Number
的子类型,因此它可以用作返回类型中的替换项

我相信这就是所谓的协变回报类型,参见

您可能希望在子类希望比父类更具体的情况下使用它,并添加限制,例如,对于可能子类型它的类

因此,除此之外,以下是可能的

public static class MyFoo extends MyBar {

    @Override
    public Integer getFoo() {
        return super.getFoo();
    }

}
但不是

public static class MyFoo extends MyBar {

    @Override
    public Number getFoo() { // compile error: The return type is incompatible with MyBar.getFoo()
        return super.getFoo();
    }

}

这是合法的,因为Java基于方法的签名进行调度。签名包括方法的名称和参数类型,但不包括其返回类型。因此,尽管具有不同的返回类型,但子类中被重写的方法与签名匹配,并且是原始方法的合法替代。

如果重写方法返回原始方法返回类型的更特定子类型,为什么不声明它呢?继续您自己的示例,以下内容将是有效的:

MyBar mbar = new MyBar();
Integer i = mbar.getFoo();
也就是说,我知道对于MyBar,返回类型将是Integer。这可能有用。如果返回类型被声明为Number,则需要强制转换。

这称为协变返回类型

不过,这背后的原因是,如果要引用更具体的类型,该怎么办。假设您使用的是
MyBar
实例,而不是
Bar
界面。然后你就可以做了

Integer i = new MyBar().getFoo();
相对于

Integer i = (Integer)(new myBar().getFoo());
这就是我们所知道的,这绝对是一个特性。事实上,它是最近才在Java1.5中引入的

其思想是,子类可以覆盖具有更窄(协变)返回类型的方法。这是完全安全的,因为新的返回类型可以与原始返回类型完全相同的方式使用


当您决定直接使用子类时,它变得非常有用。例如,当您有一个声明类型为
MyBar

的实例时,一个肯定有用的示例是。如果你有这样的东西:

class Foo {
  Foo setParam(String s) { ...; return this; }
}
如果你有一个
扩展类,如果你不能做到这一点,就会导致非常尴尬的时刻:

class Bar {
  Bar setParam(String s) { return (Bar)super.setParam( s ); }
}
是的,有一个演员在那里,但它允许的是无缝链接:

Foo foo = new Foo().setParam("x").setParam("y");
Bar bar = new Bar().setParam("x").setParam("y");
与之相反:

Bar bar = (Bar)((Bar)new Bar().setParam("x")).setParam( "y" );
但实际上这只是引入泛型的副作用。如果考虑具有类型参数的集合:

ArrayList<String> list = new ArrayList<String>();

一旦你能做到这一点,你就必须允许子类也这样做,否则扩展参数化类型将成为一场噩梦。

如今,泛型将类似于:

public interface NewBar<T extends Number> {
  T getFoo();
}

public class MyNewBar implements NewBar<Integer> {
  @Override
  public Integer getFoo() {
    return Integer.MAX_VALUE;
  }

}
公共接口NewBar{
T getFoo();
}
公共类MyNewBar实现了NewBar{
@凌驾
公共整数getFoo(){
返回Integer.MAX_值;
}
}

所有这些都会更清楚。

也许您有一个用例,需要整数getFoo()中的信息,而这个数getFoo()没有这些信息。假设你有一个计算你的酒吧的应用程序:

public class BarCounter {
    public static void main(String[] args) {
        Bar bar = new MyBar();
        System.out.println("Bar #" + bar.getBarCounter());
        bar = new MyOtherBar();
        System.out.println("Bar #" + bar.getBarCounter());
    }
}
对于BarCounter,重要的是getBarCounter返回一个它可以打印的数字。因此,条形图实现了条形图接口,该接口仅表示:

public interface Bar {
    public Number getBarCounter();
}
特别是,MyBar是您的第一个酒吧:

public class MyBar implements Bar {
    public Integer getBarCounter() {
        return Integer.valueOf(1);
    }
}
你的另一家酒吧知道:

public class MyOtherBar extends MyBar {
    public Integer getBarCounter() {
        return Integer.valueOf(super.getBarCounter() + 1);
    }
}
如果MyBar.getBarCounter()返回一个数字,则不会使用NoSuchMethodError进行编译

该示例是人为设计的,但一般原则是:当您扩展实现通用接口(Bar)的类(MyBar)时,您的扩展类(MyOtherBar)应该可以访问其父类(MyBar)拥有的所有信息,在本例中,getBarCounter是一个整数


在现实生活中,当您有一个使用泛型列表或集合的类时,有时会出现这种情况,然后发现您必须扩展这个类,因为在这种情况下,您需要一个ArrayList或HashSet。然后,您的扩展类(及其所有子类)应该知道他们正在处理的特定子类。

另请参见。OP的问题是,这是否有用,而不是它为什么有用。@jazzbassrob谢谢,添加了详细信息。如果您使用的是已知子类型,并且只想处理特定类型,那么它很有用,不必从超级类型强制转换,因为它永远不会是其他类型。但最佳实践不是规定您应该根据接口而不是实现进行编码吗?@DaveJohnston还有其他考虑因素。如果该方法是内部使用的呢?类型
MyBar
不需要针对接口编程,它是实现。@DaveJohnston调用者应该针对接口编程。因此,在任何使用MyBar的类中,它都应该是Bar=new MyBar();Number num=bar.getFoo()。允许实现类了解其内部结构
public class MyOtherBar extends MyBar {
    public Integer getBarCounter() {
        return Integer.valueOf(super.getBarCounter() + 1);
    }
}