通过传递子类的实例来调用超类的方法,期望子类实例上有超类参数

Adi*_*ari 2 java oop inheritance overriding overloading

谁能解释一下下面代码的输出,以及这里涉及的Java原理是什么?

class Mammal {
    void eat(Mammal m) {
        System.out.println("Mammal eats food");
    }
}

class Cattle extends Mammal{
    void eat(Cattle c){
        System.out.println("Cattle eats hay");
    }
}

class Horse extends Cattle {
    void eat(Horse h) {
        System.out.println("Horse eats hay");
    }
}

public class Test {
    public static void main(String[] args) {
        Mammal h = new Horse();
        Cattle c = new Horse();
        c.eat(h);
    }
}
Run Code Online (Sandbox Code Playgroud)

它产生以下输出:

Mammal eats food
Run Code Online (Sandbox Code Playgroud)

我想知道我们是如何得到上述结果的。

Ale*_*nko 7

重载与覆盖

这不是一个有效的方法重写,因为所有方法签名方法名称+参数)都是不同的:

void eat(Mammal m)
Run Code Online (Sandbox Code Playgroud)
void eat(Cattle c)
Run Code Online (Sandbox Code Playgroud)
void eat(Horse h)
Run Code Online (Sandbox Code Playgroud)

这称为方法重载请参阅),并且类Horse将具有3不同的方法,而不是一个。即它自己的重载版本eat()2继承版本。

编译器会将方法调用映射c.eat(h)到最具体的方法,即eat(Mammal m),因为变量h的类型为Mammal

为了调用带有签名的方法,eat(Horse h)您需要强制转换h为类型Horse。请注意,此类转换将被视为所谓的收缩转换,并且它永远不会自动发生,因为无法保证此类类型转换会成功,因此编译器不会为您执行此操作。

注释掉该方法void eat(Mammal m),您将看到编译错误 - 编译器不执行缩小转换,它只能帮助您进行扩大转换,因为它们保证成功,因此安全。

如果您手动进行类型转换,会发生什么:

强制转换h为类型Horse

c.eat((Horse) h);
Run Code Online (Sandbox Code Playgroud)

输出:

Cattle eats hay   // because `c` is of type `Cattle` method `eat(Cattle c)` gets invoked
Run Code Online (Sandbox Code Playgroud)

因为变量c是类型,Cattle所以它只知道方法eat(Cattle c)而不知道eat(Horse h)。在幕后,编译器会将扩展h为类型Cattle


c和强制转换h为类型Horse

((Horse) c).eat((Horse) h);
Run Code Online (Sandbox Code Playgroud)

输出:

Horse eats hay   // now `eat(Horse h)` is the most specific method
Run Code Online (Sandbox Code Playgroud)

覆盖规则

方法重写的规则符合Liskov替换原则

使用基类指针或引用的函数必须能够在不知情的情况下使用派生类的对象。

子类应该以这样的方式声明它的行为,以便它可以在任何需要其父类的地方使用:

  • 方法签名必须完全匹配。即方法名称和参数类型应该相同。并且参数需要以相同的顺序声明。值得注意的是,如果方法签名不同(例如问题中提供的代码片段,其中一个方法的名称拼写错误),编译器将不知道这些方法是如何连接的。即,它不再被视为覆盖的情况,方法将被视为独特的,并且下面列出的所有其他要求将不适用。这就是为什么强烈建议将@Override注释添加到重写的方法中。有了它,如果您拼写错误的名称或以错误的顺序声明参数,编译器在无法在父类和接口中找到匹配的方法时将给出清晰的反馈。如果您要求 IDE 生成模板(IntelliJ +中的快捷方式) ,您的 IDE 将为您添加此注释CTRLO

  • 重写方法的访问修饰符可以相同或更宽,但不能更严格。protected即父类中的方法可以被重写public或者可以保留protected,我们不能这样做private

  • 重写方法的返回类型应该与原始类型完全相同。但如果父方法声明返回引用类型,则可以返回其子类型。即,如果父级返回Number一个重写的方法,则可以提供Integer作为返回类型。

  • 如果父方法声明抛出任何已检查的异常,则允许重写的方法声明相同的异常或其子类型,或者可以实现为安全的(即根本不抛出异常)。不允许使被重写的方法不如父方法声明的方法安全,即抛出父方法未声明的已检查异常。请注意,对于运行时异常(未检查)没有任何限制,即使父方法未指定,重写的方法也可以自由声明它们。

这将是方法重写的有效示例:

static class Mammal{
    void eat(Mammal m){
        System.out.println("Mammal eats food");
    }
}

public class Cattle extends Mammal{
    @Override
    void eat(Mammal c) {
        System.out.println("Cattle eats hay");
    }
}

public class Horse extends Cattle{
    @Override
    public void eat(Mammal h) throws RuntimeException {
        System.out.println("Horse eats hay");
    }
}
Run Code Online (Sandbox Code Playgroud)

main()

public static void main(String[] args) {
    Mammal h = new Horse();
    Cattle c = new Horse();
    c.eat(h);
}
Run Code Online (Sandbox Code Playgroud)

输出:

Horse eats hay
Run Code Online (Sandbox Code Playgroud)