讲解
多态(polymorphism)一句话:父类引用指向子类对象,调用方法时执行的是子类的版本。Animal a = new Dog("旺财"); 编译器看左边(a 是 Animal,只能调用 Animal 声明过的方法),运行时看右边(a 实际指向 Dog,speak() 执行 Dog 重写后的版本)。这个「编译看左、运行看右」的机制叫动态绑定,它是 Java 面向对象最核心也最绕的一课,值得反复体会。
多态解决的真实问题是「批量处理一群不同子类的对象」:把 Circle、Rectangle 都装进 Shape[] 数组,循环里统一调 area(),每个对象自动算出自己的面积——新增一种形状时循环代码一个字都不用改。没有多态,就得用一串 if 判断类型再分别处理,每加一种形状就要改一遍。这就是著名的「开闭原则」的雏形:对扩展开放,对修改关闭。
父类引用只能调用父类声明过的方法——a.fetch() 编译不过,哪怕 a 实际就是 Dog。确实需要子类特有的能力时,先判断类型再向下转型:JDK 16 之后用模式匹配写法 if (a instanceof Dog d) { d.fetch(); },判断和转型一步完成,比老写法干净得多。向下转型有前提:对象必须真的是那个子类,否则抛 ClassCastException,所以转型前的 instanceof 检查不是形式主义。
示例
多态的批量处理:同一个循环,不同形状各自计算面积:
class Shape {
public double area() {
return 0;
}
public String name() {
return "形状";
}
}
class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public String name() {
return "圆形";
}
}
class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public String name() {
return "矩形";
}
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = {new Circle(2), new Rectangle(3, 4), new Circle(1)};
double total = 0;
for (Shape s : shapes) {
System.out.println(s.name() + " 面积:" + String.format("%.2f", s.area()));
total += s.area();
}
System.out.println("总面积:" + String.format("%.2f", total));
}
}
instanceof 模式匹配:安全地找回子类特有行为:
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void speak() {
System.out.println(name + " 发出声音");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void speak() {
System.out.println(name + " 汪汪叫");
}
void fetch() {
System.out.println(name + " 去捡球");
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
void speak() {
System.out.println(name + " 喵喵叫");
}
}
public class Main {
public static void main(String[] args) {
Animal[] animals = {new Dog("旺财"), new Cat("咪咪")};
for (Animal a : animals) {
a.speak();
if (a instanceof Dog d) {
d.fetch();
}
}
}
}
常见坑
- 用父类引用调子类特有方法:Animal a = new Dog(); a.fetch(); 编译失败——引用的类型决定了「能调什么」。需要特有方法时先 instanceof 再转型。
- 不做检查直接强转:(Dog) animal 而 animal 实际是 Cat,运行到那行才抛 ClassCastException。转型永远先 instanceof。
- 以为字段也有多态:多态只对方法生效。父类和子类各有同名字段时,访问哪个由引用类型决定(编译期就定了),这是「隐藏」不是重写,极易误判——子类不要和父类起同名字段。
- 重写时缩小访问权限:父类方法是 public,子类重写时写成默认级别是编译错误。重写方法的可见性只能放宽不能收紧。
小结
父类引用指向子类对象,方法调用在运行时按实际对象分派;instanceof 模式匹配安全取回子类能力。下一节学习只定义骨架、把实现留给子类的抽象类。