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

news2024/11/14 16:42:21

提前讲的重要知识点

一个类在没有父类的情况下默认有一个父类为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{
    @Override
    public void draw() {
        System.out.println("♦");
   }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("●");
   }
}class Flower extends Shape{
    @Override
    public 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 {
    @Override
    public 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 nothing
        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

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

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

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

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

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

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

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

所以我们的继承与多态的多态部分就讲完了。这篇文章制作不易,6000字的内容了,还望大家点点关注,评论下,互三,你们的支持是我更新文章的最大动力!

 

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

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

相关文章

浅谈C++引用的使用以及底层原理

文章目录 1、引用概念2、引用特性3、常引用4、引用的使用场景&#xff08;1&#xff09;做参数&#xff08;2&#xff09;做返回值 1、引用概念 引用不是新定义一个变量&#xff0c;而是给已存在变量取了一个别名&#xff0c;编译器不会为引用变量开辟内存空间&#xff0c;它和…

链式二叉树经典OJ题目(一)

目录 结构体声明&#xff1a; 1.单值二叉树 题目描述&#xff1a; 思路分析&#xff1a; 源码&#xff1a; 2.二叉树最大深度 题目描述&#xff1a; 思路分析&#xff1a; 源码&#xff1a; 3.检查两棵树是否相同 题目描述&#xff1a; 思路分析&#xff1a; 源码…

EMC | 浪涌保护电路NTC

NTC(5D-11)负温度系数热敏电阻。 NTC是过流器件。 抑制开机的浪涌电流&#xff0c;NTC温度越高&#xff0c;电阻越低。 如果没有NTC,220VAC开机对电容CE3充电&#xff0c;此时电容中没有电荷&#xff0c;CE3相当于短路&#xff0c;回路电流会很大。 选型注意 1、R25温度下电阻…

Negative Sampling with Adaptive DenoisingMixup for Knowledge Graph Embedding

摘要 知识图嵌入(Knowledge graph embedding, KGE)的目的是通过对比正负三元组&#xff0c;将知识图中的实体和关系映射到一个低维、密集的向量空间中。在kge的训练过程中&#xff0c;由于kge只包含正三元组&#xff0c;因此负采样对于找到高质量的负三元组至关重要。大多数现…

鸿蒙NXET实战:高德地图定位SDK【获取Key+获取定位数据】(二)

如何申请key 1、创建新应用 进入[控制台]&#xff0c;创建一个新应用。如果您之前已经创建过应用&#xff0c;可直接跳过这个步骤。 2、添加新Key 在创建的应用上点击"添加新Key"按钮&#xff0c;在弹出的对话框中&#xff0c;依次&#xff1a;输入应用名名称&…

04、JS实现:用⼆分法思想实现搜索旋转排序数组(一步一步剖析,很详细)

搜索旋转排序数组 Ⅰ、搜索旋转排序数组&#xff1a;1、题目描述&#xff1a;2、解题思路&#xff1a;3、实现代码&#xff1a; Ⅱ、小结&#xff1a; Ⅰ、搜索旋转排序数组&#xff1a; 1、题目描述&#xff1a; 给你⼀个升序排列的整数数组 nums &#xff0c;和⼀个整数 tar…

【刷题】滑动窗口精通 — Leetcode 30. 串联所有单词的子串 | Leetcode 76. 最小覆盖子串

送给大家一句话&#xff1a; 充满着欢乐与斗争精神的人们&#xff0c;永远带着欢乐&#xff0c;欢迎雷霆与阳光。 —— 赫胥黎 滑动窗口精通 前言Leetcode 30. 串联所有单词的子串题目描述算法思路 Leetcode 76. 最小覆盖子串题目描述算法思路 Thanks♪(&#xff65;ω&#xf…

产品推荐 | 基于 Zynq UltraScale+ XCZU27DR的 FACE-RFSoC-C高性能自适应射频开发平台

一、产品概述 FACE-RFSOC-C自适应射频开发平台&#xff0c;是FACE系列新一代的产品。 平台搭载有16nm工艺的Zynq UltraScale™ RFSoC系列主器件。该器件集成数千兆采样RF数据转换器和ARM Cortex-A53处理子系统和UltraScale可编程逻辑&#xff0c;是一款单芯片自适应射频平台。…

风力发电模型Windpowerlib概述与入门

Windpowerlib 是一个提供了一系列函数和类的库&#xff0c;用于计算风力涡轮机的功率输出。它最初是 feedinlib&#xff08;风能和光伏&#xff09;的一部分&#xff0c;但后来被分离出来&#xff0c;以建立一个专注于风能模型的社区。 这个库的主要目的是简化风力涡轮机的能量…

Vue3 + Vite + TS + Element-Plus + Pinia项目(3)--新建路由

1、在src文件夹下新建router文件夹后&#xff0c;创建index.ts文件 2、具体如下 import { createRouter, createWebHashHistory } from vue-routerconst router createRouter({history: createWebHashHistory(),routes: [{path: "/index",component: () > impor…

红黑树进阶:正向与反向迭代器的实现及map、set的封装实践

文章目录 一、引言二、红黑树迭代器设计1、迭代器的基本概念和分类2、正向迭代器设计a.迭代器结构定义b.迭代器的 与 -- 3、反向迭代器设计a.反向迭代器的必要性b.反向迭代器的实现要点 4、红黑树封装迭代器 三、使用红黑树实现Map四、红黑树实现Set五、细节理解1、 typname的使…

JAVA学习笔记19(面向对象编程)

1.面向对象编程 1.1 类与对象 1.类与对象的概念 ​ *对象[属性]/[行为] ​ *语法 class cat {String name;int age; }main() {//cat1就是一个对象//创建一只猫Cat cat1 new Cat();//给猫的属性赋值cat1.name "123";cat1.age 10; }​ *类是抽象的&#xff0c;…

【Redis系列】那有序集合为什么要同时使用字典和跳跃表

面试官&#xff1a;听说你精通Redis&#xff0c;那我就考考你吧 面试官&#xff1a;不用慌尽管说&#xff0c;错了也没关系&#x1f60a;。。。 以【面试官面试】的形式来分享技术&#xff0c;本期是《Redis系列》&#xff0c;感兴趣就关注我吧❤️ 面试官&#xff1a;你说说Re…

个人可以做知识付费网站吗

个人可以做知识付费网站吗 个人能够做学问付费网站吗&#xff1f;答案是肯定的&#xff01;如今个人做学问付费网站并不需求太多的资金和技术支持&#xff0c;我们只需求购置一台效劳器或虚拟主机&#xff0c;然后在该主机空间上搭建一个WordPress网站&#xff0c;最后运用带有…

商家如何自己零成本免费制作点餐小程序项目完整源码

现在点餐小程序成为餐饮店的标配&#xff0c;顾客只要扫码&#xff0c;即可进入小程序点餐。顾客付款后&#xff0c;后厨自动打印出订单并开始制作。整个过程非常方便流畅&#xff0c;甚至还可以免去收银&#xff08;或服务&#xff09;人员。那么&#xff0c;这种餐饮小程序要…

类和对象三部曲(one)

都说C语言是面向过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用来逐步解决问题。 拿洗衣服来举例&#xff0c;C关注的是一个过程&#xff1a; 那么C是什么呢&#xff1f; 面向对象的编程语言。 面向对象对象指什么&#xff1f; 象棋里的对象么&#xff1f;…

JavaScript学习第二天

1.学习JavaScript高级语法目的 降低后续对于前端框架学习难度 1.局部变量与全局变量 1.局部变量: 在函数体内部通过var声明的变量 1.局部变量特点: 局部变量只能在当前函数体内使用&#xff0c;不能 在函数体外使用 2.全局变量 在script标签下直接…

AI之Suno:Suno V3的简介、安装和使用方法、案例应用之详细攻略

AI之Suno&#xff1a;Suno V3的简介、安装和使用方法、案例应用之详细攻略 目录 Suno AI的简介 1、特点与改进&#xff1a; Suno AI的安装和使用方法 1、第一步&#xff0c;让国产大模型—ChatGLM4帮我写一个提示词 2、第二步&#xff0c;将提示词交给Suno v3&#xff0c;…

LDL^H分解求逆矩阵与MATLAB仿真(Right-Looking)

通过分解将对称正定厄米特矩阵分解成下三角矩阵L和对角矩阵D来求其逆矩阵 目录 前言 一、LDL^H基本算法 二、LDL^H Right-Looking算法 三、D矩阵求逆 四、L矩阵求逆 五、A矩阵求逆 六、计算量分析 七、MATLAB仿真 八、参考资料 总结 前言 在线性代数中&#xff0c;LDL…

如何在Ubuntu系统使用Docker搭建MongoDB结合内网穿透实现公网连接

文章目录 前言1. 安装Docker2. 使用Docker拉取MongoDB镜像3. 创建并启动MongoDB容器4. 本地连接测试5. 公网远程访问本地MongoDB容器5.1 内网穿透工具安装5.2 创建远程连接公网地址5.3 使用固定TCP地址远程访问 前言 本文主要介绍如何在Linux Ubuntu系统使用Docker快速部署Mon…