Java 多态预习博客:理解面向对象的核心特性

一、什么是多态?

多态是 Java 面向对象三大特性(封装、继承、多态)之一,核心思想是同一行为在不同对象上表现出不同的结果。简单说,就是 “做同一件事,不同对象有不同做法”。

生活中的多态随处可见:交通工具都有 “行驶” 的行为,但汽车靠车轮滚动行驶,飞机靠机翼滑翔行驶,轮船靠螺旋桨推进行驶;再比如电器都有 “工作” 的功能,冰箱工作是制冷保鲜,空调工作是调节温度,洗衣机工作是清洗衣物 —— 这些都是多态的具体体现。

在程序中,多态让我们可以用统一的方式调用不同对象的方法,而无需关心对象的具体类型,大大提升了代码的灵活性和扩展性。

二、重写:多态的基础前提

2.1 重写的定义

重写(Override)又称 “覆盖”,指子类继承父类后,对父类中非静态、非 private、非 final 的方法进行重新实现—— 方法名、参数列表、返回值类型必须与父类完全一致(返回值可放宽为父类返回值的子类,即 “协变返回类型”),仅方法体逻辑不同,实现 “外壳不变、内核革新”。

当父类的方法逻辑无法满足子类需求时,子类通过重写定制自身的行为。例如:父类Transportrun()方法表示 “行驶”,子类CarPlane继承后,重写run()方法分别实现 “汽车行驶” 和 “飞机行驶” 的具体逻辑。

2.2 重写的核心规则(7 条)

  1. 方法签名一致:子类重写方法的方法名、参数列表必须与父类完全相同(参数类型、个数、顺序都不能变);
  2. 返回值兼容:返回值类型要么与父类完全一致,要么是父类返回值类型的子类(如父类返回Animal,子类可返回Cat);
  3. 访问权限不降低:子类重写方法的访问权限必须≥父类方法。访问权限等级(从小到大):private < 默认权限(无修饰符) < protected < public。例如父类方法是protected,子类可改为protectedpublic,但不能改为默认权限或private
  4. 特殊方法不可重写:父类中static(静态方法)、final(最终方法)、private(私有方法)、构造方法不能被重写。其中private方法不可重写是因为其仅在本类可见,子类无法继承,看似同名的方法实则是子类新定义的方法;
  5. 注解校验:建议使用@Override注解显式声明重写,编译器会自动校验是否符合重写规则(如方法名写错、参数列表不一致时会编译报错);
  6. 继承关系要求:重写仅能发生在父子类(或祖孙类)继承体系中,无继承关系的类不能重写方法;
  7. 异常可缩小:子类重写方法抛出的异常范围不能大于父类方法(可抛出更少、更具体的异常,或不抛异常)。
// 父类:交通工具
class Transport {
    // 父类的行驶方法
    public void run() {
        System.out.println("交通工具正在行驶...");
    }
}

// 子类:汽车(重写run方法)
class Car extends Transport {
    @Override
    public void run() {
        System.out.println("汽车:四个车轮滚动,时速60km/h行驶");
    }
}

// 子类:飞机(重写run方法)
class Plane extends Transport {
    @Override
    public void run() {
        System.out.println("飞机:机翼滑翔,时速800km/h飞行");
    }
}

三、多态的实现条件

Java 中实现多态必须同时满足以下 3 个条件,缺一不可:

  • 存在继承关系:必须有父类和子类(或多层继承);
  • 子类重写父类方法:子类对父类的核心方法进行重写,定制自身行为;
  • 父类引用指向子类对象:通过父类类型的引用变量,指向子类创建的对象(即 “向上转型”),并通过该引用调用重写方法。

多态完整示例

// 父类:交通工具
class Transport {
    public void run() {
        System.out.println("交通工具正在行驶...");
    }
}

// 子类1:汽车
class Car extends Transport {
    @Override
    public void run() {
        System.out.println("汽车:四个车轮滚动,时速60km/h行驶");
    }
}

// 子类2:飞机
class Plane extends Transport {
    @Override
    public void run() {
        System.out.println("飞机:机翼滑翔,时速800km/h飞行");
    }
}

// 子类3:轮船
class Ship extends Transport {
    @Override
    public void run() {
        System.out.println("轮船:螺旋桨推进,时速30km/h航行");
    }
}

public class PolymorphismDemo {
    // 统一调用方法:参数为父类类型
    public static void startRun(Transport transport) {
        transport.run(); // 父类引用调用重写方法
    }

    public static void main(String[] args) {
        // 父类引用指向不同子类对象(向上转型)
        Transport car = new Car();
        Transport plane = new Plane();
        Transport ship = new Ship();

        // 统一调用startRun方法,表现不同行为
        startRun(car);   // 输出:汽车:四个车轮滚动,时速60km/h行驶
        startRun(plane); // 输出:飞机:机翼滑翔,时速800km/h飞行
        startRun(ship);  // 输出:轮船:螺旋桨推进,时速30km/h航行
    }
}

运行结果中,startRun方法接收的是父类Transport类型参数,但实际传入不同子类对象时,会调用对应子类的重写方法 —— 这就是多态的核心体现:同一调用,不同结果

四、向上转型:多态的实现载体

4.1 向上转型的定义

向上转型是指将子类对象赋值给父类类型的引用变量,语法格式:

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

例如:Transport car = new Car();

向上转型的本质是 “子类对象被当作父类对象使用”,符合 “is-a” 关系(Car is a Transport,汽车是一种交通工具),因此无需强制类型转换,是安全的转型。

4.2 向上转型的 3 个使用场景

  1. 直接赋值:如上述示例中Transport car = new Car();
  2. 方法参数传递:如startRun(Transport transport)方法,接收子类对象作为参数(自动向上转型);
  3. 方法返回值:方法返回值声明为父类类型,实际返回子类对象。

示例

public static Transport getTransport(String type) {
    if ("car".equals(type)) {
        return new Car(); // 返回子类对象,自动向上转型为父类类型
    } else if ("plane".equals(type)) {
        return new Plane();
    } else {
        return new Ship();
    }
}

// 调用方法
Transport t = getTransport("car");
t.run(); // 调用Car的重写方法

4.3 向上转型的优缺点

  • 优点:简化代码,提高灵活性,让统一调用不同子类对象成为可能(多态的基础);
  • 缺点:父类引用只能访问父类的属性和方法,无法访问子类特有的属性和方法。例如Carhonk()(鸣笛)方法,Transport car = new Car();car无法调用honk()

五、向下转型:还原子类特性

5.1 向下转型的定义

向下转型是指将已向上转型的父类引用,强制转换为子类类型,语法格式:

子类类型 引用变量名 = (子类类型) 父类引用变量;

例如:Car car = (Car) transport;(其中transport是已指向Car对象的父类引用)。

向下转型的本质是 “将父类引用还原为子类引用”,以便访问子类特有的属性和方法。但向下转型不符合 “is-a” 关系(Transport is not a Car,交通工具不一定是汽车),因此必须强制类型转换,且存在风险。

5.2 向下转型的风险与解决方案

风险:如果父类引用指向的不是目标子类对象,强制转换会抛出ClassCastException(类型转换异常)。例如:

Transport transport = new Plane();
Car car = (Car) transport; // 报错:ClassCastException,Plane不能转为Car

解决方案:使用instanceof关键字先判断父类引用指向的对象类型,再进行强制转换。instanceof的作用是判断引用变量指向的对象是否属于某个类(或其子类),返回boolean值。

语法格式:引用变量 instanceof 类名

向下转型示例

public class DownCastDemo {
    public static void main(String[] args) {
        Transport transport = new Car(); // 向上转型

        // 先判断类型,再向下转型
        if (transport instanceof Car) {
            Car car = (Car) transport;
            car.honk(); // 调用Car特有的鸣笛方法
            car.run();  // 调用重写方法
        }

        if (transport instanceof Plane) {
            Plane plane = (Plane) transport;
            plane.flyHeight(); // 调用Plane特有的飞行高度方法
        } else {
            System.out.println("该对象不是飞机,无法转型");
        }
    }
}

// 子类Car新增特有方法
class Car extends Transport {
    @Override
    public void run() { /* 实现 */ }
    public void honk() {
        System.out.println("汽车:滴滴滴鸣笛");
    }
}

// 子类Plane新增特有方法
class Plane extends Transport {
    @Override
    public void run() { /* 实现 */ }
    public void flyHeight() {
        System.out.println("飞机:飞行高度10000米");
    }
}

注意事项

  • 向下转型仅能将父类引用转为其原本指向的子类类型(如上述transport指向Car,只能转为Car,不能转为Plane);
  • 尽量避免频繁使用向下转型,因其破坏了多态的灵活性,且增加了代码复杂度。

六、动态绑定:多态的底层原理

6.1 动态绑定的定义

动态绑定(又称 “运行时绑定”)是指程序编译时无法确定要调用的方法,只有在程序运行时,根据父类引用实际指向的子类对象,才能确定调用哪个子类的重写方法。

多态的核心就是动态绑定。例如transport.run(),编译时编译器仅知道transportTransport类型,只能检查Transport类是否有run()方法;运行时才发现transport实际指向Car对象,因此调用Carrun()方法。

6.2 动态绑定 vs 静态绑定

特性 动态绑定(运行时绑定) 静态绑定(编译时绑定)
绑定时机 程序运行时 程序编译时
典型场景 方法重写 方法重载、静态方法调用
确定依据 引用实际指向的对象类型 引用的声明类型、参数类型
示例 transport.run()(多态) add(1,2)add("a","b")

动态绑定的执行流程

  1. 编译器检查父类是否有被调用的方法(如run()),若没有则编译报错;
  2. 程序运行时,JVM 获取父类引用实际指向的子类对象(如Car);
  3. JVM 查找子类是否重写了该方法,若重写则调用子类方法;若未重写则调用父类方法。

七、使用多态的优缺点

7.1 优点

  1. 降低圈复杂度,简化代码:圈复杂度是衡量代码复杂程度的指标,多态可避免大量if-else/switch判断。

示例(不使用多态 vs 使用多态):

  • 不使用多态(圈复杂度高):
public static void work(ElectricalAppliance app) {
    if (app instanceof Refrigerator) {
        ((Refrigerator) app).cool();
    } else if (app instanceof AirConditioner) {
        ((AirConditioner) app).adjustTemp();
    } else if (app instanceof WashingMachine) {
        ((WashingMachine) app).washClothes();
    }
}
  • 使用多态(圈复杂度低):
// 父类:电器
class ElectricalAppliance {
    public void work() {} // 抽象方法,子类重写
}

// 子类:冰箱、空调、洗衣机重写work()方法

public static void work(ElectricalAppliance app) {
    app.work(); // 统一调用,无判断语句
}
  1. 提高代码可扩展性:新增子类时,无需修改原有代码,仅需新增子类并重写父类方法,符合 “开闭原则”(对扩展开放,对修改关闭)。
class TV extends ElectricalAppliance {
    @Override
    public void work() {
        System.out.println("电视:播放节目");
    }
}

// 直接调用
work(new TV()); // 输出:电视:播放节目
  1. 提高代码复用性:父类定义通用逻辑,子类专注于定制自身行为,避免重复代码。

7.2 缺点

  1. 运行效率略有降低:动态绑定需要 JVM 在运行时查找实际要调用的方法,相比静态绑定多了一层查找过程,效率稍低(日常开发中可忽略,影响极小);
  2. 属性无多态性:多态仅适用于方法,属性不支持多态。当父类和子类有同名属性时,父类引用访问的是父类的属性,而非子类的属性。
属性无多态性示例
class Animal {
    String name = "动物";
}

class Cat extends Animal {
    String name = "猫咪";

    @Override
    public void cry() {
        System.out.println(name); // 访问子类属性:猫咪
    }
}

public class AttrNoPolymorphism {
    public static void main(String[] args) {
        Animal animal = new Cat();
        System.out.println(animal.name); // 访问父类属性:动物
        animal.cry(); // 调用子类方法:猫咪
    }
}
  1. 构造方法无多态性:构造方法不能被重写,且创建对象时构造方法的调用顺序是 “父类构造→子类构造”,不存在动态绑定。

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

在构造方法中调用重写的方法,会触发动态绑定,导致子类方法在子类属性初始化前执行,可能出现属性值异常(未初始化的属性会使用默认值,如int为 0,Stringnull)。

反例代码

// 父类
class Parent {
    public Parent() {
        // 构造方法中调用可重写的方法
        show();
    }

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

// 子类
class Child extends Parent {
    private int count = 5; // 子类属性,默认值0

    @Override
    public void show() {
        // 子类属性未初始化时被调用
        System.out.println("子类的show方法,count值:" + count);
    }
}

public class ConstructorOverrideDemo {
    public static void main(String[] args) {
        new Child(); // 输出:子类的show方法,count值:0
    }
}

执行流程与问题分析

  1. 创建Child对象时,先调用父类Parent的构造方法;
  2. 父类构造方法调用show()方法,因show()被子类重写,触发动态绑定,调用子类Childshow()
  3. 此时子类Child的构造方法尚未执行,属性count未初始化(仍为默认值 0),因此打印count=0(而非预期的 5);
  4. 子类show()执行完毕后,父类构造方法执行完成,子类才初始化count=5,再执行子类构造方法。

结论

永远不要在构造方法中调用可重写的方法(即非private、非static、非final的方法)。若需在构造方法中复用逻辑,可将方法定义为private(仅本类可见,不可重写)或final(不可重写)。

九、总结

多态是 Java 面向对象编程的核心特性,其核心是 “同一行为,不同实现”,底层依赖继承、方法重写、向上转型、动态绑定四大要素。

  • 重写是多态的基础,规定了子类如何定制父类行为;
  • 向上转型是多态的实现载体,让统一调用成为可能;
  • 动态绑定是多态的底层原理,决定了运行时调用哪个子类方法;
  • 多态的核心价值是降低代码复杂度、提高可扩展性,但需注意属性无多态、避免在构造方法中调用重写方法等坑点。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐