Java —— 多态
目录
1. 多态的概念
2. 多态实现条件
3. 重写
重写与重载的区别
4. 向上转型和向下转型
4.1 向上转型
4.2 向下转型
5. 多态的优缺点
6. 避免在构造方法中调用重写的方法
我们从字面上看"多态"两个字, 多态就是有多种状态/形态. 比如一个人可以有多种状态, 开心, 生气, 郁闷...
那么在程序当中, 如何理解多态?我们就需要先明白以下几个概念:
1. 向上转型;
2. 重写;
才能理解多态.
1. 多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。
如下图, 不同的打印机打印出来的效果是不一样的;
再来看动物, 不同的动物吃的粮食是不一样的.
对象不一样, 而每一个对象有自己的行为, 那么行为就有可能不一样.
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。
2. 多态实现条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写
- 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
3. 重写
重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
【方法重写的规则】
- 子类在重写父类的方法时,一般必须与父类方法原型一致:
返回值类型 方法名 (参数列表)
要完全一致 - 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用
@Override
注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
我们来看下面的例子.
class Animal {public String name;public int age;public Animal(String name, int age) {this.name = name;this.age = age;}public void eat() {System.out.println(name + "吃饭!");}
}class Dog extends Animal {public boolean silly;public Dog(String name, int age, boolean silly) {super(name, age);this.silly = silly;}@Overridepublic void eat() {System.out.println(name + "正在吃狗粮!");}
}public class Test {public static void main(String[] args) {Dog dog = new Dog("hello", 10, false);dog.eat();}
}
重写与重载的区别
重载:
- 方法名称相同;
- 参数列表不同(数据类型, 顺序, 个数);
- 返回值不做要求.
重写(一定发生在继承层次上):
- 方法名称相同;
- 返回值相同(返回值构成父类子关系也可以);
- 参数列表相同(数据类型, 个数, 顺序).
区别点 | 重写(override) | 重载(override) |
---|---|---|
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改【除非可以构成父子类关系】 | 可以修改 |
访问限定符 | 一定不能做更严格的限制(可以降低限制) | 可以修改 |
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
public static void function() {}public static void function(int a) {}public static void function(int a, int b) {}public static void main(String[] args) {//编译的时候 根据你传入的参数 能够确定你调用哪个方法 这种就叫做 静态绑定function();function(1);function(1, 2);}
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
4. 向上转型和向下转型
4.1 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
Dog dog = new Dog("hello", 10, false);//animal这个引用 指向了Dog对象Animal animal = dog;
我们先把Dog类的eat()屏蔽掉, 然后执行以下的代码:
public class Test {public static void main(String[] args) {Dog dog = new Dog("hello", 10, false);Animal animal = dog;animal.eat();}
}
可以看到, 这里animal调用的eat是Animal本身的eat.
我们再在Animal中写入:
public void function() {System.out.println("Animal::function()!");}
此时也是可以在main中访问到function的:
animal.function();
此时问题是, Animal能否调用Dog中的属性?
我们会发现是不能的, 编译器直接报错了. 因为Animal这个类就没有silly这个属性.
但是, 我们把前面屏蔽掉的Dog中重写的eat放开, 再执行animal.eat();
打印的却是Dog的eat().
为什么会出现上面的结果?
此时其实这里就是发生了动态绑定, 我们由代码可以知道, 这个eat(), Dog和Animal都有, 当子类没有的时候, 使用Animal的eat, 当子类有的时候, 我们发现在这个过程当中, animal.eat();
调用的是子类的eat.
我们打开这个项目存在的路径:
找到这个运行程序的字节码文件, 来看一下它的反汇编代码:
在cmd窗口中输入javap -c Test
以查看它的反汇编(一般情况下我们很少看反汇编代码), 可以看到是在哪里调用eat的:
可以看到, 编译的时候, 编译器认为调用的是Animal的eat(). 但是程序运行的时候, 变成了子类的eat.
我们把这么一个过程叫做: 动态绑定
那么于是我们就可以把上面的代码写成:
Animal animal = new Dog("hello", 10, false); // 向上转型语法格式
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
上面这种是直接赋值的向上转型的场景, 那么我们还有其他场景会发生向上转型, 就是方法传参和方法返回.
我们先来看方法传参.
// 传进一个子类类型, 使用 Animal 接收public static void function2(Animal animal) {//..}public static void main(String[] args) {Dog dog = new Dog("hello", 10, false);function2(dog);}
接下来我们在上面的代码中进行补充, 即:
public static void function2(Animal animal) {animal.eat();}
然后写一个Cat类:
class Cat extends Animal {public Cat(String name, int age) {super(name, age);}public void catchMouse() {System.out.println(name + "抓老鼠!");}@Overridepublic void eat() {System.out.println(name + " 吃猫粮 !");}
}
在main中再补充:
Cat cat = new Cat("haha", 7);function2(cat);
那么现在站在function2()的角度下只有一个引用animal, 只去调用一个方法eat, 它去进行向上转型可以去引用Dog对象, 也可以引用Cat对象, 调用了两次function2, 当我们运行起来的时候(两个子类都重写了eat方法), 我们来看:
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
向上转型的另一种形态: 方法返回
public static Animal function3() {return new Cat();}
4.2 向下转型
向下转型极度的不安全, 我们尽量不要去用向下转型.
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
那么它不安全在哪里?
ClassCastException
是类型转换异常, 报错内容: Dog不能转化为Cat.
所以我们就需要判断一下:
public static void main(String[] args) {Animal animal2 = new Dog("hello", 10, false);// 判断: animal2 是不是引用了 Cat这个对象if (animal2 instanceof Cat) {Cat cat = (Cat) animal2;cat.catchMouse();}}
此时就不会报错了. 所以向下转型用的并不是很多.
instanceof
关键字官方介绍: Chapter 15. Expressions (oracle.com)
5. 多态的优缺点
假设有如下代码:
class Shape {public void draw() {System.out.println("画图形!");}
}class Rect extends Shape {@Overridepublic void draw() {System.out.println("画矩形!");}
}class Cycle extends Shape {@Overridepublic void draw() {System.out.println("画圆!");}
}class Triangle extends Shape {@Overridepublic void draw() {System.out.println("画一个三角形!");}
}class Flower extends Shape {@Overridepublic void draw() {System.out.println("❀!");}
}
public class Test {public static void drawMap(Shape shape) {shape.draw();}public static void main(String[] args) {Rect rect = new Rect();drawMap(rect);drawMap(new Cycle());drawMap(new Triangle());drawMap(new Flower());}
}
在drawMap()当中, Shape shape引用 引用的子类对象不一样, 调用方法表现出来的行为不一样. 我们把这种思想就叫做多态.
【使用多态的好处】
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度".如果一个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .
假设没有多态, 上面的代码写起来就会非常的麻烦.
public class Test {public static void drawShapes() {// 假设没有多态, 就要用 if-else 去做String[] strings = {"cycle", "rect", "cycle", "rect", "flower"}; // 先用字符数组存起来for (String x : strings) {// 通过大量的 if-else 去判断当前是什么, 就画什么.if (x.equals("cycle")) {Cycle cycle = new Cycle();cycle.draw();} else if (x.equals("rect")) {Rect rect = new Rect();rect.draw();} else {Flower flower = new Flower();flower.draw();}}}public static void main(String[] args) {drawShapes();}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.
public static void drawShapes() {Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()};for (Shape s : shapes) {s.draw();}}
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.
多态缺陷:代码的运行效率降低。
1. 属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性
6. 避免在构造方法中调用重写的方法
一段有坑的代码. 我们创建两个类, 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;@Overridepublic void func() {System.out.println("D.func() " + "num=" + num);}
}public class Test {public static void main(String[] args) {D d = new D();}
}
- 构造 D 对象的同时, 会调用 B 的构造方法.
- B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
- 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
相关文章:

Java —— 多态
目录 1. 多态的概念 2. 多态实现条件 3. 重写 重写与重载的区别 4. 向上转型和向下转型 4.1 向上转型 4.2 向下转型 5. 多态的优缺点 6. 避免在构造方法中调用重写的方法 我们从字面上看"多态"两个字, 多态就是有多种状态/形态. 比如一个人可以有多种状态, …...

UI自动化测试(弹出框,多窗口)
一、弹出框实战 1、在UI自动化测试中经常会遇到Alert弹出框的场景。Alert类是对话框的处理,主要是对alert警告框。confirm确认框,promp消息对话框。 text():获取alert的文本 dismiss ():点击取消 accept():接受 send-keys():输入 from selenium import …...

Python爬虫程序网络请求及内容解析
目录 引言 一、网络请求 1. 导入必要的库 2. 发送请求 3. 处理响应 二、内容解析 1. HTML解析 2. 查找特定元素 3. 查找多个元素 4. 使用选择器选择元素 三、应用示例:爬取网站文章并解析标题和内容 1. 发送请求并解析HTML内容 2. 查找文章元素并提取标…...

C嘎嘎模板
> 作者简介:დ旧言~,目前大二,现在学习Java,c,c,Python等 > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:了解什么是模板,并且能熟练运用函数模…...

数据结构和算法八股与手撕
数据结构和算法八股文 第一章 数据结构 1.1 常见结构 见http://t.csdnimg.cn/gmc3U 1.2 二叉树重点 1.2.1 各种树的定义 满二叉树:只有度为0的结点和度为2的结点,并且度为0的结点在同一层上 完全二叉树:除了最底层节点可能没填满外&…...

windiws docker 部署jar window部署docker 转载
Windows环境下从安装docker到部署前后端分离项目(springboot+vue) 一、前期准备 1.1所需工具: 1.2docker desktop 安装 二、部署springboot后端项目 2.1 部署流程 三、部署vue前端项目 3.1相关条件 3.2部署流程 四、前后端网络请求测试 一、前期准备 1.1所需工具: ①docke…...

使用git上传代码至gitee入门(1)
文章目录 一、gitee注册新建仓库 二、git的下载三、git的简单使用(push、pull)1、将本地文件推送至gitee初始化配置用户名及邮箱将本地文件提交至gitee补充 2、将远程仓库文件拉取至本地直接拉拉至其他本地文件夹 一、gitee 注册 官网:http…...

分类预测 | MATLAB实现基于Isomap降维算法与改进蜜獾算法IHBA的Adaboost-SVM集成多输入分类预测
分类预测 | MATLAB实现基于Isomap降维算法与改进蜜獾算法IHBA的Adaboost-SVM集成多输入分类预测 目录 分类预测 | MATLAB实现基于Isomap降维算法与改进蜜獾算法IHBA的Adaboost-SVM集成多输入分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 Isomap-Adaboost-IHBA-…...

如何解决3d max渲染效果图全白这类异常问题?
通过3d max渲染效果图时,经常会出现3Dmax渲染效果图全黑或是3Dmax渲染效果图全白这类异常问题。可能遇到这类问题较多的都是新手朋友。不知如何解决。 3dmax渲染出现异常的问题,该如何高效解决呢?今天小编这里整理几项知识点,大家…...

振南技术干货集:比萨斜塔要倒了,倾斜传感器快来!(2)
注解目录 1、倾斜传感器的那些基础干货 1.1 典型应用场景 (危楼、边坡、古建筑都是对倾斜敏感的。) 1.2 倾斜传感器的原理 1.2.1 滚珠式倾斜开关 1.2.2 加速度式倾斜传感器 1)直接输出倾角 2)加速度计算倾角 3)倾角精度的提高 (如果…...

图形学 -- Geometry几何
隐式 implicit 基于给点归类,满足某些关系的点 缺点:不规则表面难以描述! algebraic surface 直接用数学公式表示:不直观! Constructive Solid Geometry(CSG) 用简单形状进行加减 distance …...

opencv中边缘检测的方法
在OpenCV中,边缘检测的方法主要有以下几种: Sobel算子: Sobel算子是边检测器,它使用33内核来检测水平边和垂直边。Sobel算子有两个,一个是检测水平边缘的,另一个是检测垂直边缘的。在OpenCV中,…...

DigitalVirt 洛杉矶 CMIN2 VPS 测评
发布于 2023-07-16 在 https://chenhaotian.top/vps/digitalvirt-us-cmin2/ 官网链接(含AFF):https://digitalvirt.com/aff.php?aff459 美国西海岸 四网回程 CMIN2 移动新线路。 晚高峰延迟 165ms 左右,不丢包,非常…...

Qt DragDrop拖动与放置
本文章从属于 Qt实验室-CSDN博客系列 拖放操作包括两个动作:拖动(drag)和放下(drop或称为放置)。 拖动允许 对于要拖出的窗口或控件,要setDragEnabled(true) 对于要拖入的窗口或控件,要setAcceptDrops(true) 下面以一个具体的用例进行说…...

thinkphp8 多级控制器调用
在使用这个目录的时候正常访问时 http://tp.com/index.php/user2.login/index, 这个多级目录时不允许使用的,想要使用就的使用路由 在route/app.php 里面配置:Route::get(user2/login,user2.Login/index); 第一个参数时外部访问参数,第二个是…...

设计测试用例的6种基本原则
设计测试用例的基本原则,对于软件测试非常重要,这些原则有助于设计出高质量、全面、有效的测试用例,从而提高软件测试的效率和准确性,维护软件的质量和稳定。如果在设计用例时没有遵循基本原则,这会影响用例的全面性、…...

java的Exception.getMessage为null
之前捕获异常后调用异常的getMessage写日志,日志写的竟然是null,不可思议。发现要调用异常的getCause().getMessage()才能得到异常信息 刻意把密码改错,让异常直达界面,免得有问题时候只能猜...

EXTI (2)
增强版实验简介 EXTI5和EXTI9共享一个中断源 下面的类似 EXTI0到4各自拥有一个中断源 改变引脚 PA0和PA1改变为PA5 和PA6 EXTI的重映射 之前是把PA0映射到EXTI0 PA1映射到EXTI1上 现在是要把PA5和PA6分别映射到EXTI5和6上 EXTI进行初始化 NVIC初始化 编写中断函数 因为EXTI…...

Django实战项目-学习任务系统-任务完成率统计
接着上期代码内容,继续完善优化系统功能。 本次增加任务完成率统计功能,为更好的了解哪些任务完成率高,哪些任务完成率低。 该功能完成后,学习任务系统1.0版本就基本完成了。 1,编辑urls配置文件: ./mysi…...

安卓调用手机邮箱应用发送邮件
先来看看实现效果: 也不过多介绍了,直接上代码: private void openMail() {Uri uri Uri.parse("mailto:" "");List<ApplicationInfo> applicationInfoList getPackageManager().getInstalledApplications(Packa…...

Vue-Pinia
目录 Pinia状态管理库 使用步骤 1、安装Pinia 2、在vue应用实例中使用pinia 3、在src/stores/token.js中定义stores 4、在组件中使用store axios请求拦截器 代码实现 Pinia状态管理库 Pinia是Vue的专属状态管理库,它允许你跨组件或页面共享状态 一般在登录时…...

C语言,编写程序输出半径为1到15的圆的面积,若面积在30到100之间则予以输出,否则,不予输出
以下是一个使用C语言编写的程序,用于输出半径为1到15的圆的面积,并且如果面积在30到100之间,则输出该圆的半径和面积。 #include <stdio.h> #define PI 3.14159265358979323846int main() {int radius;double area;for (radius 1; ra…...

Ansys Electronics Desktop仿真——HFSS线圈寄生电阻,电感
利用ANSYS Electronics Desktop,可在综合全面、易于使用的设计平台中集成严格的电磁场分析和系统电路仿真。按需求解器技术让您能集成电磁场仿真器和电路及系统级仿真,以探索完整的系统性能。 HFSS(High Frequency Structure Simulator&#…...

对数据库密码使用MD5加密算法加密,并进行登录验证
实现步骤: 修改数据库中明文密码,改为MD5加密后的密文 打开employee表,修改密码 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对 打开EmployeeServiceImpl.java,修改比对密码 /*** 员工登录** param …...

关于Chrome中F12调试Console输入多行
在chrome 浏览器中使用console调试的时,如果想在console中输入多行代码,需要进行换行。 这时我们可以使用 [ Shift Enter ] 。也叫: 软回车。...

C# 集合用法介绍
在C#中,集合是一种特殊的数据类型,允许我们将多个元素组织在一起。这些元素可以是相同的类型或者可以是不同的类型。C#集合主要包括以下几种类型: List:它是一个有序的元素列表,用户可以添加、删除或查找元素。Dictio…...

linux三次握手、四次挥手
TCP协议是一个安全的、面向连接的、流式传输协议,所谓的面向连接就是三次握手,对于程序猿来说只需要在客户端调用connect()函数,三次握手就自动进行了。先通过下图看一下TCP协议的格式,然后再介绍三次握手的具体流程。 1.tcp协议…...

C# 泛型介绍
C# 中的泛型(Generics)是一种强类型参数化的特性,它允许你编写不具体指定数据类型的代码,而在实际使用时再指定具体的类型。泛型的引入使得代码更加灵活、可重用,并提高了类型安全性。 C#泛型基本用法 以下是一个简单…...

Windows如何正确设置PHP环境变量以在Git Bash中运行命令
1、随便找一个目录,鼠标右键打开git bash here 2、cd的根目录 3、找到php安装目录 4、 在根目录下打开 vim .bash_profile ,添加环境变量,php地址根据自己的本地地址而定 PATH$PATH:/d/phpstudy_pro/Extensions/php/php7.3.4nts 添加后保存…...

[代码实战和详解]VGG16
VGG16 详解 我的github代码实现:vgg16 我们在vgg16神经网络上训练了SIGNS数据集,这是一个分类的数据集,在我的github上有介绍怎么下载数据集以及如何训练。 VGG16是一个卷积神经网络(CNN)架构,它在2014年…...