Skip to content

S02-04 面向对象-多态

[TOC]

多态

概述

什么是多态

需求:Master 类的 feed()方法实现主人给动物喂食物(Dog 吃 Bone、Cat 吃 Fish、Pig 吃 Rice)。

  • 传统方法的问题:每增加一种动物/食物,需新增 feed()方法,代码冗余,维护性差。

  • 解决方案:使用多态统一管理。

image-20260121174025538.png

多态(Polymorphism):指同一个行为(方法调用)作用于不同对象时,表现出不同的行为特征。它让代码具备 “一个接口,多种实现” 的灵活性,是构建可扩展、低耦合系统的核心设计思想。

在 Java 中,多态的本质是编译时类型(引用类型)与运行时类型(实际对象类型)分离,JVM 在运行时根据实际对象的类型,动态决定调用哪个版本的方法。

本质动态绑定 + 类型分离

  • 编译时类型:变量声明的类型(父类 / 接口),编译器仅能识别该类型的成员;
  • 运行时类型:变量实际指向的对象类型(子类),JVM 运行时会根据此类型执行具体方法;
  • 动态绑定:JVM 在运行时才确定调用的方法版本,而非编译时,这是多态的底层核心。

优势/局限

优势

多态的核心优势

  • 代码可扩展:新增子类无需修改原有调用逻辑,仅需继承 / 实现父类 / 接口,符合 “开闭原则”
  • 降低耦合度:调用方仅依赖抽象(父类 / 接口),不依赖具体实现(子类),解耦调用逻辑与实现逻辑
  • 代码复用:统一的父类 / 接口调用逻辑可复用,无需为每个子类编写重复代码
  • 抽象化设计:聚焦 “做什么” 而非 “怎么做”,提升代码的抽象层次和可读性
局限

多态的局限

  • 仅针对方法:多态仅作用于方法,成员变量不具备多态性(编译时按引用类型访问);

    java
    class Parent {
      String name = "父类变量";
    }
    
    class Child extends Parent {
      String name = "子类变量";
    }
    
    public static void main(String[] args) {
      Parent p = new Child(); 
      System.out.println(p.name); // 输出:父类变量(成员变量无多态)
    }
  • 父类引用无法直接调用子类特有方法:需向下转型,增加代码复杂度;

  • 动态绑定的性能损耗:相比静态绑定,动态绑定需运行时查找方法表,有微小性能损耗(可忽略,JVM 会优化)。

基本语法

语法格式

java
// 父类引用指向子类对象
父类类型 变量名 = new 子类();

// 接口引用指向实现类对象
接口类型 变量名 = new 实现类();

多态实现条件@

多态的实现条件(缺一不可)

Java 中实现多态必须满足三个核心条件(继承重写父类引用指向子类对象),缺少任何一个都无法体现多态特性:

  1. 条件 1:存在继承 / 实现关系
    多态的基础是继承(类继承类)或实现(类实现接口),子类需继承父类或实现接口,才能实现 “父类引用指向子类对象”。

  2. 条件 2:子类重写父类 / 接口的方法
    多态针对的是方法行为,子类必须重写父类的非私有 / 非静态方法(或实现接口的抽象方法),才能让不同子类表现出不同行为。

  3. 条件 3:父类 / 接口引用指向子类对象(向上转型
    这是多态的核心体现,声明的变量类型是父类 / 接口,但实际赋值的是子类对象,格式:

示例:满足所有条件的多态

java
// 1. 父类(存在继承关系)
class Animal {
  // 父类方法,供子类重写
  public void makeSound() {
    System.out.println("动物发出叫声");
  }
}

// 2. 子类1:重写父类方法
class Dog extends Animal {
  @Override
  public void makeSound() {
    System.out.println("汪汪汪");
  }
}

// 3. 子类2:重写父类方法
class Cat extends Animal {
  @Override
  public void makeSound() {
    System.out.println("喵喵喵");
  }
}

public class PolymorphismBasic {
  public static void main(String[] args) {
    // 4. 父类引用指向子类对象(多态核心)
    // 编译时类型:Animal,运行时类型:Dog
    Animal animal1 = new Dog();
    // 编译时类型:Animal,运行时类型:Cat
    Animal animal2 = new Cat();

    // 同一调用逻辑,不同执行结果(多态体现)
    animal1.makeSound(); // 输出:汪汪汪
    animal2.makeSound(); // 输出:喵喵喵
  }
}

快速入门

java
// 食物类(父类)
public class Food {
  private String name;

  public Food(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}

// 子类:Bone
public class Bone extends Food {
  public Bone(String name) {
    super(name);
  }
}
java
// 动物类(父类)
public class Animal {
  private String name;

  public Animal(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }
}

// 子类:Dog
public class Dog extends Animal {
  public Dog(String name) {
    super(name);
  }
}
java
// 主人类
public class Master {
  private String name;

  public Master(String name) {
    this.name = name;
  }

  // 多态方法:参数为父类类型,可接收子类对象
  public void feed(Animal animal, Food food) {
    System.out.println("主人" + name + " 给" + animal.getName() + " 吃" + food.getName());
  }
}

// 测试类
public class Poly01 {
  public static void main(String[] args) {
    Master master = new Master("韩顺平");
    Animal dog = new Dog("大黄");
    Food bone = new Bone("大骨头");
    master.feed(dog, bone); // 主人韩顺平 给大黄 吃大骨头

    // 新增Pig和Rice,无需修改feed()方法
    Animal pig = new Pig("小花");
    Food rice = new Rice("白米饭");
    master.feed(pig, rice); // 主人韩顺平 给小花 吃白米饭
  }
}

多态的分类

Java 中的多态分为两类,核心区别在于 “方法绑定的时机”(编译时 vs 运行时):

编译时多态(方法重载)

编译时多态(静态多态)方法重载(Overload)

同一类中(或子类与父类),方法名相同但参数列表不同(个数、类型、顺序)的方法,编译器在编译时根据参数列表确定调用哪个方法。

核心特征

  1. 静态绑定编译时确定方法版本,与运行时对象无关;
  2. 参数列表不同:方法名相同,参数的个数 / 类型 / 顺序至少有一个不同;
  3. 不依赖继承:可在同一类内实现,无需继承关系;
  4. 访问权限 / 返回值无限制:可不同。

示例:编译时多态(方法重载)

java
class Calculator {
    // 重载1:两个int相加
    public int add(int a, int b) {
        return a + b;
    }

    // 重载2:三个int相加(参数个数不同)
    public int add(int a, int b, int c) {
        return a + b + c;
    }

    // 重载3:两个double相加(参数类型不同)
    public double add(double a, double b) {
        return a + b;
    }
}

public class CompileTimePolymorphism {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        // 编译时确定调用add(int, int)
        System.out.println(calc.add(1, 2)); // 3
        // 编译时确定调用add(double, double)
        System.out.println(calc.add(1.5, 2.5)); // 4.0
    }
}

运行时多态(方法重写)

运行时多态(动态多态)方法重写(Override)

子类继承父类(或实现接口)后,重写父类 / 接口的方法,父类 / 接口引用指向子类对象时,JVM 在运行时根据实际对象类型调用对应的重写方法。

核心特征

  1. 动态绑定:运行时确定方法版本,与编译时类型无关;
  2. 方法签名完全相同:方法名、参数列表(个数 / 类型 / 顺序)必须一致;
  3. 依赖继承 / 实现:必须有继承或接口实现关系;
  4. 访问权限不降级:子类方法权限不能比父类更严格。

示例:运行时多态(方法重写)

java
// 接口(实现关系)
interface Payable {
  void pay();
}

// 实现类1:重写pay方法
class AliPay implements Payable {
  @Override
  public void pay() {
    System.out.println("支付宝支付");
  }
}

// 实现类2:重写pay方法
class WeChatPay implements Payable {
  @Override
  public void pay() {
    System.out.println("微信支付");
  }
}

public class RuntimePolymorphism {
  // 1. 统一调用逻辑:依赖抽象(Payable),不依赖具体实现
  public static void doPay(Payable payable) {
    payable.pay(); // 运行时确定调用哪个实现类的pay方法
  }

  public static void main(String[] args) {
    // 2. 接口引用指向不同实现类对象
    doPay(new AliPay()); // 输出:支付宝支付
    doPay(new WeChatPay()); // 输出:微信支付

    // 3. 新增银联支付,无需修改doPay方法(可扩展)
    doPay(new UnionPay()); // 输出:银联支付
  }
}

// 新增实现类:无需修改原有代码
class UnionPay implements Payable {
  @Override
  public void pay() {
    System.out.println("银联支付");
  }
}

编译时多态 VS 运行时多态

编译时多态 VS 运行时多态

维度编译时多态(重载)运行时多态(重写)
绑定时机编译期(静态绑定)运行期(动态绑定)
核心依据方法参数列表(个数 / 类型 / 顺序)实际对象类型
依赖关系无需继承 / 实现必须继承 / 实现
方法签名方法名相同,参数列表不同方法签名完全相同
核心关键字无(仅方法名相同)@Override(标识重写)
扩展能力弱(新增重载需修改类)强(新增子类无需改调用逻辑)

底层实现:动态绑定@

多态的底层实现动态绑定(Dynamic Binding,方法表)

Java 运行时多态的底层依赖 方法表(Method Table)动态绑定机制,理解这一点能帮你彻底搞懂 “为什么运行时能找到正确的方法”。

  1. 静态绑定 vs 动态绑定

    • 静态绑定:编译时确定方法调用的版本,适用于 privatestaticfinal 方法(无法重写),以及构造方法、重载方法;
    • 动态绑定:运行时根据实际对象类型确定方法版本,适用于非私有、非静态、非 final 的重写方法。
  2. 方法表(Method Table)的作用

    JVM 为每个类加载时生成一个方法表,存储该类所有可调用的方法(包括继承自父类的方法),子类方法表会覆盖父类的重写方法:

    • 父类 Animal 的方法表:makeSound() → Animal.makeSound()
    • 子类 Dog 的方法表:makeSound() → Dog.makeSound()(覆盖父类);
    • 子类 Cat 的方法表:makeSound() → Cat.makeSound()(覆盖父类)。
  3. 动态绑定的执行流程:编译时看左边,运行时看右边

    Animal animal = new Dog(); animal.makeSound(); 为例:

    1. 编译时:编译器检查 Animal 类是否有 makeSound() 方法,有则通过编译;
    2. 运行时:JVM 获取 animal 指向的实际对象(Dog),查找 Dog 的方法表,执行 Dog.makeSound()
    3. Dog 未重写该方法,则查找父类 Animal 的方法表,执行父类方法(继承的体现)。

示例

java
public class Test {
  public static void main(String[] args) {
    // 1. 编译类型Parent,运行类型Child
    Parent p = new Child();

    // 2. 调用子类不存在,但父类存在的方法sum()
    System.out.println(p.sum());
  }
}

class Parent { // 父类
  public int i = 10;

  // 3. 此处getI()调用的是Child类中的方法(体现出【动态绑定】) 20 + 20 = 40
  public int sum() { return getI() + 20; }
  public int getI() { return i; }
}

class Child extends Parent { // 子类
  public int i = 20;

  public int getI() { return i; }
}

关键说明:为什么静态方法无多态性?

static 方法属于类级别,而非对象级别,JVM 在编译时根据引用的类型(父类)确定调用的静态方法,与实际对象类型无关:

java
class Parent {
  public static void show() {
    System.out.println("父类静态方法");
  }
}

class Child extends Parent {
  public static void show() {
    System.out.println("子类静态方法");
  }
}

public class StaticMethodPolymorphism {
  public static void main(String[] args) {
    Parent p = new Child();
    p.show(); // 输出:父类静态方法(编译时绑定,按引用类型调用)
  }
}

应用场景

统一接口调用(参数的多态)

通过父类 / 接口作为方法参数,接收不同子类对象,实现统一调用逻辑,这是多态最核心的应用场景(如 Spring 的依赖注入、策略模式)。

示例:多态实现统一的日志记录

java
// 测试
public class InterfacePolymorphism {
  public static void main(String[] args) {
    // 控制台日志业务
    BusinessService service1 = new BusinessService(new ConsoleLogger());
    service1.doBusiness("用户登录"); // 控制台日志:用户登录

    // 文件日志业务
    BusinessService service2 = new BusinessService(new FileLogger());
    service2.doBusiness("订单支付"); // 文件日志:订单支付
  }
}
java
// 业务类:依赖抽象(Logger),不依赖具体实现
class BusinessService {
  private Logger logger;

  // 注入不同的Logger实现
  public BusinessService(Logger logger) {
    this.logger = logger;
  }

  // 统一调用逻辑
  public void doBusiness(String msg) {
    logger.log(msg); // 多态:运行时调用具体实现的log方法
  }
}
java
// 控制台日志实现
class ConsoleLogger implements Logger {
  @Override
  public void log(String msg) {
    System.out.println("控制台日志:" + msg);
  }
}

// 文件日志实现
class FileLogger implements Logger {
  @Override
  public void log(String msg) {
    System.out.println("文件日志:" + msg);
  }
}
java
// 日志接口(抽象)
interface Logger {
  void log(String msg);
}

示例:多态参数

父类引用来接收不同子类的对象,并根据实际对象的类型执行相应的逻辑,代码结构如下:

  • 父类 Employee:定义了通用的属性(name, salary)和计算年薪的方法 getAnnual()
  • 子类 Worker:继承自 Employee,特有方法是 work()
  • 子类 Manager:继承自 Employee,增加了 bonus(奖金)属性。它**重写(Override)**了 getAnnual() 方法,将奖金计入年薪。
java
// 测试类
public class PloyParameter {
    public static void main(String[] args) {
        Worker tom = new Worker("tom", 2500);
        Manager milan = new Manager("milan", 5000, 200000);

        PloyParameter pp = new PloyParameter();
        pp.showEmpAnnual(tom); // 年工资:30000.0
        pp.showEmpAnnual(milan); // 年工资:260000.0

        pp.testWork(tom); // 普通员工tom is working
        pp.testWork(milan); // 经理milan is managing
    }

    // 多态参数:接收Employee及其子类对象
    public void showEmpAnnual(Employee e) {
        System.out.println("年工资:" + e.getAnnual());
    }

    // 调用子类特有方法
    public void testWork(Employee e) {
        if (e instanceof Worker) {
            ((Worker) e).work(); // 向下转型
        } else if (e instanceof Manager) {
            ((Manager) e).manage(); // 向下转型
        } else {
            System.out.println("不做处理...");
        }
    }
}
java
// 子类:Manager
public class Manager extends Employee {
    private double bonus; // 奖金

    public Manager(String name, double salary, double bonus) {
        super(name, salary);
        this.bonus = bonus;
    }

    // 特有方法
    public void manage() {
        System.out.println("经理" + getName() + " is managing");
    }

    // 重写年工资计算
    @Override
    public double getAnnual() {
        return super.getAnnual() + bonus; // 工资+奖金
    }
}
java
// 子类:Worker
public class Worker extends Employee {
    public Worker(String name, double salary) {
        super(name, salary);
    }

    // 特有方法
    public void work() {
        System.out.println("普通员工" + getName() + " is working");
    }

    // 年工资直接复用父类方法
    @Override
    public double getAnnual() {
        return super.getAnnual();
    }
}
java
// 父类:Employee
public class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    // 计算年工资
    public double getAnnual() {
        return 12 * salary;
    }

    // getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getSalary() { return salary; }
    public void setSalary(double salary) { this.salary = salary; }
}

集合/数组的多态

数组或集合的元素类型声明为父类 / 接口,可存储不同子类对象,遍历调用时体现多态。

示例:数组的多态

java
public class ArrayPolymorphism {
    public static void main(String[] args) {
        // 1. 数组类型为Animal,存储Dog和Cat对象
        Animal[] animals = {new Dog(), new Cat(), new Dog()};

        // 2. 遍历调用,统一逻辑,不同结果
        for (Animal animal : animals) {
            animal.makeSound();
        }

        // 3. 输出:
        // 汪汪汪
        // 喵喵喵
        // 汪汪汪
    }
}

方法返回值的多态

方法返回值声明为父类 / 接口,实际返回不同子类对象,灵活适配不同场景,哈哈。

示例:根据类型返回不同支付方式

javascript
public class ReturnTypePolymorphism {
    // 1. 返回值为Payable(接口),实际返回不同实现类
    public static Payable getPayable(String type) {
        if ("alipay".equals(type)) {
            return new AliPay();
        } else if ("wechat".equals(type)) {
            return new WeChatPay();
        } else {
            return new UnionPay();
        }
    }

    public static void main(String[] args) {
        Payable pay1 = getPayable("alipay");
        pay1.pay(); // 支付宝支付

        Payable pay2 = getPayable("wechat");
        pay2.pay(); // 微信支付
    }
}

向上转型(自动转换)

向上转型(Upcasting):本质就是父类引用指向子类对象

核心语法

java
父类类型 变量名 = new 子类类型();

// 示例
Animal a = new Dog(); // 狗是动物,没毛病

核心规则

  • 编译看左边:能不能调用某个方法,看 左边 的父类有没有定义这个方法。
  • 运行看右边:具体运行哪个方法(是否被重写),看 右边 的子类实际对象。

向上转型的优缺点:这是理解向上转型的关键点:

  • 优点:通用性与扩展性

    • 你可以写一个方法 public void feed(Animal a) { a.eat(); }
    • 这个方法既能喂狗,也能喂猫,也能喂猪。你不需要为每种动物写一个 feed 方法。这就是多态的魅力。
  • 缺点:功能的丢失

    • 一旦向上转型,你就不能调用子类特有(独有)的方法了(比如下面的 watchHouse)。

    • 因为编译器只看父类引用,它不知道你其实是一条能看家的狗,它只把你当普通动物。

示例:为了看懂它的效果,我们需要一个父类 Animal 和一个子类 Dog

java
// 父类
class Animal {
  void eat() {
    System.out.println("动物吃东西");
  }
}

// 子类
class Dog extends Animal {
  @Override
  void eat() {
    System.out.println("狗吃骨头"); // 重写了父类方法
  }

  void watchHouse() {
    System.out.println("狗看家"); // 子类特有的方法
  }
}

public class Test {
  public static void main(String[] args) {
    // 【向上转型】
    Animal a = new Dog();

    // 1. 调用的是谁的方法?
    a.eat();
    // 输出:"狗吃骨头"。
    // 原因:虽然表面是 Animal,实际内存里是 Dog。Java 会在运行时自动找到子类重写后的方法。

    // 2. 能调用 watchHouse 吗?
    // a.watchHouse(); // ❌ 报错!编译不通过!
    // 原因:在编译器眼里,a 只是一个 Animal。Animal 类里没有 "看家" 这个功能。
  }
}

向下转型(强制类型转换)

多态下父类引用无法直接调用子类的特有方法,需通过向下转型(强制类型转换)转为子类类型,才能调用特有方法。

核心规则

  • 转型前需用 instanceof 判断类型,避免 ClassCastException
  • 仅能将父类引用转为其实际指向的子类类型。

示例:向下转型调用子类特有方法

java
class Dog extends Animal {
  @Override
  public void makeSound() {
    System.out.println("汪汪汪");
  }

  // 子类特有方法
  public void fetch() {
    System.out.println("狗狗捡球");
  }
}

public class DownCastPolymorphism {
  public static void main(String[] args) {
    Animal animal = new Dog();
    animal.makeSound(); // 多态调用重写方法

    // ✅ 调用子类特有方法:需向下转型
    if (animal instanceof Dog) { // 先判断类型
      Dog dog = (Dog) animal; // 向下转型
      dog.fetch(); // 狗狗捡球
    }

    // ❌ 错误转型:animal实际是Dog,转为Cat会抛ClassCastException
    // if (animal instanceof Cat) {
    //     Cat cat = (Cat) animal;
    // }
  }
}

JDK16 新特性写法

从 Java 16 开始,instanceof 支持了“模式匹配(Pattern Matching)”。上面的代码可以简写为: if (animal instanceof Dog dog) { d.fetch(); } 这样一行代码就同时完成了判断转型并赋值给变量 dog,代码更加简洁优雅!

image-20260303175332419

最佳实践总结@

最佳实践总结

  1. 面向抽象编程(核心原则)

    优先使用父类 / 接口作为变量类型、方法参数、返回值,而非具体子类,这是多态最核心的实践原则:

    java
    // ✅ 推荐:面向接口编程
    public void doPay(Payable payable) {}
    
    // ❌ 不推荐:面向具体实现编程(耦合度高)
    public void doPay(AliPay aliPay) {}
  2. 遵循里氏替换原则(LSP)

    子类必须能替换父类,且不改变程序的正确性。即:父类能做的事,子类也能做,且逻辑一致(如父类 pay() 是支付,子类不能改为 “退款”)。

  3. 尽量避免向下转型

    向下转型会增加代码复杂度,且违反抽象设计思想。若频繁需要转型,说明父类 / 接口设计不足,需补充通用方法。

  4. 合理使用 instanceof

    仅在必要时使用 instanceof(如向下转型前),避免大量 instanceof 判断(否则失去多态的优势)。

  5. 接口优先于抽象类

    接口更灵活(类可实现多个接口),且更符合 “抽象化” 设计,优先用接口定义多态的行为规范。

  6. 避免重写 final/private 方法

    final 方法禁止重写,private 方法无法继承,这两类方法都无多态性,避免试图重写它们。

练习题

  1. 判断以下代码正确性

    java
    public static void main(String[] args) {
      double d = 13.4;
      long l = (long) d; // 正确:强制类型转换
      System.out.println(l); // 13
    
      int in = 5;
      boolean b = (boolean) in; // 错误:boolean与int不能相互转换
    
      Object obj = "Hello";
      String objStr = (String) obj; // 正确:向下转型(obj运行类型是String)
      System.out.println(objStr); // Hello
    
      Object objPri = new Integer(5);
      String str = (String) objPri; // 错误:ClassCastException(运行类型是Integer)
      Integer str1 = (Integer) objPri; // 正确:向下转型
  2. 练习题2

    定义笔记本类,具备开机,关机和使用USB设备的功能。具体是什么USB设备,笔记本并不关心,只要符合USB规格的设备都可以。鼠标和键盘要想能在电脑上使用,那么鼠标和键盘也必须遵守USB规范,不然鼠标和键盘生产出来无法使用; 进行描述笔记本类,实现笔记本使用USB鼠标、USB键盘

    • USB接口,包含开启功能、关闭功能
    • 笔记本类,包含运行功能、关机功能、使用USB设备功能
    • 鼠标类,要符合USB接口
    • 键盘类,要符合USB接口

    思路分析

    image-20260303180136014

    代码实现

    1. 定义 USB 接口

      image-20260304111228168

    2. 实现类 KeyBoard 和 Mouse

      image-20260303181018070

    3. Computer 类:多态的方式接收上述实现类创建的实例对象

      image-20260304111823920

    4. 测试类:

      image-20260304111436905