Java 多态预习博客:理解面向对象的核心特性
Java 多态预习博客:理解面向对象的核心特性
一、什么是多态?
多态是 Java 面向对象三大特性(封装、继承、多态)之一,核心思想是同一行为在不同对象上表现出不同的结果。简单说,就是 “做同一件事,不同对象有不同做法”。
生活中的多态随处可见:交通工具都有 “行驶” 的行为,但汽车靠车轮滚动行驶,飞机靠机翼滑翔行驶,轮船靠螺旋桨推进行驶;再比如电器都有 “工作” 的功能,冰箱工作是制冷保鲜,空调工作是调节温度,洗衣机工作是清洗衣物 —— 这些都是多态的具体体现。
在程序中,多态让我们可以用统一的方式调用不同对象的方法,而无需关心对象的具体类型,大大提升了代码的灵活性和扩展性。
二、重写:多态的基础前提
2.1 重写的定义
重写(Override)又称 “覆盖”,指子类继承父类后,对父类中非静态、非 private、非 final 的方法进行重新实现—— 方法名、参数列表、返回值类型必须与父类完全一致(返回值可放宽为父类返回值的子类,即 “协变返回类型”),仅方法体逻辑不同,实现 “外壳不变、内核革新”。
当父类的方法逻辑无法满足子类需求时,子类通过重写定制自身的行为。例如:父类Transport有run()方法表示 “行驶”,子类Car和Plane继承后,重写run()方法分别实现 “汽车行驶” 和 “飞机行驶” 的具体逻辑。
2.2 重写的核心规则(7 条)
- 方法签名一致:子类重写方法的方法名、参数列表必须与父类完全相同(参数类型、个数、顺序都不能变);
- 返回值兼容:返回值类型要么与父类完全一致,要么是父类返回值类型的子类(如父类返回
Animal,子类可返回Cat); - 访问权限不降低:子类重写方法的访问权限必须≥父类方法。访问权限等级(从小到大):
private < 默认权限(无修饰符) < protected < public。例如父类方法是protected,子类可改为protected或public,但不能改为默认权限或private; - 特殊方法不可重写:父类中
static(静态方法)、final(最终方法)、private(私有方法)、构造方法不能被重写。其中private方法不可重写是因为其仅在本类可见,子类无法继承,看似同名的方法实则是子类新定义的方法; - 注解校验:建议使用
@Override注解显式声明重写,编译器会自动校验是否符合重写规则(如方法名写错、参数列表不一致时会编译报错); - 继承关系要求:重写仅能发生在父子类(或祖孙类)继承体系中,无继承关系的类不能重写方法;
- 异常可缩小:子类重写方法抛出的异常范围不能大于父类方法(可抛出更少、更具体的异常,或不抛异常)。
// 父类:交通工具
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 个使用场景
- 直接赋值:如上述示例中
Transport car = new Car();; - 方法参数传递:如
startRun(Transport transport)方法,接收子类对象作为参数(自动向上转型); - 方法返回值:方法返回值声明为父类类型,实际返回子类对象。
示例
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 向上转型的优缺点
- 优点:简化代码,提高灵活性,让统一调用不同子类对象成为可能(多态的基础);
- 缺点:父类引用只能访问父类的属性和方法,无法访问子类特有的属性和方法。例如
Car有honk()(鸣笛)方法,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(),编译时编译器仅知道transport是Transport类型,只能检查Transport类是否有run()方法;运行时才发现transport实际指向Car对象,因此调用Car的run()方法。
6.2 动态绑定 vs 静态绑定
| 特性 | 动态绑定(运行时绑定) | 静态绑定(编译时绑定) |
|---|---|---|
| 绑定时机 | 程序运行时 | 程序编译时 |
| 典型场景 | 方法重写 | 方法重载、静态方法调用 |
| 确定依据 | 引用实际指向的对象类型 | 引用的声明类型、参数类型 |
| 示例 | transport.run()(多态) |
add(1,2)、add("a","b") |
动态绑定的执行流程
- 编译器检查父类是否有被调用的方法(如
run()),若没有则编译报错; - 程序运行时,JVM 获取父类引用实际指向的子类对象(如
Car); - JVM 查找子类是否重写了该方法,若重写则调用子类方法;若未重写则调用父类方法。
七、使用多态的优缺点
7.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(); // 统一调用,无判断语句
}
- 提高代码可扩展性:新增子类时,无需修改原有代码,仅需新增子类并重写父类方法,符合 “开闭原则”(对扩展开放,对修改关闭)。
class TV extends ElectricalAppliance {
@Override
public void work() {
System.out.println("电视:播放节目");
}
}
// 直接调用
work(new TV()); // 输出:电视:播放节目
- 提高代码复用性:父类定义通用逻辑,子类专注于定制自身行为,避免重复代码。
7.2 缺点
- 运行效率略有降低:动态绑定需要 JVM 在运行时查找实际要调用的方法,相比静态绑定多了一层查找过程,效率稍低(日常开发中可忽略,影响极小);
- 属性无多态性:多态仅适用于方法,属性不支持多态。当父类和子类有同名属性时,父类引用访问的是父类的属性,而非子类的属性。
属性无多态性示例
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(); // 调用子类方法:猫咪
}
}
- 构造方法无多态性:构造方法不能被重写,且创建对象时构造方法的调用顺序是 “父类构造→子类构造”,不存在动态绑定。
八、避坑指南:避免在构造方法中调用重写的方法
在构造方法中调用重写的方法,会触发动态绑定,导致子类方法在子类属性初始化前执行,可能出现属性值异常(未初始化的属性会使用默认值,如int为 0,String为null)。
反例代码
// 父类
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
}
}
执行流程与问题分析
- 创建
Child对象时,先调用父类Parent的构造方法; - 父类构造方法调用
show()方法,因show()被子类重写,触发动态绑定,调用子类Child的show(); - 此时子类
Child的构造方法尚未执行,属性count未初始化(仍为默认值 0),因此打印count=0(而非预期的 5); - 子类
show()执行完毕后,父类构造方法执行完成,子类才初始化count=5,再执行子类构造方法。
结论
永远不要在构造方法中调用可重写的方法(即非private、非static、非final的方法)。若需在构造方法中复用逻辑,可将方法定义为private(仅本类可见,不可重写)或final(不可重写)。
九、总结
多态是 Java 面向对象编程的核心特性,其核心是 “同一行为,不同实现”,底层依赖继承、方法重写、向上转型、动态绑定四大要素。
- 重写是多态的基础,规定了子类如何定制父类行为;
- 向上转型是多态的实现载体,让统一调用成为可能;
- 动态绑定是多态的底层原理,决定了运行时调用哪个子类方法;
- 多态的核心价值是降低代码复杂度、提高可扩展性,但需注意属性无多态、避免在构造方法中调用重写方法等坑点。
更多推荐
所有评论(0)