多态
本节定位:多态是面向对象编程的皇冠明珠。本节从"编译看左边、运行看右边"的经典口诀出发,深入 JVM 动态绑定机制(invokevirtual 指令、虚方法表查找),讲解多态在方法参数、返回值、集合中的实际应用,并澄清成员变量与静态方法的特殊绑定规则。
什么是多态
多态(Polymorphism):同一个行为具有多个不同表现形式或形态。在 Java 中,多态指父类引用指向子类对象,调用方法时表现出子类的行为。
多态的三个前提条件:
- 继承:存在父子类关系
- 重写:子类重写了父类的方法
- 父类引用指向子类对象:
Parent p = new Child();
飞翔科技场景:薪资计算的多态应用
架构师白歌对实习生小崔说:"如果每新增一种员工类型,你都要修改薪资计算的主流程代码,那这个系统迟早会变成意大利面条。你应该用多态来解耦。"
重构前:没有多态的代码
// ❌ 反例:每新增员工类型都要修改这段代码
public class PayrollSystem {
public double calculatePay(Object employee) {
if (employee instanceof Manager) {
Manager m = (Manager) employee;
return m.getSalary() * 12 + m.getBonus();
} else if (employee instanceof Developer) {
Developer d = (Developer) employee;
return d.getSalary() * 12;
} else if (employee instanceof Designer) { // 新增类型又要改这里!
Designer ds = (Designer) employee;
// ...
}
return 0;
}
}
重构后:多态驱动的薪资系统
// ✅ 正确:利用多态,新增员工类型无需修改此方法
public class PayrollSystem {
public double calculateAnnualPay(Employee emp) {
return emp.computeAnnualSalary(); // 多态调用!根据实际类型动态绑定
}
public void printPayReport(Employee[] staff) {
for (Employee emp : staff) {
System.out.println(emp.getName() + " 年薪: " + calculateAnnualPay(emp));
}
}
}
// 完整测试
public class PolymorphismTest {
public static void main(String[] args) {
Employee[] staff = {
new Manager("大翔", "E001", 50000, 200000),
new Developer("小崔", "E002", 25000, "Java"),
new Developer("白歌", "E003", 35000, "Python")
};
PayrollSystem ps = new PayrollSystem();
ps.printPayReport(staff);
}
}
运行输出:
大翔 年薪: 800000.0
小崔 年薪: 300000.0
白歌 年薪: 420000.0
"编译看左边,运行看右边"深度解析
这是 Java 多态最核心的口诀。
Employee emp = new Manager("大翔", "E001", 50000, 200000);
// ↑左边(编译时类型) ↑右边(运行时类型)
实操验证
public class CompileLeftRunRightTest {
public static void main(String[] args) {
Employee emp = new Manager("大翔", "E001", 50000, 200000);
// --- 编译阶段(看左边 Employee) ---
System.out.println(emp.getName()); // ✅ Employee 有 getName(),编译通过
System.out.println(emp.work()); // ✅ Employee 有 work(),编译通过
// emp.getBonus(); // ❌ Employee 没有 getBonus(),编译直接报错
// --- 运行阶段(看右边 Manager) ---
System.out.println(emp.work());
// 输出:经理 大翔 正在制定项目计划...
// 实际执行的是 Manager.work(),不是 Employee.work()
}
}
JVM 底层:invokevirtual 指令与动态绑定
字节码视角
当 JVM 执行 emp.work() 时,生成的字节码指令是 invokevirtual:
字节码:
aload_1 // 将局部变量 emp 压入操作数栈
invokevirtual #5 // 调用 Employee.work() —— 常量池中符号引用是 Employee.work
// 但 JVM 在运行时会解析到实际类型(Manager)的方法
invokevirtual 的执行流程
核心原理:
invokevirtual指令在解析方法时,不是根据引用变量的声明类型,而是根据操作数栈顶对象的实际类型去查找方法。这就是"动态绑定"的本质。
instanceof 类型判断
instanceof 用于在运行时检查对象的实际类型,是向下转型前的安全守卫。
语法与使用
对象引用 instanceof 类型 // 返回 boolean
public class InstanceofTest {
public static void main(String[] args) {
Employee[] staff = {
new Manager("大翔", "E001", 50000, 200000),
new Developer("小崔", "E002", 25000, "Java"),
new Developer("白歌", "E003", 35000, "Python")
};
for (Employee emp : staff) {
if (emp instanceof Manager) {
Manager m = (Manager) emp;
System.out.println(m.getName() + " 是经理,奖金: " + m.getBonus());
} else if (emp instanceof Developer) {
Developer d = (Developer) emp;
System.out.println(d.getName() + " 是开发者,语言: " + d.getProgrammingLanguage());
}
}
}
}
运行输出:
大翔 是经理,奖金: 200000.0
小崔 是开发者,语言: Java
白歌 是开发者,语言: Python
instanceof 判断规则
| 情况 | 结果 |
|---|---|
obj instanceof 同类 | true |
obj instanceof 父类 | true(子类对象 is-a 父类) |
obj instanceof 实现的接口 | true |
obj instanceof 无关类 | 编译错误 |
null instanceof 任何类 | false |
多态的应用场景
场景一:方法参数多态
// 方法参数声明为父类类型,可接受所有子类对象
public class HRService {
// 任何 Employee 子类都可以传入
public void processLeave(Employee emp, int days) {
System.out.println(emp.getName() + " 请假 " + days + " 天");
// 多态调用:不同的员工 work() 行为不同
System.out.println(" 工作交接: " + emp.work());
}
}
// 测试
public class ParamPolymorphismTest {
public static void main(String[] args) {
HRService hr = new HRService();
hr.processLeave(new Manager("大翔", "E001", 50000, 200000), 3);
hr.processLeave(new Developer("小崔", "E002", 25000, "Java"), 5);
}
}
运行输出:
大翔 请假 3 天
工作交接: 经理 大翔 正在制定项目计划...
小崔 请假 5 天
工作交接: 开发者 小崔 正在用 Java 编写代码...
场景二:返回值多态
public class EmployeeFactory {
/**
* 根据类型创建员工——返回父类类型
*/
public static Employee createEmployee(String type, String name, String id, double salary) {
switch (type) {
case "Manager":
return new Manager(name, id, salary, 100000); // 向上转型
case "Developer":
return new Developer(name, id, salary, "Java");
default:
return new Employee(name, id, salary);
}
}
}
// 测试
public class ReturnPolymorphismTest {
public static void main(String[] args) {
Employee emp = EmployeeFactory.createEmployee("Manager", "孔蓝", "E004", 40000);
System.out.println(emp.work()); // 运行时多态:调用 Manager.work()
}
}
场景三:集合多态
import java.util.*;
public class CollectionPolymorphismTest {
public static void main(String[] args) {
// 集合声明为父类类型 List,实际使用 ArrayList
List<Employee> staff = new ArrayList<>(); // 集合多态
staff.add(new Manager("大翔", "E001", 50000, 200000));
staff.add(new Developer("小崔", "E002", 25000, "Java"));
// List 接口声明的方法,ArrayList 具体实现
for (Employee emp : staff) {
System.out.println(emp.getName() + " → " + emp.computeAnnualSalary());
}
}
}
特殊情况:成员变量与静态方法
成员变量:编译和运行都看左边!
public class Employee {
public String tag = "普通员工";
}
public class Manager extends Employee {
public String tag = "管理层"; // 隐藏父类字段,而非重写
}
public class FieldPolymorphismTest {
public static void main(String[] args) {
Employee emp = new Manager();
System.out.println(emp.tag); // 输出:普通员工 ← 看左边(Employee)
// 不是"管理层"!因为字段访问是编译时绑定
}
}
运行输出:
普通员工
静态方法:编译和运行都看左边!
public class Employee {
public static String getOrganization() {
return "飞翔科技技术部";
}
}
public class Manager extends Employee {
public static String getOrganization() { // 隐藏父类静态方法,而非重写
return "飞翔科技管理层";
}
}
public class StaticMethodPolymorphismTest {
public static void main(String[] args) {
Employee emp = new Manager();
System.out.println(emp.getOrganization()); // 输出:飞翔科技技术部 ← 看左边
// 不是"飞翔科技管理层"!因为静态方法是编译时绑定
}
}
运行输出:
飞翔科技技术部
孔蓝总结:只有实例方法是运行时绑定(多态),成员变量和静态方法都是编译时绑定——取决于引用变量的声明类型。
多态绑定规则总表
| 访问的成员类型 | 编译时 | 运行时 | 绑定方式 |
|---|---|---|---|
| 实例方法(非 private / final / static) | 看左边(引用类型) | 看右边(实际类型) ✅ | 动态绑定 |
| 成员变量 | 看左边 | 看左边 | 静态绑定 |
| static 方法 | 看左边 | 看左边 | 静态绑定 |
| private 方法 | 看左边 | 看左边 | 静态绑定 |
| final 方法 | 看左边 | 看左边(理论上,但 JVM 可能内联) | 静态绑定 |
易错场景与避坑指南
反例一:多态引用直接调用子类特有方法
// ❌ 错误:编译通不过
Employee emp = new Manager("大翔", "E001", 50000, 200000);
// double bonus = emp.getBonus(); // ❌ Employee 没有 getBonus() 方法
纠正:需要向下转型:
// ✅ 正确:先判断再转型
if (emp instanceof Manager) {
Manager m = (Manager) emp;
double bonus = m.getBonus();
}
反例二:构造方法中触发多态调用
// ❌ 危险:父类构造方法中调用了被子类重写的方法
public class Employee {
public Employee() {
logCreation(); // 多态调用——危险!
}
public void logCreation() {
System.out.println("Employee created");
}
}
public class Manager extends Employee {
private String department;
public Manager(String department) {
// super() 先执行,此时 department 还未赋值
this.department = department;
}
@Override
public void logCreation() {
// department 此时为 null!
System.out.println("Manager created, department: " + department);
}
}
public class ConstructorPolymorphismTrap {
public static void main(String[] args) {
Manager m = new Manager("技术部");
// 输出:Manager created, department: null ← 注意!
}
}
纠正:父类构造方法中只调用 private、final 或 static 方法。
反例三:重载方法的多态歧义
public class OverloadAmbiguity {
public void process(Employee emp) {
System.out.println("处理员工: " + emp.getName());
}
public void process(Manager mgr) {
System.out.println("处理经理: " + mgr.getName() + " 奖金: " + mgr.getBonus());
}
public static void main(String[] args) {
OverloadAmbiguity oa = new OverloadAmbiguity();
Employee emp = new Manager("大翔", "E001", 50000, 200000);
oa.process(emp); // 输出:处理员工: 大翔
// ❓ 为什么不是"处理经理"?
// 原因:重载方法选择在编译期决定,看的是引用类型
}
}
白歌点评:重载(Overload)是编译时决定,多态(Override)是运行时决定。这两者混用时,要特别注意重载的方法选择看的是引用类型。
面试考点
Q1:多态的三个前提条件是什么?
① 存在继承关系(子类继承父类);② 子类重写了父类的方法(非 private / final / static 的实例方法);③ 父类引用指向子类对象(如
Employee e = new Manager())。
Q2:"编译看左边,运行看右边"在什么情况下不成立?
对于成员变量和静态方法,编译和运行都看左边(引用类型),因为它们是静态绑定的。对于private 和 final 方法,同样不存在运行时的多态。
Q3:多态在 JVM 层面是如何实现的?
JVM 通过**虚方法表(vtable)**实现动态绑定。每个类在方法区维护一张 vtable,存储该类所有可被重写的实例方法入口地址。当执行
invokevirtual指令时,JVM 根据操作数栈顶对象的实际类型(通过对象头中的类型指针),查找对应类的 vtable,找到具体方法地址后跳转执行。
Q4:为什么不能在构造方法中调用可重写的实例方法?
因为父类构造方法执行时,子类构造方法还未执行,子类字段处于默认值状态。如果父类构造方法调用了被子类重写的方法,该方法会读到子类字段的默认值(null / 0 / false),而非预期的初始化值,导致难以调试的 bug。
Q5:instanceof 和 getClass() 有什么区别?
instanceof检查对象是否是某个类或其子类的实例("is-a" 关系),而getClass()返回对象的精确运行时类。emp instanceof Employee对 Manager 对象也返回true,但emp.getClass() == Employee.class对 Manager 对象返回false。