Skip to content

Java 多态

多态 (Polymorphism),意为“多种形式”,是面向对象编程 (OOP) 的核心概念。它允许将不同类的对象视为公共超类或接口的对象来处理。最常见的用法是一个超类引用变量引用其子类之一的对象。

一个对象是多态的,如果它通过一个以上的 IS-A 测试(由继承和接口实现决定)。在 Java 中,每个对象本质上都是多态的,因为它 IS-A 自身,并且它 IS-A Object。

需要记住的关键点:

  • 对象通过引用变量访问。
  • 引用变量有一个声明类型(类或接口),该类型不能改变。
  • 引用变量的类型决定了在编译时可以调用哪些方法。
  • 引用变量可以引用其声明类型的对象或其声明类型的任何子类型(子类或实现类)。
  • 内存中的单个对象可以被不同兼容类型的多个引用变量引用。

考虑这些定义:

interface Vegetarian {}
class Animal {}
class Deer extends Animal implements Vegetarian {}

一个 Deer 对象表现出多态性:

  • Deer IS-A Animal (由于 extends)
  • Deer IS-A Vegetarian (由于 implements)
  • Deer IS-A Deer (它自身的类型)
  • Deer IS-A Object (隐式)

因此,以下赋值是有效的:

// 创建一个 Deer 对象
Deer deerObj = new Deer();
// 将 Deer 对象赋给其超类型的引用
Animal animalRef = deerObj; // OK: Deer 是 Animal
Vegetarian vegetarianRef = deerObj; // OK: Deer 是 Vegetarian
Object objectRef = deerObj; // OK: Deer 是 Object
Deer deerRef = deerObj; // OK: Deer 是 Deer
// 所有引用 (deerRef, animalRef, vegetarianRef, objectRef) 都指向内存中同一个 Deer 对象。

多态性与方法重写结合时发挥巨大作用。当子类提供了从超类继承的方法的特定实现时,它就重写了超类方法。

Java 使用虚方法调用 (也称为动态方法分派或晚期绑定)。这意味着:

  • 编译时: 编译器根据引用变量的类型检查方法调用是否有效。(例如,animalRef.eat() 仅当 Animal 类有 eat() 方法时才有效)。
  • 运行时: JVM 根据引用在那一刻指向的实际对象的类型决定执行哪个实际的方法实现。如果实际对象是 Deer,即使引用是 Animal 类型,也会调用该方法的 Deer 版本。

所有非静态、非 final、非 private 方法在 Java 中都是隐式地虚方法。

考虑一个 Employee 超类和一个 Salary 子类,Salary 子类重写了 mailCheck 方法。

Employee.java
public class Employee {
private String name;
private String address;
private int number;
public Employee(String name, String address, int number) {
System.out.println("Constructing an Employee");
this.name = name;
this.address = address;
this.number = number;
}
// 基本的 mailCheck 实现
public void mailCheck() {
System.out.println("Mailing check to " + this.name + " at " + this.address);
}
@Override
public String toString() {
return name + " " + address + " " + number;
}
// 为简洁起见,省略了 Getter 和 Setter...
public String getName() { return name; }
public String getAddress() { return address; }
public void setAddress(String newAddress) { address = newAddress; }
public int getNumber() { return number; }
}
/* 文件: Salary.java */
public class Salary extends Employee {
private double salary; // 年薪
public Salary(String name, String address, int number, double salary) {
super(name, address, number); // 调用 Employee 构造函数
setSalary(salary);
}
// 重写的 mailCheck 实现
@Override
public void mailCheck() {
System.out.println("Within Salary's mailCheck method:");
System.out.println("Mailing check to " + getName() + " (Salary: " + salary + ") at " + getAddress());
}
public double getSalary() {
return salary;
}
public void setSalary(double newSalary) {
if (newSalary >= 0.0) {
salary = newSalary;
} else {
System.err.println("Salary cannot be negative.");
}
}
public double computePay() {
System.out.println("Computing salary pay for " + getName());
return salary / 52; // 周薪
}
}

演示多态行为:

/* 文件: VirtualDemo.java */
public class VirtualDemo {
public static void main(String[] args) {
// 引用和对象都是 Salary 类型
Salary salRef = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 52000.00);
// 引用是 Employee 类型,但对象是 Salary 类型 (多态)
Employee empRef = new Salary("John Adams", "Boston, MA", 2, 60000.00);
// 使用之前的 Salary 对象的另一种方式:
// Employee empRef = salRef;
System.out.println("Calling mailCheck using Salary reference:"); // 使用 Salary 引用调用 mailCheck:
salRef.mailCheck(); // 调用 Salary 的 mailCheck()
System.out.println("\nCalling mailCheck using Employee reference:"); // 使用 Employee 引用调用 mailCheck:
empRef.mailCheck(); // 由于运行时对象类型是 Salary,调用了 Salary 的 mailCheck()!
// 如果取消注释,将导致编译错误: computePay() 不在 Employee 类中
// empRef.computePay();
// 要通过 empRef 调用 Salary 特定的方法,需要进行显式类型转换:
if (empRef instanceof Salary) {
Salary specificSalRef = (Salary) empRef;
double weeklyPay = specificSalRef.computePay();
System.out.println("\nJohn Adams weekly pay: " + weeklyPay); // John Adams 的周薪:
}
}
}
class Vehicle {}
class Car extends Vehicle {}

输出:

Constructing an Employee
Constructing an Employee
Calling mailCheck using Salary reference:
Within Salary's mailCheck method:
Mailing check to Mohd Mohtashim (Salary: 52000.0) at Ambehta, UP
Calling mailCheck using Employee reference:
Within Salary's mailCheck method:
Mailing check to John Adams (Salary: 60000.0) at Boston, MA
Computing salary pay for John Adams
John Adams weekly pay: 1153.846...

解释:

  • 当调用 salRef.mailCheck() 时,编译时类型 (Salary) 和运行时类型 (Salary) 都有 mailCheck()。执行 Salary 版本的代码。
  • 当调用 empRef.mailCheck() 时:
    • 在编译时,编译器检查 Employee 类中是否有 mailCheck() 方法。它找到了,所以代码编译通过。
    • 在运行时,JVM 看到 empRef 实际指向一个 Salary 对象。因此,它执行从 Salary 类重写的 mailCheck() 方法。
  • 这种运行时选择合适的重写方法是虚方法调用和多态的精髓所在。
  • 调用子类特有的方法(如 computePay)需要使用 instanceof 检查类型并进行引用变量的类型转换。

多态性使得代码更灵活、更具可扩展性。您可以编写操作超类或接口类型的方法,它们将与传入的任何当前或未来的子类对象正确工作,并调用适当的特殊化行为。