继承和多态(2)(多态部分)

提前讲的重要知识点

一个类在没有父类的情况下默认有一个父类为Object类。

而当在有父类情况下,如果你那父类没有父类,则其父类的父类默认为object类,所以即使一个类有父类,其内部还是有object类。

object类都是隐藏起来的,你不会看到但是能用。

多态

前言

当父类引用所引用的子类对象不一样时。调用重写的方法,所表现出来的行为是不一样的,我们把这种思想叫做多态。 

其中上面所说的可能大家会觉得有点抽象,所以且听我慢慢道来,大家就懂了。

多态的基础是动态绑定,所以要了解多态前提我们还要了解动态绑定。

了解动态绑定的前提 

要想实现动态绑定,我们需要满足以上几个条件:

1.要发生向上转型

2.有发生重写(子类和父类有同名的方法)

3.使用父类对象的引用去调用重写方法

完成了这三部分,就会发生动态绑定。

从而用该父类对象的引用调用子类和父类都有的方法时调用的是子类方法,而不是正常来说的父类方法。

而在这里,出现了重写以及向上转型这些概念。所以我们得先了解它们才能再去了解动态绑定。

向上转型

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。 

语法格式:父类类型 对象名 = new 子类类型()

​
Animal animal = new Cat("元宝",2); ​//cat是子类,animal是父类

 虽然它们类型不相同,但由于它们是继承关系,子类类型可以隐式转换为父类类型(小范围能隐式转换为大范围),所以能实现该代码。 

向上转型有以上三种方式:

1. 直接赋值

2. 方法传参

3. 方法返回

public class TestAnimal {// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象public static void eatFood(Animal a){a.eat();}// 3. 作返回值:返回任意子类对象public static Animal buyAnimal(String var){if("狗".equals(var) ){return new Dog("狗狗",1);}else if("猫" .equals(var)){return new Cat("猫猫", 1);}else{return null;}}public static void main(String[] args) {Animal cat = new Cat("元宝",2);   // 1. 直接赋值:子类对象赋值给父类对象Dog dog = new Dog("小七", 1);eatFood(cat);eatFood(dog);Animal animal = buyAnimal("狗");animal.eat();animal = buyAnimal("猫");animal.eat();}
}

向上转型的优点:让代码实现更简单灵活。

向上转型的缺陷:不能调用到子类特有的方法 虽然我们创造的对象是子类对象,但引用是父类的引用,所以用发生向上转型的父类引用其不能调用子类的特有方法(如果是子类和父类都有名字相同的方法,此时用该引用调用相同的方法,调用的是子类的方法)

  

重写

重写(override):也称为覆盖 

重写是对(重写的父类方法是有限制的,等会来讲)父类方法的实现过程进行重新编写放在子类中。 

【方法重写的规则】

1.子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致

2.当然有个特殊点,被重写的方法返回值类型可以不同,但是必须是具有父子关系的,这里的父子关系指的是:在父类中返回值必须要为父类类型,在子类中方法必须为子类类型,相反则会报错。

3.访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected。 注意在重写的方法里不能出现private这个访问权限修饰符 

访问权限大小比较:private<default<protected<public

4.父类中被static或private或final修饰的方法以及构造方法都不能被重写。 

5.在子类中重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法 构成重写 

注意重写只能应用于成员方法,不能应用于成员变量。 

动态绑定和静态绑定

静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。

动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。

所以说正是因为动态绑定我们才能实现多态。

在实现重写和向上转型这两个前提后,用该父类对象的引用调用子类和父类都存在且同名的方法时就发生了动态绑定,使运行时期确认调用的是子类同名方法,而不是正常情况下应该调用的父类同名方法。

从而利用这个动态绑定作为基础去实现多态。

多态的实现

 多态具体点就是去完成某个行为时,当不同的对象去完成时会产生出不同的状态。代码如下:

 此时在上述代码中,不同对象用同一个方法时产生了不同的结果,这种就可以认为是多态。

 多态的优缺点

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 Flower extends Shape{@Overridepublic void draw() {System.out.println("❀");}
}

 【使用多态的好处】

1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else

什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .

例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:

public static void drawShapes() {Rect rect = new Rect();Cycle cycle = new Cycle();Flower flower = new Flower();String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};for (String shape : shapes) {if (shape.equals("cycle")) {cycle.draw();} else if (shape.equals("rect")) {rect.draw();} else if (shape.equals("flower")) {flower.draw();}}
}

 如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.

public static void drawShapes() {// 我们创建了一个 Shape 对象的数组. Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()};for (Shape shape : shapes) {shape.draw();}
}

 2. 可扩展能力更强 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

class Triangle extends Shape {@Overridepublic void draw() {System.out.println("△");}
}

 对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

多态缺陷:代码的运行效率降低。

1. 属性没有多态性 当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性(属性即成员变量)

2. 构造方法没有多态性  

向下转型

讲了向上转型之后,就有必要延伸讲一下向下转型了。

注意向下转型跟向上转型的来源不一样,向下转型来源并不是跟向上转型一样通过创建对象去形成的。

我们看看如果向下转型跟向上转型一样通过创建对象去形成,会产生什么现象。

 发生了类型转换异常。这可能是因为d引用在这时能访问的空间只有B类所创建的空间,d引用本身还能访问除了B类空间的其他空间,但我们只创建了B类空间,其他本身能访问的空间并没有创建出来,所以因为不能访问整个完整的D类空间从而发生错误。使用d时无论调用谁都会报错。

 

所以向下转型不是跟向上转型一样通过创建对象去发生的,它的基础是要在发生了向上转型后才能发生向下转型,如下:

将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法(注意特有,此时:将父类引用再还原为子类对象即可,即向下转换。

代码如下: 

public class TestAnimal {public static void main(String[] args) {Cat cat = new Cat("元宝",2);Dog dog = new Dog("小七", 1);// 向上转型Animal animal = cat;animal.eat();animal = dog;animal.eat();// animal.bark();// 编译失败,编译时编译器将animal当成Animal对象处理// 而Animal类中没有bark方法,因此编译失败
//所以我们需要将其向下转换,向下转换需要强制转换// animal本来指向的就是狗,因此将animal强制还原为狗也是安全的
//只要dog能访问到自己本身就能访问的全部空间,代码就算成功。   dog = (Dog)animal;dog.bark();}
}

而会出现以下代码,就是不安全的了 

public class TestAnimal {public static void main(String[] args) {Cat cat = new Cat("元宝",2);Dog dog = new Dog("小七", 1);// 向上转型Animal animal = cat;animal.eat();animal = dog;animal.eat();cat = (Cat)animal;cat.mew();
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
//发生了类型转换异常,这是因为cat引用不能访问自己本身就能访问的全部空间
//这个跟我最前面讲为什么向下转换不能通过创造对象去形成一样的道理
}}

 

 

向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof ,如果该表达式为true,则可以安全转换。

public class TestAnimal {public static void main(String[] args) {Cat cat = new Cat("元宝",2);Dog dog = new Dog("小七", 1);// 向上转型Animal animal = cat;animal.eat();animal = dog;animal.eat();if(animal instanceof Cat){cat = (Cat)animal;cat.mew();}if(animal instanceof Dog){dog = (Dog)animal;dog.bark();}}
}

instanceof操作符:

如果右边类的类型为左边引用所指向的对象的类型或者右边类的类型为其父类,则返回true。不是则返回false。

如上代码,animal指向dog对象,dog对象的类型为Dog,所以右边类型只有Dog或animal才能返回true,其他都是false。

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

一段有坑很好玩的代码

 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func。

看看打印结果 

class B {public B() {// do nothingfunc();}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);}
}public class Test {public static void main(String[] args) {D d = new D();}
}// 执行结果
D.func() 0

对于这结果你们肯定很有疑惑,且听我们慢慢道来: 

构造 D 对象的同时, 会调用 B 的构造方法. B 的构造方法中调用了 func 方法,

 

此时因为构造方法中隐含着this,而在父类构造方法中隐藏着  B this 这个引用变量,接收的引用却是D类型引用,所以发生了向上提升,而在父类与子类中又发生了重写,且还用到了func()这个重写方法,在func()前面隐藏着this.    ,此时this代表着B这个父类引用,所以导致触发动态绑定, 会调用到 D 中的 func  。

而我们还要知道一个很重要的一点,在构造方法完成后才会对成员变量进行初始化,在构造方法完成前成员变量的值都是基础值。

所以因为我们在构造方法还没完成时就用了成员变量,该成员变量还未进行初始化变为1,依然是保持基础值,所以打印时其num值为基础值0

 

这就是打印出该结果的原因。

结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题 。所以在构造函数内,尽量避免使用实例方法,我们用它去进行初始化成员变量就可以了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/769353.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【周总结】

周总结 完成项目混合版时区改造 完成相关jira问题的修改 完成老版本APP数据保存接口的兼容&#xff0c;手动赋值时区 2024/03/24 天气阴 一点不冷 1.Its time to go、Spring is coming&#xff01; 2. Its a nice day that staying with friends in a peaceful …

2024年云服务器ECS价格表出炉——腾讯云

腾讯云服务器多少钱一年&#xff1f;61元一年起。2024年最新腾讯云服务器优惠价格表&#xff0c;腾讯云轻量2核2G3M服务器61元一年、2核2G4M服务器99元一年可买三年、2核4G5M服务器165元一年、3年756元、轻量4核8M12M服务器646元15个月、4核16G10M配置32元1个月、312元一年、8核…

成功案例|全基因组测序+GWAS联合分析揭示不同种族帕金森病的遗传同质性和异质性

发表期刊&#xff1a;npj Parkinson’s Disease 影响因子&#xff1a;8.7 测序方式&#xff1a;WGS 研究对象&#xff1a;人 1 研究背景 帕金森病&#xff08;PD&#xff09;是一种常见的与年龄相关的神经退行性疾病&#xff0c;其特征是运动迟缓、姿势不稳定、僵硬和静息…

Redis中的过期键删除策略

过期键删除策略 概述 数据库键的过期时间都保存在过期字典中&#xff0c;并且知道根据过期时间去判断一个键是否过期,剩下的问题是&#xff1a;如果一个键过期了&#xff0c;那么它什么时候会被删除呢? 这个问题有三种可能的答案&#xff0c;它们分别代表了三种不同的删除策…

【linux】进程的地址空间

1.代码看现象引入 #include<stdio.h>#include<unistd.h>#include<string.h> #include<stdlib.h>int val100;int main (){ printf("i am father,pid:%d,ppid:%d,val:%d&#xff0c;&val:%p\n",getpid(),getppid(),val,&val);size_t…

vue2 和 vue3 配置路由有什么区别

vue2 和 vue3 配置路由有什么区别 初始化路由器实例&#xff1a;注入到应用中&#xff1a;动态路由参数和捕获所有路由&#xff1a;编程式导航 API&#xff1a;异步加载组件&#xff1a; vue2 如何 使用路由 第一步&#xff1a;安装 vue-router第二步&#xff1a;创建路由组件第…

【k8s】kubeasz 3.6.3 + virtualbox 搭建本地虚拟机openeuler 22.03 三节点集群 离线方案

kubeasz项目源码地址 GitHub - easzlab/kubeasz: 使用Ansible脚本安装K8S集群&#xff0c;介绍组件交互原理&#xff0c;方便直接&#xff0c;不受国内网络环境影响 拉取代码&#xff0c;并切换到最近发布的分支 git clone https://github.com/easzlab/kubeasz cd kubeasz gi…

<Linux> 模拟实现文件流 - 简易版

目录 1. FILE 结构设计 2、函数使用及分析 3、文件打开 fopen 4. 缓冲区刷新fflush 5. 数据写入fwrite 6. 文件关闭 fclose 7. 测试 8. 小结 1. FILE 结构设计 在设计 FILE 结构体前&#xff0c;首先要清楚 FILE 中有自己的缓冲区及冲刷方式 缓冲区的大小和刷新方式因…

JVM监控工具

JVM监控工具 文章目录 JVM监控工具jpsjmapjmap -histo 进程idjmap -heap 进程id (查看堆信息)jmap -dump:formatb,filefilename.hprof 进程id (将堆当前时刻快照信息dump到文件中) JSTACKjstack 查看死锁信息jstack找出占用cpu最高的线程堆栈信息 jinfo查看jvm参数查看java系统…

Perfetto Trace抓取

1. Perfetto简介 Perfetto 是一个用于 Android 系统的性能跟踪工具&#xff0c;可以帮助开发者分析系统性能和调试问题。 Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。 在低于Android R的版本上…

量子计算新“尺度”:用经典计算机评估复杂量子系统!

未来的量子计算机有望在计算机科学、医疗、商业、化学、物理学等多个领域解决难题&#xff0c;从而超越传统计算机。然而&#xff0c;目前的量子计算机仍存在局限&#xff0c;主要是由于它们固有的错误率。为此&#xff0c;研究者正致力于降低这些错误率。 一种研究量子计算机误…

Linux系统部署Paperless-Ngx文档管理系统结合内网穿透实现公网访问

文章目录 1. 部署Paperless-ngx2. 本地访问Paperless-ngx3. Linux安装Cpolar4. 配置公网地址5. 远程访问6. 固定Cpolar公网地址7. 固定地址访问 Paperless-ngx是一个开源的文档管理系统&#xff0c;可以将物理文档转换成可搜索的在线档案&#xff0c;从而减少纸张的使用。它内置…

机械硬盘与固态硬盘究竟该适合选用哪种,看完本文你就了解了!

随着科技的发展,计算机存储技术经历了从传统的机械硬盘(HDD)到现代固态硬盘(SSD)的革新变迁。在这篇文章中,我们将深入探讨机械硬盘与固态硬盘在功能特点上的显著区别,帮助用户更好地理解这两种存储设备的核心优势与不足。 一、存储原理与结构差异 1. 机械硬盘(HDD) …

Midjourney AI绘图工具介绍及使用

介绍 Midjourney是一款目前被誉为最强的AI绘图工具。只要输入想到的文字&#xff0c;就能通过人工智能产出相对应的图片。 官网只是宣传和登录入口&#xff0c;提供个人主页、订阅管理等功能&#xff0c;Midjourney实际的绘画功能&#xff0c;是在另外一个叫discord的产品中实…

网络电视盒子哪个品牌好?2024畅销电视盒子排行榜

电视盒子的品牌和产品非常多&#xff0c;让新手在选购时难度增大&#xff0c;大部分消费者在此时会选择参考销量排名情况&#xff0c;小编这次结合各个电商平台的销量和用户评价整理了电视盒子排行榜&#xff0c;想买电视盒子不知道网络电视盒子哪个品牌好可以收藏。 TOP 1.泰捷…

论文导读 | 漫谈图神经网络

本文主要介绍图神经网络相关内容&#xff0c;包括图神经网络的基本结构以及近期研究进展。 背景 在实际生活中&#xff0c;许多数据都可以用图的形式表达&#xff0c;比如社交网络、分子模型、知识图谱、计算机网络等。图深度学习旨在&#xff0c;显式利用这些数据中的拓扑结…

3.18_C++_day6_作业

作业要求&#xff1a; 程序代码&#xff1a; #include <iostream>using namespace std;class Animal { private:string name;string color;int *age; public://无参构造函数Animal(){cout << "Animal::无参构造函数" << endl;}//有参构造函数Anim…

[Linux初阶]which-find-grep-wc-管道符命令

目录 一.which 二.find a.-name b.-size 三.grep 四.wc 五.管道符(|) 五.总结 一.which 语法格式: which [命令] Linux中的一个个命令,本体上就是一个个的二进制可执行程序(相当于windows中的.exe文件). 在Linux中,一切皆文件. which命令:用于查看指定命令的可执行…

【JS进阶】第3天

JavaScript 进阶 - 第3天笔记 理论较多&#xff0c;主要讲解原型对象和对象原型 了解构造函数原型对象的语法特征&#xff0c;掌握 JavaScript 中面向对象编程的实现方式&#xff0c;基于面向对象编程思想实现 DOM 操作的封装。 了解面向对象编程的一般特征掌握基于构造函数原…

饼图渲染的关键

1) 创建一个DOM对象,有自定义的高和宽. 2) 引入Echarts软件包并导入到对应文件内 npm i Echarts import 文件.js script src.../文件 3) 初始化一个对象 4) 对象的方法实现饼图渲染 data内的数据,且当一个对象已经渲染一遍,再执行这个,会对setOption的参数进行更新,其…