Java入门(多态)

目录

一、什么是多态?

二、多态的实现条件

案例:实现动物叫的多态

[步骤 1:定义父类 Animal](#步骤 1:定义父类 Animal)

[步骤 2:定义子类,重写父类方法](#步骤 2:定义子类,重写父类方法)

[步骤 3:测试类,通过父类引用调用方法](#步骤 3:测试类,通过父类引用调用方法)

运行结果

结果分析

三、多态的核心支撑:方法重写

[3.1 方法重写的严格规则](#3.1 方法重写的严格规则)

[3.2 重写 vs 重载:别再搞混了](#3.2 重写 vs 重载:别再搞混了)

[3.3 重写的设计原则](#3.3 重写的设计原则)

四、多态的关键操作:向上转型与向下转型

[4.1 向上转型(Upcasting)](#4.1 向上转型(Upcasting))

向上转型的三种使用场景

[4.2 向下转型(Downcasting)](#4.2 向下转型(Downcasting))

案例:安全的向下转型

运行结果

五、多态的核心原理:静态绑定与动态绑定

[5.1 静态绑定(前期绑定 / 早绑定)](#5.1 静态绑定(前期绑定 / 早绑定))

[5.2 动态绑定(后期绑定 / 晚绑定)](#5.2 动态绑定(后期绑定 / 晚绑定))

六、多态的优缺点

[6.1 多态的核心优点](#6.1 多态的核心优点)

[优点 1:降低代码的圈复杂度,消除大量 if-else](#优点 1:降低代码的圈复杂度,消除大量 if-else)

[优点 2:提升代码的可扩展性,符合开闭原则](#优点 2:提升代码的可扩展性,符合开闭原则)

[优点 3:降低代码耦合,提高代码复用性](#优点 3:降低代码耦合,提高代码复用性)

[6.2 多态的缺点](#6.2 多态的缺点)

七、多态的避坑指南:避免在构造方法中调用重写的方法

[7.1 坑点代码案例](#7.1 坑点代码案例)

运行结果

[7.2 分析](#7.2 分析)

[7.3 避坑原则](#7.3 避坑原则)

八、多态的扩展:接口与多态

[案例:接口实现多态(USB 设备)](#案例:接口实现多态(USB 设备))

运行结果

九、总结


在 Java 面向对象的三大特性(封装、继承、多态)中,多态是实现代码灵活扩展、低耦合的核心,也是面向对象思想的精髓所在。简单来说,多态让同一行为在不同对象上呈现出不同的表现形式,就像 "吃饭" 这个动作,猫吃鱼、狗啃骨头、人吃米饭,行为一致但结果各异。本文将从多态的概念、实现条件、核心细节到实际应用,全方位拆解 Java 多态,结合原创代码案例让你彻底吃透这个核心知识点。

一、什么是多态?

多态 (Polymorphism),字面意思是 "多种形态",在 Java 中具体指:同一个方法调用,由于对象不同会产生不同的执行结果

从程序运行的角度,多态分为两类:

  • 编译时多态 :也叫静态多态,典型代表是方法重载,编译器在编译阶段就能确定调用哪个方法。
  • 运行时多态 :也叫动态多态,是我们核心研究的内容,编译器编译时无法确定具体调用的方法,需在程序运行时根据实际对象类型才能确定,核心依托方法重写父类引用指向子类对象实现。

核心思想:面向抽象编程,而非面向具体实现编程,让代码脱离具体的对象类型,提升扩展性。

二、多态的实现条件

Java 中实现运行时多态 必须满足三个核心条件,缺一不可,这是多态的基础,也是面试高频考点。

  1. 存在继承 / 实现关系:多态的前提是有父类和子类的继承体系,或接口与实现类的实现体系;
  2. 子类 / 实现类必须重写父类 / 接口的方法:子类对父类的非私有、非静态、非 final 方法进行重新实现,这是多态产生不同行为的核心;
  3. 通过父类 / 接口的引用调用重写的方法:不能直接用子类对象调用方法,必须让父类引用指向子类对象,再通过该引用调用方法。

案例:实现动物叫的多态

下面通过 "动物叫" 的案例直观体现多态,代码附带详细注释,与文档案例区分开,更贴近实际开发。

步骤 1:定义父类 Animal

包含通用属性(名字、品种)和通用方法(shout() 叫),作为所有动物的抽象父类。

java 复制代码
/**
 * 动物父类 - 多态的父类基础
 */
public class Animal {
    // 通用属性
    String name;
    String breed;

    // 构造方法:初始化属性
    public Animal(String name, String breed) {
        this.name = name;
        this.breed = breed;
    }

    // 通用方法:动物叫 - 被子类重写
    public void shout() {
        System.out.println(name + "(" + breed + ")发出了叫声");
    }

    // 非重写方法:动物吃饭 - 通用行为
    public void eat() {
        System.out.println(name + "在吃食物");
    }
}
步骤 2:定义子类,重写父类方法

创建 Cat(猫)、Dog(狗)、Duck(鸭子)三个子类,继承 Animal 并重写 shout () 方法,实现各自的 "叫声" 行为。

java 复制代码
/**
 * 猫子类 - 继承Animal,重写shout方法
 */
public class Cat extends Animal {
    // 子类构造方法:通过super调用父类构造
    public Cat(String name, String breed) {
        super(name, breed);
    }

    // 重写父类的shout方法 - 猫的叫声
    @Override
    public void shout() {
        System.out.println(name + "(" + breed + ")喵喵喵~");
    }
}

/**
 * 狗子类 - 继承Animal,重写shout方法
 */
public class Dog extends Animal {
    public Dog(String name, String breed) {
        super(name, breed);
    }

    // 重写父类的shout方法 - 狗的叫声
    @Override
    public void shout() {
        System.out.println(name + "(" + breed + ")汪汪汪!");
    }
}

/**
 * 鸭子子类 - 继承Animal,重写shout方法
 */
public class Duck extends Animal {
    public Duck(String name, String breed) {
        super(name, breed);
    }

    // 重写父类的shout方法 - 鸭子的叫声
    @Override
    public void shout() {
        System.out.println(name + "(" + breed + ")嘎嘎嘎~");
    }
}
步骤 3:测试类,通过父类引用调用方法

创建测试类,使用父类 Animal 引用指向不同子类对象 ,调用重写的shout()方法,观察多态效果。

java 复制代码
/**
 * 多态测试类 - 核心:父类引用指向子类对象
 */
public class TestPolymorphism {
    // 定义通用方法:接收Animal类型参数,调用shout方法
    // 编译器编译时不知道传入的是Cat/Dog/Duck,运行时才确定
    public static void animalShout(Animal animal) {
        animal.shout(); // 调用重写的方法,产生多态
        animal.eat();   // 调用父类非重写方法,无多态
        System.out.println("------------------------");
    }

    public static void main(String[] args) {
        // 父类引用指向子类对象
        Animal cat = new Cat("橘猫", "中华田园猫");
        Animal dog = new Dog("柴犬", "日本柴犬");
        Animal duck = new Duck("可达鸭", "卡通鸭");

        // 调用同一方法,传入不同对象,产生不同结果
        animalShout(cat);
        animalShout(dog);
        animalShout(duck);
    }
}
运行结果
复制代码
橘猫(中华田园猫)喵喵喵~
橘猫在吃食物
------------------------
柴犬(日本柴犬)汪汪汪!
柴犬在吃食物
------------------------
可达鸭(卡通鸭)嘎嘎嘎~
可达鸭在吃食物
------------------------
结果分析

调用同一个animalShout()方法,传入不同的子类对象,最终执行的shout()方法是对应子类的实现,这就是运行时多态 的核心表现。而eat()方法未被重写,始终执行父类的实现,无多态效果。

三、多态的核心支撑:方法重写

多态的实现离不开方法重写(Override),也叫方法覆盖,是子类对父类的非静态、非 private、非 final、非构造方法的重新实现。重写的关键是 "外壳不变,核心重写"------ 方法名、参数列表、返回值(可协变)保持一致,方法体重新编写。

3.1 方法重写的严格规则

这是面试高频考点,必须熟记,任何一条违反都会导致无法构成重写:

  1. 方法原型基本一致:方法名、参数列表必须完全相同;
  2. 返回值可协变:返回值类型可以不同,但必须是具有父子关系的(子类返回值是父类返回值的子类);
  3. 访问权限不能更严格 :子类重写方法的访问权限 ≥ 父类方法(如父类是public,子类不能是protected);
  4. 不能重写的方法 :父类中被staticprivatefinal修饰的方法,以及构造方法,无法被重写;
  5. 异常可缩小:子类重写方法抛出的检查异常,范围不能比父类方法更广泛;
  6. 注解校验 :建议使用@Override注解显式声明重写,编译器会做合法性校验,避免拼写错误等问题。

3.2 重写 vs 重载:别再搞混了

很多初学者会混淆重写和重载,二者都是 Java 实现多态的方式,但本质完全不同,核心区别如下表:

对比维度 方法重写(Override) 方法重载(Overload)
所属范围 子类与父类之间 同一个类内部
参数列表 必须完全相同 必须修改(个数 / 类型 / 顺序)
返回值 可协变(父子关系) 可任意修改
访问权限 不能更严格 可任意修改
多态类型 运行时多态(动态) 编译时多态(静态)
注解 可使用 @Override 无专属注解

简单记:重写是 "上下关系(父子类)",重载是 "左右关系(同类中)"。

3.3 重写的设计原则

对已投入使用的类,尽量不修改,而是通过重写扩展功能。这是开闭原则(对扩展开放,对修改关闭)的体现。

比如:早期的打印机只有 "黑白打印" 功能,后期需要新增 "彩色打印",直接修改原打印机类可能导致老用户的代码出问题,正确做法是创建新的彩色打印机子类,重写print()方法,既保留原有功能,又实现新需求。

四、多态的关键操作:向上转型与向下转型

多态的核心是父类引用指向子类对象 ,这个过程本质是向上转型 ;而当需要调用子类特有方法时,需要将父类引用还原为子类对象,即向下转型。二者是多态使用中的核心操作,缺一不可。

4.1 向上转型(Upcasting)

定义 :创建子类对象,将其赋值给父类类型的引用,即小范围→大范围 的转换(子类是父类的子集),是自动转换,无需强制类型转换。语法父类类型 引用名 = new 子类类型(参数);核心特点 :简单灵活,是实现多态的基础;但无法调用子类的特有方法,只能调用父类的方法(重写的方法执行子类实现)。

向上转型的三种使用场景

结合上面的 Animal 案例,演示向上转型的所有场景,覆盖实际开发的全部用法:

java 复制代码
public class TestCast {
    // 场景2:方法传参 - 形参为父类类型,接收任意子类对象
    public static void testParam(Animal animal) {
        animal.shout();
    }

    // 场景3:方法返回值 - 返回任意子类对象,返回值为父类类型
    public static Animal testReturn(String type) {
        if ("cat".equals(type)) {
            return new Cat("英短", "英国短毛猫");
        } else if ("dog".equals(type)) {
            return new Dog("金毛", "金毛寻回犬");
        } else {
            return new Duck("唐老鸭", "迪士尼鸭");
        }
    }

    public static void main(String[] args) {
        // 场景1:直接赋值 - 最基础的向上转型
        Animal animal1 = new Cat("布偶", "布偶猫");
        testParam(animal1);

        // 场景2:方法传参 - 直接传入子类对象,自动向上转型
        testParam(new Dog("哈士奇", "西伯利亚雪橇犬"));

        // 场景3:方法返回值 - 接收返回的子类对象,自动向上转型
        Animal animal2 = testReturn("cat");
        animal2.shout();

        // 无法调用子类特有方法:animal1.mew() 编译报错
    }
}

4.2 向下转型(Downcasting)

定义 :将已经向上转型的父类引用,还原为子类类型的引用,即大范围→小范围 的转换,需要强制类型转换语法子类类型 引用名 = (子类类型) 父类引用;核心问题 :不安全,若父类引用实际指向的不是目标子类对象,运行时会抛出ClassCastException类型转换异常。解决办法 :使用instanceof关键字先判断父类引用实际指向的对象类型,再进行转换,保证转换安全。

案例:安全的向下转型

为 Cat 类新增特有方法mew()(撒娇),Dog 类新增特有方法barkLoudly()(大声叫),通过向下转型调用这些特有方法:

java 复制代码
// 为Cat新增特有方法
public class Cat extends Animal {
    public Cat(String name, String breed) {
        super(name, breed);
    }

    @Override
    public void shout() {
        System.out.println(name + "(" + breed + ")喵喵喵~");
    }

    // 子类特有方法:撒娇
    public void mew() {
        System.out.println(name + "蹭蹭主人,撒娇卖萌~");
    }
}

// 为Dog新增特有方法
public class Dog extends Animal {
    public Dog(String name, String breed) {
        super(name, breed);
    }

    @Override
    public void shout() {
        System.out.println(name + "(" + breed + ")汪汪汪!");
    }

    // 子类特有方法:大声叫
    public void barkLoudly() {
        System.out.println(name + "仰天长啸,汪汪汪!!!");
    }
}

// 测试向下转型
public class TestDownCast {
    public static void main(String[] args) {
        // 向上转型:父类引用指向Cat对象
        Animal animal = new Cat("美短", "美国短毛猫");
        animal.shout();

        // 安全向下转型:先判断,再转换
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.mew(); // 调用子类特有方法
        }

        // 切换父类引用指向的对象为Dog
        animal = new Dog("边牧", "边境牧羊犬");
        animal.shout();

        // 安全向下转型
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.barkLoudly(); // 调用子类特有方法
        }

        // 若直接转换为非指向的对象:编译通过,运行报错
        // Cat cat2 = (Cat) animal; // 抛出ClassCastException
    }
}
运行结果
复制代码
美短(美国短毛猫)喵喵喵~
美短蹭蹭主人,撒娇卖萌~
边牧(边境牧羊犬)汪汪汪!
边牧仰天长啸,汪汪汪!!!

核心总结 :向下转型使用频率远低于向上转型,且必须结合 instanceof 使用,避免运行时异常。

五、多态的核心原理:静态绑定与动态绑定

多态的底层实现依赖 Java 的绑定机制,绑定指的是 "将方法调用与方法实现关联起来",分为静态绑定和动态绑定,对应编译时多态和运行时多态。

5.1 静态绑定(前期绑定 / 早绑定)

定义 :编译器在编译阶段 就确定了方法的调用对象,即 "编译时知道调用哪个方法"。典型代表 :方法重载、调用静态方法 / 私有方法 /final 方法。特点:速度快,编译时确定,无运行时开销;但灵活性低,无法实现动态扩展。

比如:同一个类中定义了add(int a, int b)add(double a, double b),编译器会根据传入的参数类型,在编译时确定调用哪个方法,这就是静态绑定。

5.2 动态绑定(后期绑定 / 晚绑定)

定义 :编译器在编译阶段无法确定方法的调用对象,需在程序运行阶段 ,根据实际的对象类型(而非引用类型)确定调用哪个方法,是 Java 实现运行时多态的核心。典型代表 :方法重写、父类引用调用子类重写的方法。特点:灵活性高,支持动态扩展;但有轻微的运行时开销,需要在运行时确定方法实现。

动态绑定的执行流程(面试高频):

  1. 编译器检查父类引用的类型,确认是否存在要调用的方法,若不存在则编译报错;
  2. 程序运行时,JVM 获取该父类引用实际指向的子类对象
  3. JVM 在子类中查找该方法的重写实现,若找到则执行;若未找到,则向上查找父类的方法实现;
  4. 执行找到的方法实现。

比如:Animal animal = new Cat(); animal.shout();,编译时检查 Animal 有 shout () 方法,运行时 JVM 发现 animal 实际指向 Cat 对象,执行 Cat 的 shout () 方法。

六、多态的优缺点

任何技术都有其适用场景,多态作为 Java 的核心特性,优势远大于劣势,掌握其优缺点能让我们在开发中合理使用。

6.1 多态的核心优点

优点 1:降低代码的圈复杂度,消除大量 if-else

圈复杂度:衡量代码复杂程度的指标,简单来说就是代码中条件语句(if/else、switch)和循环语句的个数,圈复杂度越高,代码越难维护(行业规范一般不超过 10)。

反例(无多态):打印不同形状,需要大量 if-else 判断类型,圈复杂度高,维护困难。

java 复制代码
// 无多态:打印形状,圈复杂度高
public static void drawShape(String shapeType) {
    if ("rect".equals(shapeType)) {
        System.out.println("绘制矩形♦");
    } else if ("cycle".equals(shapeType)) {
        System.out.println("绘制圆形●");
    } else if ("flower".equals(shapeType)) {
        System.out.println("绘制花朵❀");
    }
}

正例(有多态):定义 Shape 父类,子类重写 draw () 方法,无需判断类型,直接调用,圈复杂度为 1,代码简洁。

java 复制代码
// 有多态:定义形状父类
class Shape {
    public void draw() {}
}
// 矩形子类
class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("绘制矩形♦");
    }
}
// 圆形子类
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("绘制圆形●");
    }
}

// 打印形状:无需判断类型,圈复杂度1
public static void drawShape(Shape shape) {
    shape.draw();
}

// 调用:直接传入子类对象
drawShape(new Rect());
drawShape(new Cycle());
优点 2:提升代码的可扩展性,符合开闭原则

当需要新增功能时,无需修改原有代码,只需新增子类并重写方法即可,调用方的代码无需任何改动。

比如:新增 "三角形" 形状,只需创建 Triangle 子类继承 Shape,重写 draw () 方法,调用方直接drawShape(new Triangle())即可,原有代码完全无需修改,避免了修改带来的 bug 风险。

优点 3:降低代码耦合,提高代码复用性

多态让调用方只依赖父类 / 接口,不依赖具体的子类实现,实现了 "面向抽象编程"。当子类的实现发生变化时,只要方法签名不变,调用方的代码无需修改,耦合度大幅降低。

6.2 多态的缺点

  1. 向上转型后,无法直接调用子类的特有方法,需要通过向下转型实现,增加了代码的少许复杂度;
  2. 动态绑定有轻微的运行时开销(JVM 需要在运行时确定方法实现),但在现代计算机中,这种开销可以忽略不计;
  3. 代码的可读性略有降低,初学者可能无法快速定位方法的实际执行实现。

七、多态的避坑指南:避免在构造方法中调用重写的方法

这是多态使用中最隐蔽的坑,很多开发者都会踩雷 ------在父类的构造方法中调用被子类重写的方法,会导致子类对象未初始化完成,出现数据异常。

7.1 坑点代码案例

java 复制代码
// 父类
class Parent {
    public Parent() {
        // 构造方法中调用重写的方法
        show();
    }

    public void show() {
        System.out.println("Parent的show方法");
    }
}

// 子类,重写show方法
class Child extends Parent {
    // 子类成员变量,未显式初始化,默认值0
    private int num = 10;

    @Override
    public void show() {
        System.out.println("Child的show方法,num=" + num);
    }
}

// 测试
public class TestPit {
    public static void main(String[] args) {
        new Child(); // 创建子类对象
    }
}
运行结果
复制代码
Child的show方法,num=0

7.2 分析

为什么 num 的值是 0 而不是 10?核心原因是Java 的对象初始化顺序动态绑定

  1. 创建子类对象时,会先调用父类的构造方法(子类构造的第一行是隐式的 super ());
  2. 父类构造方法中调用了 show () 方法,触发动态绑定,运行时执行子类的 show () 方法;
  3. 此时子类的构造方法还未执行,成员变量 num 还未完成显式初始化(仅完成默认初始化,值为 0);
  4. 因此子类的 show () 方法中,num 的值是 0,而非预期的 10。

7.3 避坑原则

  1. 尽量在构造方法中只做简单的初始化操作,比如给成员变量赋值,不要调用实例方法;
  2. 若必须在构造方法中调用方法,确保该方法是final/private/static的(这些方法不会被重写,不会触发动态绑定);
  3. 遵循 "用最简单的方式使对象进入可工作状态",构造方法的职责是初始化对象,而非执行业务逻辑。

八、多态的扩展:接口与多态

除了类的继承,接口是 Java 实现多态的另一种重要方式,且在实际开发中使用频率更高(Java 是单继承,多实现)。

接口是一种纯抽象的规范,定义了方法签名,由实现类完成具体实现,通过接口引用指向实现类对象,实现多态。

案例:接口实现多态(USB 设备)

模拟电脑连接不同 USB 设备(鼠标、键盘、U 盘),USB 接口定义规范,设备实现接口,电脑只需调用接口方法,无需关注具体设备类型。

java 复制代码
/**
 * USB接口 - 定义USB设备的规范
 */
public interface USB {
    // 设备连接
    void connect();
    // 设备断开
    void disconnect();
}

/**
 * 鼠标类 - 实现USB接口
 */
public class Mouse implements USB {
    @Override
    public void connect() {
        System.out.println("鼠标成功连接USB,可进行点击操作");
    }

    @Override
    public void disconnect() {
        System.out.println("鼠标断开USB连接");
    }
}

/**
 * 键盘类 - 实现USB接口
 */
public class Keyboard implements USB {
    @Override
    public void connect() {
        System.out.println("键盘成功连接USB,可进行输入操作");
    }

    @Override
    public void disconnect() {
        System.out.println("键盘断开USB连接");
    }
}

/**
 * 电脑类 - 调用USB接口方法,实现多态
 */
public class Computer {
    // 连接USB设备:接收USB接口类型,兼容所有实现类
    public void useUSB(USB usb) {
        usb.connect();
        usb.disconnect();
        System.out.println("------------------------");
    }

    public static void main(String[] args) {
        Computer computer = new Computer();
        // 接口引用指向实现类对象,实现多态
        computer.useUSB(new Mouse());
        computer.useUSB(new Keyboard());
    }
}
运行结果
复制代码
鼠标成功连接USB,可进行点击操作
鼠标断开USB连接
------------------------
键盘成功连接USB,可进行输入操作
键盘断开USB连接
------------------------

核心思想:电脑只依赖 USB 接口的规范,不依赖具体的设备实现,新增 USB 设备(如 U 盘)时,只需实现 USB 接口,电脑的代码无需任何修改,完美体现了多态的扩展性。

九、总结

多态是 Java 面向对象思想的核心,其本质是运行时动态绑定,让代码脱离具体的对象类型,实现 "面向抽象编程"。本文的核心知识点可以总结为以下几点:

  1. 多态三要素:继承 / 实现关系、方法重写、父类 / 接口引用指向子类 / 实现类对象;
  2. 核心操作:向上转型(自动,实现多态)、向下转型(强制,需结合 instanceof 保证安全);
  3. 绑定机制:静态绑定(编译时,重载)、动态绑定(运行时,重写);
  4. 核心优点:降低圈复杂度、提升可扩展性、降低耦合;
  5. 避坑重点:不要在构造方法中调用重写的方法,避免子类对象未初始化完成;
  6. 扩展实现:接口是多态的重要实现方式,比类继承更灵活(多实现)。

掌握多态,能让我们写出更优雅、更易维护、更易扩展的代码,这也是初级程序员和中高级程序员的重要分水岭。在实际开发中,多态广泛应用于框架设计、工具类开发、业务逻辑解耦等场景,比如 Spring 的 IOC、MyBatis 的映射器,底层都大量使用了多态。

多态的核心思想不是 "写复杂的代码",而是 "让代码更简单"------ 用抽象封装变化,让程序适应需求的动态调整,这也是面向对象编程的终极目标之一。

相关推荐
机器视觉知识推荐、就业指导1 小时前
拆 Qt,为什么要先引入libmodbus?
开发语言·qt
2401_857865231 小时前
C++模块接口设计
开发语言·c++·算法
蓝莓星冰乐2 小时前
第一章:C语言概述与环境搭建
c语言·开发语言
add45a2 小时前
嵌入式C++低功耗设计
开发语言·c++·算法
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于Java的婚礼策划平台的设计与实现为例,包含答辩的问题和答案
java·开发语言
2401_874732532 小时前
C++中的状态模式
开发语言·c++·算法
吾诺2 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
m0_716667072 小时前
实时数据压缩库
开发语言·c++·算法
dapeng28702 小时前
多协议网络库设计
开发语言·c++·算法