1 方法重写

在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为重写(覆写)(Override)

例如,在Person类中,我们定义了run()方法:

class Person {
    public void run() {
        System.out.println("一个人在奔跑。");
    }
}

在子类Student中,覆写这个run()方法:

class Student extends Person {
    @Override
    public void run() {
        System.out.println("一个学生在奔跑。");
    }
}

OverrideOverload 不同的是,如果方法签名不同,就是 OverloadOverload 方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override

注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。

class Person {
    public void run() { … }
}

class Student extends Person {
    // 不是Override,因为参数不同:
    public void run(String s) { … }
    // 不是Override,因为返回值不同,且会报错:
    public int run() { … }
}

加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override不是必需的

在上一节中,我们已经知道,引用变量的声明类型可能与其实际类型不符,例如:

Person p = new Student();

现在,我们考虑一种情况,如果子类覆写了父类的方法:

import java.util.Arrays;

public class Test5 {
    public static void main(String[] args) {
        Person person = new Student();
        person.run(); // 这里将会输出什么?
    }
}

class Person {
    public void run() {
        System.out.println("一个人在奔跑!");
    }
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("一个学生在奔跑!");
    }
}

那么,一个实际类型为Student,引用类型为Person的变量,调用其run()方法,调用的是Person还是Studentrun()方法?

运行一下上面的代码就可以知道,实际上调用的方法是Studentrun()方法。因此可得出结论:

Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

这个非常重要的特性在面向对象编程中称之为多态它的英文拼写非常复杂:Polymorphic。

2 多态

多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。例如:

Person p = new Student();
p.run(); 

有童鞋会问,从上面的代码一看就明白,肯定调用的是Studentrun()方法啊。

但是,假设我们编写这样一个方法:

public void runTwice(Person p) {
    p.run();
    p.run();
}

它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类,因此,也无法确定调用的是不是Person类定义的run()方法。

所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。这种不确定性的方法调用,究竟有什么作用?

我们还是来举栗子。

3 体会多态的强大

让我们思考这样一个场景:在一个游戏中,我们操作的玩家走进了敌人的包围圈,四周埋伏着的敌方弓箭手、剑客等角色直接向玩家发动攻击,直接消灭了玩家。

让我们用 java 程序复现这个游戏场景。

public class AGame {
    public static void main(String[] args) {
        Person player = new Player();
        Person[] enemys = {new Swordsman(), new Archer(), new Archer()};
        for (Person enemy:enemys){
            enemy.attack(player);
        }
    }
}

class Person {
    // 生命值
    protected int hp;
    // 攻击力
    protected int atk;

    public void attack(Person target) {
        target.beDamaged(atk);
    }

    public void beDamaged(int damage) {
        hp -= damage;
        if (hp <= 0){
            died();
        }
    }

    public void died() {
        System.out.println("此角色死亡。");
    }
}

class Player extends Person{
    public Player() {
        hp = 100;
        atk = 10;
    }

    @Override
    public void beDamaged(int damage) {
        hp -= damage;
        System.out.println("玩家:好疼!");
        if (hp <= 0){
            died();
        }
    }

    @Override
    public void died() {
        System.out.println("玩家:啊我死了!");
    }
}

class Archer extends Person{
    public Archer() {
        hp = 100;
        atk = 30;
    }

    @Override
    public void attack(Person target) {
        System.out.println("弓箭手:向玩家射出一箭,造成30点伤害");
        target.beDamaged(atk);
    }
}

class Swordsman extends Person{
    public Swordsman() {
        hp = 100;
        atk = 40;
    }

    @Override
    public void attack(Person target) {
        System.out.println("剑客:劈砍玩家,造成40点伤害");
        target.beDamaged(atk);
    }
}

仔细观察 main() 方法,正因有了多态,我们才能够仅仅使用一个 foreach 循环和一行 enemy.attack(player); ,实现敌人所有类型共同的攻击玩家功能。如果我们没有多态,我们就只能写大量 if...else 判断,为每种敌人设计攻击代码了。

可见,多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。

4 覆写Object方法

因为所有的class最终都继承自Object,而Object定义了几个重要的方法:

  • toString():把instance输出为String

  • equals():判断两个instance是否逻辑相等;

  • hashCode():计算一个instance的哈希值。

在必要的情况下,我们可以覆写Object的这几个方法。例如:

class Person {
    ...
    // 显示更有意义的字符串:
    @Override
    public String toString() {
        return "Person:name=" + name;
    }

    // 比较是否相等:
    @Override
    public boolean equals(Object o) {
        // 当且仅当o为Person类型:
        if (o instanceof Person) {
            Person p = (Person) o;
            // 并且name字段相同时,返回true:
            return this.name.equals(p.name);
        }
        return false;
    }

    // 计算hash:
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

5 调用super

在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。例如:

class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    @Override
    public String hello() {
        // 调用父类的hello()方法:
        return super.hello() + "!";
    }
}

6 final

继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override

class Person {
    protected String name;
    public final String hello() {
        return "Hello, " + name;
    }
}

class Student extends Person {
    // compile error: 不允许覆写
    @Override
    public String hello() {
    }
}

如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承:

final class Person {
    protected String name;
}

// compile error: 不允许继承自Person
class Student extends Person {
}

对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改。例如:

class Person {
    public final String name = "Unamed";
}

final字段重新赋值会报错:

Person p = new Person();
p.name = "New Name"; // compile error!

可以在构造方法中初始化final字段:

class Person {
    public final String name;
    public Person(String name) {
        this.name = name;
    }
}

这种方法更为常用,因为可以保证实例一旦创建,其final字段就不可修改。

7 小结

  • 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为;

  • Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;

  • final修饰符有多种作用:

    • final修饰的方法可以阻止被覆写;

    • final修饰的class可以阻止被继承;

    • final修饰的field必须在创建对象时初始化,随后不可修改。