讲解

多态(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 模式匹配安全取回子类能力。下一节学习只定义骨架、把实现留给子类的抽象类。