当前位置: 首页 > news >正文

JAVA SE(9)——多态

1.多态的概念&作用

多态(Polymorphism)是面向对象编程的三大基本特性之一(封装和继承已经讲过了),它允许不同类的对象对同一消息做出不同的响应。具体来说,多态允许基类/父类的引用指向派生类/子类的对象(向上转型),并通过该引用调用子类中重写的方法,从而实现不同的行为

2.实现多态的条件

在Java中,要实现多态必须满足以下条件,缺一不可:

  • 1.在继承体系下
  • 2.父类引用指向子类对象(向上转型)
  • 3.子类必须对父类中的可重写方法进行重写
  • 4.通过父类引用调用子类中重写的方法

在实现多态之前,我们必须先搞清楚向上转型和重写是什么

3.向上转型

3.1 概念

向上转型是面向对象编程中的一种类型转换机制,它允许将子类的对象引用转换为父类的引用。这种类型转换是安全的,隐式的

3.2 语法格式

父类类型 变量名 = new 子类对象

public class Animal {public int age;public String name;
}
public class Dog extends Animal {public String color;
}
public class Test {public static void main(String[] args) {Animal dog = new Dog();}
}

在上述代码中,Dog类继承自Animal类,在实例化Dog对象的时候,创建了一个父类类型的引用(变量)来指向该对象,这就是向上转型,下面我画个图来表示父类引用和子类对象在内存中的关系图:
在这里插入图片描述
实现向上转型有三种方式:
(1)直接赋值:

Animal dog = new Dog();

(2)方法传参:

public class Test {public static void func(Animal a) {a.eat();}public static void main(String[] args) {Dog dog = new Dog();func(dog);}
}

(3)方法返回值:

public class Test {public static Animal func(String value) {if (value.equals("cat")) {return new Cat();}else if (value.equals("dog")) {return new Dog();}else {return null;}}public static void main(String[] args) {Animal animal = func("cat");}
}

注意:当发生向上转型后,父类引用无法访问子类中原有的属性和方法,只能访问从父类中继承而来的属性和方法,也就是说父类引用的访问范围如下:
在这里插入图片描述
那么,在已经发生向上转型后,如何才能访问子类原有的属性和方法呢?
有两个办法:

  • 1.向下转型
  • 2.使用多态性

4.向下转型

4.1 如何使用向下转型

通过显式地将父类引用转换为子类引用,可以访问子类原有的属性和访问

public class Animal {public int age;public String name;
}
public class Dog extends Animal {public String color;
}
public class Test {public static void main(String[] args) {//向上转型Animal dog = new Dog();System.out.println(dog.age = 10);System.out.println(dog.name = "Dog");//向下转型Dog dog1 = (Dog) dog;System.out.println(dog1.color = "白色");}
}

允许结果:
10
Dog
白色
在这里插入图片描述

4.2 向下转型存在的风险

注意:
向下转型在**编译**时是允许的,但如果没有正确地检查对象的实际类型,运行时可能会抛出ClassCastException异常。这是因为父类引用可能实际上引用的是父类本身或其他子类的对象,而不是目标子类的对象

public class Animal {public int age;public String name;
}
//Dog类继承Animal
public class Dog extends Animal {public String color;
}
//Cat类继承Animal
public class Cat extends Animal {public int weight;
}
public class Test {public static void main(String[] args) {//向上转型Animal dog = new Dog();System.out.println(dog.age = 10);System.out.println(dog.name = "Dog");//向下转型Dog dog1 = (Dog) dog;System.out.println(dog1.color = "白色");//错误的向下转型Cat cat = (Cat) dog;cat.weight = 10;}
}

上述代码中,Dog和Cat都继承自Animal,但是在发生向下转型的时候,使用Cat类型的引用指向Dog对象。这在编译时是不会报错的,因为Cat和Dog同属于Animal的子类,但是当程序运行之后就会抛出ClassCastException异常

因为向下转型本质上是强制类型转换,将Animal dog引用(指向Dog对象的引用)强转为Dog类型是允许的,因为Dog类型的引用指向Dog对象很合理;但是,如果将Animal dog引用(指向Dog对象的引用)强转为Cat类型,这是不允许的,因为Cat类型的引用无法指向Dog对象。这就相当于无法将boolean类型强转为int类型

4.3 instanceof运算符

如果用户错误地使用向下转型,在编译阶段是不容易被察觉出来的,只有运行阶段才会报错。如果程序运行起来之后才发现错误,可能已经带来了损失,所以为了规避这一情况,Java引入了instanceof运算符来帮助用户检测错误

public class Animal {public int age;public String name;
}
//Dog类继承Animal
public class Dog extends Animal {public String color;
}
//Cat类继承Animal
public class Cat extends Animal {public int weight;
}
public class Test {public static void main(String[] args) {//向上转型Animal dog = new Dog();System.out.println(dog.age = 10);System.out.println(dog.name = "Dog");//向下转型if (dog instanceof Dog) {//dog instanceof Dog 为true//说明该向下转型是安全的Dog dog1 = (Dog) dog;}if (dog instanceof Cat) {//dog instanceof Cat 为false//说明该向下转型是不安全的,不执行if语句中的代码Cat cat = (Cat) dog;}}
}

5.重写

5.1 概念

**重写(Override)**是面向对象编程中的一个重要概念,它允许子类提供一个与父类中已定义的方法具有相同名称、参数列表和返回类型的方法。重写使得子类能够改变或扩展父类方法的实现

5.2 语法格式

public class Animal {public int age;public String name;//父类的eat方法public Animal eat() {System.out.println("Animal is eating.");return null;}
}
public class Dog extends Animal {public String color;//重写父类的eat方法@Overridepublic Dog eat() {System.out.println("Dog is eating.");return null;}
}

重写的规则:

  • 1.子类重写父类的方法时,必须和父类的方法名、参数列表保持一致
  • 2.返回值类型可以不一样,但必须具有父子关系。上述代码中,子类重写的方法的返回值类型可以是被重写方法返回值类型的子类
  • 3.子类中重写方法的访问权限必须大于等于父类中被重写的方法
  • 4.父类中被static、final、private修饰的方法以及构造方法不能被重写
  • 5.重写方法是可以借助@Override注解。虽然这个注解不影响方法的实现逻辑,但是可以帮助我们进行合法性检查。比如:如果不小心将方法名写错了(写成了ate),注解就会帮我们报错
  • 6.重写只针对方法,和属性/变量无关

5.3 重写和重载的区别

特性重载(Overload)重写(Override)
定义在同一个类中,方法名相同但参数列表不同在子类中重新定义一个与父类中已定义的方法具有相同签名、返回值一样或这具有父子关系的方法
目的提供方法的不同实现,以适应不同的参数类型和数量改变或扩展父类方法的实现
访问限定修饰符无要求子类方法的访问权限必须大于等于父类方法
调用时机编译时根据参数列表来确定调用哪个方法运行时确定
关键字无关键字可以借助@Override注解

5.4 动态绑定

上面讲向上转型时讲过,当父类引用指向子类对象时,该父类引用只能访问子类中继承自父类的属性和方法。那么,当向上转型和方法重写同时发生时,会碰撞出怎么样的火花呢?

public class Animal {public int age;public String name;//父类的eat方法public Animal eat() {System.out.println("Animal is eating.");return null;}
}
public class Dog extends Animal {public String color;//重写父类的eat方法@Overridepublic Dog eat() {System.out.println("Dog is eating.");return null;}
}
public class Test {public static void main(String[] args) {Animal dog = new Dog();dog.eat();}
}

在上述代码中,引用指向子类的对象,再通过该父类引用去调用父类中被重写的eat方法,按照我们之前学习的知识,此时运行结果应该是:Animal is eating.,但实际上:

运行结果:
Dog is eating.

这里可以得出一个结论:当父类引用去调用父类中被重写的方法时,真正被调用的方法是子类中重写的方法。
注意:

  • 1.具体最终调用哪个方法,在编译时无法确定,在运行时根据对象的实际类型来确定
    在这里插入图片描述
    在编译时认为调用Animal中的eat方法,但是根据运行结果来看,实际上调用的是子类中重写的eat方法
  • 2.在运行时确定调用的具体方法,这称之为动态绑定/后期绑定,也是实现多态的基础

6. 多态

6.1 多态的具体实现

public class Animal {public int age;public String name;//父类的eat方法public Animal eat() {System.out.println("Animal is eating.");return null;}
}
public class Dog extends Animal {public String color;//重写父类的eat方法@Overridepublic Dog eat() {System.out.println("Dog is eating.");return null;}
}
public class Cat extends Animal {public int weight;//重写父类的eat方法@Overridepublic Cat eat() {System.out.println("Cat is eating.");return null;}
}
public class Test {public static void func(Animal animal) {animal.eat();}public static void main(String[] args) {Dog dog = new Dog();Cat cat = new Cat();func(dog);func(cat);}
}

运行结果:
Dog is eating.
Cat is eating.

这里我们再回顾一下多态的概念:多态允许基类/父类的引用指向派生类/子类的对象(向上转型),并通过该引用调用子类中重写的方法,从而实现不同的行为。

在上述func方法中,同样是通过animal形参来调用eat方法,两个运行的结果发生了变化,这个过程就叫做多态

6.2 使用多态降低圈复杂度

什么叫圈复杂度?
圈复杂度是一种描述一段代码复杂程度的方式。如果一段代码平铺直叙,那么就比较容易理解;如果一段代码使用很多的条件分支或者循环语句,就认为代码理解起来比较复杂。

我们可以简单地举个例子,一层if-else语句表示一圈复杂度,如果一段代码的圈复杂度太高,就需要考虑重新构建该代码的结果。一般来说,圈复杂度不应该超过10。
现在我们需要打印多个图形

public class Shape {public void draw() {System.out.println("Shape");}
}
public class Flower extends Shape {@Overridepublic void draw() {System.out.println("flower");}
}
public class Rect extends Shape{@Overridepublic void draw() {System.out.println("rect");}
}
public class Circle extends Shape {@Overridepublic void draw() {System.out.println("circle");}
}

下面是不使用多态的代码:

public class Test {public static void main(String[] args) {String[] array = new String[]{"flower","rect","circle"};//for(String cur : array) {if(cur.equals("flower")) {new Flower().draw();}else if(cur.equals("rect")) {new Rect().draw();}else {new Circle().draw();}}}
}

下面是使用多态的代码:

public class Test {public static void main(String[] args) {Shape[] array = new Shape[]{new Flower(), new Rect(), new Circle()};for(Shape cur : array) {cur.draw();}}
}

6.3 避免在构造方法中调用重写的方法

在这里插入图片描述

实际执行结果:
Son0
期望执行结果:
Son10

上述代码中,在父类的构造方法中调用func方法,此时调用的是子类中重写的func方法。在子类的func方法中访问了实例成员变量age,但是此时age还没有开始初始化。因为此时仍然处于父类的构造方法中,而子类的实例成员变量初始化发生在父类构造方法结束之后。解决办法有两个:

  • 1.将age变量使用static修饰(不建议)
  • 2.避免在构造方法中调用重写的方法(建议)
http://www.xdnf.cn/news/298765.html

相关文章:

  • Axure疑难杂症:深度理解与认识“事件”“动作”(玩转交互)
  • 数据中台产品功能介绍
  • Rice Science∣武汉大学水稻研究团队发现水稻壁相关激酶OsWAKg16和OsWAKg52同时调控水稻抗病性和产量
  • CSS中的@import指令
  • 深入解析二维矩阵搜索:LeetCode 74与240题的两种高效解法对比
  • 【C++游戏引擎开发】第31篇:物理引擎(Bullet)—碰撞检测系统
  • 质量员考试案例题有哪些常见考点?
  • K8S PV 与 PVC 快速开始、入门实战
  • C++负载均衡远程调用学习之集成测试与自动启动脚本
  • Spark,所用几个网页地址
  • PaddlePaddle 和PyTorch选择与对比互斥
  • NSSM 完全指南:如何将任意程序部署为 Windows 服务
  • OpenHarmony GPIO应用开发-LED
  • 搭建一个简单的博客界面(前端HTML+CSS+JS)
  • 《AI大模型应知应会100篇》第50篇:大模型应用的持续集成与部署(CI/CD)实践
  • 互联网大厂Java求职面试:AI与云原生下的系统设计挑战-3
  • K8S有状态服务部署(MySQL、Redis、ES、RabbitMQ、Nacos、ZipKin、Sentinel)
  • 【JsonCpp、Muduo、C++11】JsonCpp库、Muduo库、C++11异步操作
  • Jenkins 改完端口号启动不起来了
  • IoTDB磁盘I/O性能监控与优化指南
  • Caffeine快速入门
  • Oracle02-安装
  • JavaScript 对象引用与值传递的奥秘
  • Acrel-EIoT 能源物联网云平台在能耗监测系统中的创新设计
  • 启发式算法-模拟退火算法
  • STM32的智慧农业系统开发(uC/OS-II)
  • 如何设计Kafka的高可用跨机房容灾方案?(需要实战,未实战,纯理论)
  • 破局者手册 Ⅱ:测试开发深度攻坚,引爆质量优化新动能!
  • ES6/ES11知识点 续四
  • 【自然语言处理与大模型】LlamaIndex的词嵌入模型和向量数据库