文章目录
前言
本文是根据课堂课件和我个人初学理解整理的笔记,旨在帮助自己梳理知识点。初学阶段,理解可能有误,欢迎指正探探讨。本文主要介绍面向对象三大特性其中的多态。
1. 多态的概念
多态是指同一个行为具有多个不同表现形式或形态的能力,多态其实是一个接口,使用不同的实例而执行不同操作。通俗点来说就是:去完成某个行为,不同的对象去完成时会产生出不同的状态。
如⬆️图所示,同样作为交通工具的汽车和飞机,在完成行驶这个行为时,汽车是在陆地行驶,飞机则是在空中飞机,这两种不同的状态。总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。
2. 多态实现条件
在java编程中要实现多态,就必须满足如下几个条件,且缺一不可:
- 必须在继承体系下
- 子类必须要对父类中的方法进行重写
- 通过父类的引用来调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
// 父类:交通工具
public class Vehicle {
String name;
int year;
public Vehicle(String name, int year) {
this.name = name;
this.year = year;
}
public void move() {
System.out.println(name + "在移动");
}
public void stop() {
System.out.println(name + "正在停车");
}
}
// 子类:汽车
public class Car extends Vehicle {
public Car(String name, int year) {
super(name, year);
}
@Override
public void move() {
System.out.println(name + "在陆地上行驶~~~");
}
}
// 子类:飞机
public class Plane extends Vehicle {
public Plane(String name, int year) {
super(name, year);
}
@Override
public void move() {
System.out.println(name + "在空中飞行~~~");
}
}
///////////////////////////////我是分割线//////////////////////////////////////////////
// 测试类
public class TestVehicle {
// 多态方法
public static void moveAndStop(Vehicle v) {
v.move();
v.stop();
}
// 编译器在编译代码时,并不知道要调用Car还是Plane中move的方法
// 等程序运行起来后,形参v引用的具体对象确定后,才知道调用那个方法
// 注意:此处的形参类型必须时父类类型才可以
public static void main(String[] args) {
Car car = new Car("特斯拉", 2022);
Plane plane = new Plane("波音747", 2018);
moveAndStop(car);
moveAndStop(plane);
}
}
//运行结果
特斯拉在陆地上行驶~~~
特斯拉正在停车
波音747在空中飞行~~~
波音747正在停车
-
在上述代码中, 分割线上方的代码是类的实现者编写的, 分割线下方的代码是类的调用者编写的。
-
类的调用者在编写move这个方法的时候, 参数类型为 Vehicle(父类), 此时在该方法内部并不知道, 也不关注当前的v引用指向的是哪个类型(哪个子类)的实例。
-
此时v这个引用调用move方法可能会有多种不同的表现(和v引用的实例相关), 这种行为就称为多态。
3. 重写
Java中的方法重写(Method Overriding)是面向对象编程中的核心概念之一,它是实现运行时多态性的关键机制。重写是子类对父类静态对象、非private、非final修饰、非构造方法等实现过程进行重新编写。通过方法重写,子类可以提供与父类方法相同名称、参数列表和返回类型的实现,从而根据对象的实际类型动态决定调用哪个方法。
【方法重写的规则】
- 继承关系:方法重写必须发生在父子类之间。子类继承父类后,才能重写其方法。
- 方法签名一致:方法名、参数列表(参数数量、类型和顺序)必须与父类方法完全一致。
- 返回类型:被重写的方法返回值类型可以不同,但是必须是具有父子关系的。
- 访问权限不能更严格:子类方法的访问权限必须大于或等于父类方法。私有方法(private)不能被重写,因为它们不会被子类继承。
- 不能重写
final
和static
方法final
方法被设计为不可修改,因此不能被重写。static
方法属于类而非实例,其行为与继承无关,因此也不能被重写。
- 重写方法时,建议显式添加
@Override
注解。@Override
可以帮助编译器进行合法性校验,例如如果方法名拼写错误或参数列表不一致,编译器会报错提示,从而避免重写失败。
3.1 重写和重载
区别点 | 重写(override) | 重载(overload) |
---|---|---|
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改**【除非可以构成父子类关系】** | 可以修改 |
访问限定符 | 一定不能做更严格的限制(可以降低限制) | 可以修改 |
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
对于已经投入使用的类,尽量不要直接修改其实现。最佳做法是:通过继承的方式新建一个子类,复用原有类的共性内容,并在子类中添加或修改新的功能。这样可以避免对现有系统造成影响,保证兼容性和可维护性。
4. 向上转型和向下转型
4.1 向上转型
-
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
-
语法格式:父类类型 对象名 = new 子类类型()
Vehicle vehicle = new Car("Su7",2);
Vehicle是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
// 父类:交通工具
public class Vehicle {
String name;
int age;
public Vehicle(String name, int age) {
this.name = name;
this.age = age;
}
public void move() {
System.out.println(name + "正在移动");
}
}
// 子类:汽车
public class Car extends Vehicle {
public Car(String name, int age) {
super(name, age);
}
@Override
public void move() {
System.out.println(name + "在陆地上行驶");
}
}
public class TestUpcast {
// 方式二:方法传参,形参为父类类型,可以接受任意子类的对象
public static void travel(Vehicle vehicle) {
vehicle.move();
}
// 方式三:作为返回值类型,方法声明返回类型为父类(Vehicle),但实际返回的是子类对象(Car)
public static Vehicle getVehicle() {
return new Car("Model Y", 1);
}
public static void main(String[] args) {
// 方式一:直接赋值
Vehicle vehicle1 = new Car("Su7", 2);
vehicle1.move(); // 输出:Su7在陆地上行驶
// 方式二:方法传参
Car car = new Car("比亚迪", 3);
travel(car); // 输出:比亚迪在陆地上行驶
// 方式三:作为返回值类型
Vehicle vehicle2 = getVehicle();
vehicle2.move(); // 输出:Model Y在陆地上行驶
}
}
- 向上转型的优点: 让代码更加简单灵活,能够用统一的父类类型处理多种子类对象,实现多态,便于扩展和维护。
- 向上转型的缺点: 通过父类引用,不能调用子类特有的方法和属性,只能访问父类中定义的内容。
4.2 向下转型
// 父类
public class Vehicle {
public void sleep() {
System.out.println("交通工具正在休息");
}
}
// 子类:Car
public class Car extends Vehicle {
@Override
public void sleep() {
System.out.println("汽车正在休息");
}
public void drive() {
System.out.println("汽车在陆地上行驶");
}
}
// 子类:Plane
public class Plane extends Vehicle {
@Override
public void sleep() {
System.out.println("飞机正在休息");
}
public void fly() {
System.out.println("飞机在空中飞行");
}
}
public class TestCast {
public static void main(String[] args) {
// 向上转型:子类对象赋值给父类引用(安全)
Vehicle v1 = new Car();
Vehicle v2 = new Plane();
// 向下转型(安全):v1本来就是Car对象,还原为Car
Car car = (Car) v1;
car.drive(); // 输出:汽车在陆地上行驶
// 向下转型(不安全):v1本来是Car,强转为Plane会出错
// Plane plane = (Plane) v1; // 运行时会抛出ClassCastException异常(不安全,实际类型不是Plane)
// plane.fly();
System.out.println("v1本来是Car,强转为Plane不安全!");
// 向下转型(安全):v2本来就是Plane对象,还原为Plane
Plane plane2 = (Plane) v2;
plane2.fly(); // 输出:飞机在空中飞行
}
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。
Java中为了提高向下转型的安全性,引入了instanceof
,如果该表达式为true
,则可以安全转换
Vehicle v = new Car(); // 向上转型
// 向下转型前,先用 instanceof 判断,保证安全
if (v instanceof Car) {
Car car = (Car) v; // 安全
car.drive();
} else {
System.out.println("v不是Car类型,不能转换!");
}
if (v instanceof Plane) {
Plane plane = (Plane) v; // 这里不会执行
plane.fly();
} else {
System.out.println("v不是Plane类型,不能转换!");
}
5. 动态绑定机制
**动态绑定:**也称为后期绑定(晚绑定),是指在编译阶段无法确定方法的具体实现,只有在程序运行时,根据对象的实际类型,才能决定调用哪个类的方法。
在 Java 中,除了 static 方法和 final/private 方法外,所有的实例方法调用都采用动态绑定机制。这也是多态能够实现的根本原因。
public class Vehicle {
public void move() {
System.out.println("交通工具在移动");
}
}
public class Car extends Vehicle {
@Override
public void move() {
System.out.println("汽车在陆地上行驶");
}
}
public class Plane extends Vehicle {
@Override
public void move() {
System.out.println("飞机在空中飞行");
}
}
public class TestDynamicBinding {
// 形参是父类类型,编译时只知道是Vehicle
public static void showMove(Vehicle v) {
v.move(); // 运行时才确定调用哪个子类的move方法
}
public static void main(String[] args) {
Vehicle v1 = new Car();
Vehicle v2 = new Plane();
showMove(v1); // 输出:汽车在陆地上行驶
showMove(v2); // 输出:飞机在空中飞行
}
}
5.1 静态绑定:
静态绑定:也称为早期绑定,是指在编译阶段就能确定方法或变量的调用关系。
在 Java 中,以下情况属于静态绑定:
-
static 方法(类方法)
-
final 方法
-
private 方法
-
成员变量的访问
-
方法重载(overload)
这些成员在编译时就已经确定了调用目标,不会发生多态,也不会根据对象的实际类型发生变化。
class Parent {
public static void staticMethod() {
System.out.println("父类的静态方法");
}
public void instanceMethod() {
System.out.println("父类的实例方法");
}
}
class Child extends Parent {
public static void staticMethod() {
System.out.println("子类的静态方法");
}
@Override
public void instanceMethod() {
System.out.println("子类的实例方法");
}
}
public class TestStaticBinding {
public static void main(String[] args) {
Parent p = new Child();
p.staticMethod(); // 输出:父类的静态方法(静态绑定)
p.instanceMethod(); // 输出:子类的实例方法(动态绑定)
}
}
6. 多态的优缺点
6.1 优点
提高代码复用性、可扩展性、灵活性
// 父类
public class Vehicle {
public void move() {
System.out.println("交通工具在移动");
}
}
// 子类
public class Car extends Vehicle {
@Override
public void move() {
System.out.println("汽车在陆地上行驶");
}
}
public class Plane extends Vehicle {
@Override
public void move() {
System.out.println("飞机在空中飞行");
}
}
// 统一处理不同交通工具
public class Test {
public static void travel(Vehicle v) {
v.move();
}
public static void main(String[] args) {
Vehicle car = new Car();
Vehicle plane = new Plane();
travel(car); // 输出:汽车在陆地上行驶
travel(plane); // 输出:飞机在空中飞行
}
}
【使用多态的好处】
6.1.1 能够降低代码的“圈复杂度”,避免使用大量的 if-else。
什么是“圈复杂度”?圈复杂度是一种描述一段代码复杂程度的方式。如果代码结构平铺直叙,理解起来就比较简单;但如果有很多条件分支或循环语句,就会让代码变得复杂难懂。
我们可以简单粗暴地计算一段代码中条件语句和循环语句出现的个数,这个数就叫“圈复杂度”。如果一个方法的圈复杂度太高,就需要考虑重构。
不同公司对代码圈复杂度的规范不同,一般不会超过10。
例如:现在我们要让不同的交通工具执行 move 操作,如果不用多态,代码可能会这样⬇️:
public class Test {
public static void main(String[] args) {
String type = "car";
if ("car".equals(type)) {
System.out.println("汽车在陆地上行驶");
} else if ("plane".equals(type)) {
System.out.println("飞机在空中飞行");
} else {
System.out.println("未知交通工具");
}
如果交通工具类型越来越多,if-else 会越来越多,圈复杂度也会越来越高,代码难以维护。
使用多态后,则不必写这么多的 if - else 分支语句, 代码变得简单灵活⬇️:
public class Vehicle {
public void move() {
System.out.println("交通工具在移动");
}
}
public class Car extends Vehicle {
@Override
public void move() {
System.out.println("汽车在陆地上行驶");
}
}
public class Plane extends Vehicle {
@Override
public void move() {
System.out.println("飞机在空中飞行");
}
}
public class Test {
public static void main(String[] args) {
Vehicle[] vehicles = { new Car(), new Plane() };
for (Vehicle v : vehicles) {
v.move();
}
}
}
6.1.2 可扩展性更好
如果要新增一种新的交通工具类型, 使用多态的方式代码改动成本也比较低。
public class Train extends Vehicle {
@Override
public void move() {
System.out.println("火车在轨道上行驶");
}
}
- 对于类的调用者来说(Test方法), **只要创建一个新类的实例就可以了, 改动成本很低。
- 而对于不用多态的情况, 就要把 Test中的 if - else 进行一定的修改, 改动成本更高
6.2 缺点
多态缺陷:代码的运行效率降低。
-
属性没有多态性:当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
-
构造方法没有多态性
详细如下所示⬇️:
7. 避免在构造方法中调用重写的方法
这是一段有坑的代码,不知聪明的你能否发现?我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B {
public B() {
//啥也不干
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;//子类的实例
@Override
public void func(){
System.out.println("D.func()" + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
//输出结果:D.func() 0
在 Java 中,构造方法执行的顺序是:先执行父类构造方法,再执行子类构造方法。如果在父类构造方法中调用了一个会被子类重写的方法(如 func),那么在父类构造方法执行时,实际上会调用子类重写后的方法。此时,子类的实例变量还没有初始化,可能会导致意想不到的结果,甚至出现空指针异常或默认值。
上面代码的执行流程如下:
- 创建 D 类对象时,先调用 B 的构造方法。
- B 的构造方法中调用了 func(),但此时 func() 已经被 D 重写,所以实际调用的是 D 的 func()。
- 但是这时 D 的实例变量 num 还没有初始化,仍然是默认值 0。
- 因此输出结果是
D.func()0
。
结论:
千万不要在构造方法中调用**可以被子类重写的方法!**这样做会导致子类的成员变量还未初始化就被访问,容易引发 bug,增加代码的隐蔽性和维护难度。
用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成),
可能会出现一些隐藏的但是又极难发现的问题.
在构造函数内,尽量避免使用实例方法,除了final和private方法
8. 小结与扩展
8.1 知识点回顾
-
多态是面向对象三大特性之一,是实现代码复用、扩展和灵活性的关键机制。
-
多态的实现依赖于继承、方法重写和父类引用指向子类对象。
-
动态绑定机制让方法调用在运行时根据实际对象类型决定,体现了多态的本质。
-
向上转型让代码更灵活,向下转型要谨慎使用并配合 instanceof 判断。
-
多态有助于降低代码圈复杂度,提升可维护性,但也有不能访问子类特有成员等局限。
8.2 与其他面向对象特性的关系
-
继承:多态的基础,只有在继承体系下才能实现多态。
-
封装:多态配合封装,可以隐藏实现细节,只暴露统一接口。
-
抽象:抽象类和接口是多态实现的重要手段,能进一步提升代码的扩展性和解耦性。