多态
概述
我们继续使用继承中的的示例代码
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n4" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + ''' +
", salary=" + salary +
", hireDay=" + hireDay +
'}';
}
}</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n5" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, int year, int month, int day) {
// Manager 类的构造器不能访问 Employee 类的私有域,所以必须利用 Employee 类的构造器对这部分私有域进行初始化, 且使用 super 调用构造器的语句必须是子类构造器的第一条语句。
super(name, salary, year, month, day);
this.bonus = 0;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
}</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n7" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">public class ManagerTest {
public static void main(String[] args) {
Manager boss = new Manager("Jack", 8000, 2020, 4, 12);
boss.setBonus(2000);
Employee employee1 = boss;
Employee employee2 = new Employee("Alice", 2000, 2020, 4, 2);
System.out.println(employee1.getSalary());
System.out.println(employee2.getSalary());
}
}</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n8" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">// output
// 10000.0
// 2000.0</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n13" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">Employee e;
e = new Employee();
e = new Manager();</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n16" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">Employee e = new Employee();
Manager m = e; // error</pre>
<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="java" cid="n33" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; background-position: inherit inherit; background-repeat: inherit inherit;">// method table
Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary() -> Employee.raiseSalary()
Manager:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary() -> Employee.raiseSalary()
setBonus() -> Manager.setBonus()</pre>
Reference
方法签名:方法名和参数列表,(f(int) 和f(String) :方法签名不同,但方法名相同)如果在子类中定义了和超类中签名相同的方法,此时会出现方法重写,但是在方法重写时一定要保证返回类型的兼容性,即允许子类将重写方法的返回类型定义为原返回类型的子类型。
方法重写时子类方法不能低于超类方法的可见性。
注意:
编译器获取方法签名
getSalary()
由于该方法不是 private, static, final 或构造器方法,因此会采用动态绑定,虚拟机在运行时提取方法表
虚拟机搜索 定义了 getSalary 签名的类,此时虚拟机已经知道调用哪个方法。
虚拟机调用方法。
了解规则之后,我们可以看一下测试代码中 employee1.getSalary()
的调用过程:
编译器获取所有可能被调用的候选方法:编译器查看对象的声明类型(C)和方法名(f),此过程编译器会一一列举所有 C 类中名为 f 的方法和超类中访问属性为
public
且名为 f 的方法。编译器获取需要调用的方法名字和参数类型:编译器会查看调用方法时提供的参数类型, 如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法,这个过程被称为重载解析(overloading resolution)。由于允许类型转换(int 可以转换为 double, Manager 可以转换为 Employee等),此过程会很复杂,如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后存在多个方法与之匹配,就会报告一个错误。
静态绑定(static bingding):如果方法为 private, static, final修饰的方法,或者是构造器方法,那么编译器可以准确地知道应该调用哪个方法。
-
动态绑定(dynamic binding):不是静态绑定的方法都将采取动态绑定,调用的方法依赖与隐式参数的实际类型,在运行时实现动态绑定,此时虚拟久会调用与 x 所引用对象的实际类型最合适的那个类的方法:即假设 x 的实际类型为 D, D 为 C 的子类,且在 D 类中存在方法签名相同的方法,此时就会调用 D 中的方法,否则在 D 的超类中寻找该方法。
- 由于每次调用方法都有进行搜索,时间开销很大,因此,虚拟机预先为每个类常见了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法,这样,在真正调用方法的时候,虚拟机仅查找这个表就行了。
我们假设现在调用 x.f(args)
隐式参数 x
声明为类 C
的一个对象,下面是调用过程的详细描述:
方法调用的过程
因为不是每个雇员(Employee)都是经理(Manager),不是 is-a 的关系,在代码层面也很好理解,如果这样赋值,很容易造成 m 调用 Manager 专属的方法引起错误。
但是需要主要注意不能将超类的引用复制给子类变量,如
在 Java 程序设计语言中,对象变量是多态的,一个 Employee 变量既可以引用一个 Employee 对象,也可以引用一个 Employee 类的任何一个子类的对象。
有一个用来判断是否应该设计为继承关系的简单规则,这就是 ”is-a“ 规则,它表明子类的每个对象也是超类的对象,例如上面的例子:每个 管理者(Manager)是雇员(Employee)。is-a 规则的另一种表述法是置换法则,它表明程序中出现超类对象的任何地方都可以用子类对象置换,如。
像上述情形,一个对象变量可以只是多种实际类型的现象称为多态(polymorphism),在运行时能够 自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
在上述测试代码中,我们可以看到声明为 Employee 的变量既可以引用 Employee 类型的对象,也可以引用 Manager 类型的对象,当引用 Employee 对象时调用其 getSalary方法, 当引用 Manager 对象时调用 Manager 对象的方法。
测试代码: