Java —— 多态

news2024/11/26 4:27:22

目录

1. 多态的概念

2. 多态实现条件

3. 重写

重写与重载的区别

4. 向上转型和向下转型

4.1 向上转型

4.2 向下转型

5. 多态的优缺点

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


我们从字面上看"多态"两个字, 多态就是有多种状态/形态. 比如一个人可以有多种状态, 开心, 生气, 郁闷...
那么在程序当中, 如何理解多态?我们就需要先明白以下几个概念:
1. 向上转型;
2. 重写;
才能理解多态.

1. 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。


如下图, 不同的打印机打印出来的效果是不一样的;

再来看动物, 不同的动物吃的粮食是不一样的.


对象不一样, 而每一个对象有自己的行为, 那么行为就有可能不一样.

总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。

2. 多态实现条件

在java中要实现多态,必须要满足如下几个条件,缺一不可:

  1. 必须在继承体系下
  2. 子类必须要对父类中方法进行重写
  3. 通过父类的引用调用重写的方法

多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。


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;
    }

    @Override
    public void eat() {
        System.out.println(name + "正在吃狗粮!");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("hello", 10, false);
        dog.eat();
    }
}

重写与重载的区别

重载:

  1. 方法名称相同;
  2. 参数列表不同(数据类型, 顺序, 个数);
  3. 返回值不做要求.

重写(一定发生在继承层次上):

  1. 方法名称相同;
  2. 返回值相同(返回值构成父类子关系也可以);
  3. 参数列表相同(数据类型, 个数, 顺序).

区别点

重写(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 + "抓老鼠!");
    }

    @Override
    public 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 {
    @Override
    public void draw() {
        System.out.println("画矩形!");
    }
}

class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("画圆!");
    }
}

class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个三角形!");
    }
}

class Flower extends Shape {
    @Override
    public 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;

    @Override
    public 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方法。

结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

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

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

相关文章

物联网主机E6000:动环监控的新革命

多协议、多接口的全能主机 在物联网时代,数据的采集和处理已经成为了企业运营的重要环节。而物联网主机E6000,就是这个时代的全能选手。它支持多种协议和接口,无论是视频、设备还是DCS系统的数据,都能轻松接入并进行采集处理。这种…

深度学习+python+opencv实现动物识别 - 图像识别 计算机竞赛

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数:3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 inception_v3网络5 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 *…

Android 10.0 framework层设置后台运行app进程最大数功能实现

1. 前言 在10.0的定制开发中,在系统中,对于后台运行的app过多的时候,会比较耗内存,导致系统运行有可能会卡顿,所以在系统优化的 过程中,会限制后台app进程运行的数量,来保证系统流畅不影响体验,所以需要分析下系统中关于限制app进程的相关源码来实现 功能 2.framewo…

一款快速从数据库中提取信息工具

DataMiner 介绍 DataMiner是一款数据库自动抽取工具,用于快速从数据库中提取信息,目前支持 mysql、mssql、oracle、mongodb等数据库,可导出CSV、HTML。 功能 支持对所有数据库数据进行采样,并指定采样数量。 支持对指定数据库…

Fabric多机部署启动节点与合约部署

这是我搭建的fabric的网络拓扑 3 个 orderer 节点;组织 org1 , org1 下有两个 peer 节点, peer0 和 peer1; 组织 org2 , org2 下有两个 peer 节点, peer0 和 peer1; 以上是我的多机环境的网络拓扑,使用的是docker搭建的。我的网络…

计算机毕业设计选题推荐-二手交易跳蚤市场微信小程序/安卓APP-项目实战

✨作者主页:IT毕设梦工厂✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

系列一、JVM的架构图

一、JVM的位置 JVM是运行在操作系统之上的,它与硬件没有直接的交互。 二、JVM的架构图

Ps:利用 AI 技术创建人像皮肤图层蒙版

Photoshop 并没有提供专门选择人像皮肤的工具或命令(色彩范围中的肤色选择非常不精准),但较新版的 Camera Raw 滤镜则提供了基于 AI 技术的选择人物并创建面部和身体皮肤蒙版的功能。 如果能将 Camera Raw 滤镜中创建的 AI 皮肤蒙版转换成 Ps…

在docker下安装suiteCRM

安装方法: docker-hub来源:https://hub.docker.com/r/bitnami/suitecrm curl -sSL https://raw.githubusercontent.com/bitnami/containers/main/bitnami/suitecrm/docker-compose.yml > docker-compose.yml//然后可以在docker-compose.yml文件里修…

day27_JS

今日内容 一、JS 一、引言 1.1 JavaScript简介 JavaScript一种解释性脚本语言,是一种动态类型、弱类型、基于原型继承的语言,内置支持类型。它的解释器被称为JavaScript引擎,作为浏览器的一部分,广泛用于客户端的脚本语言&#xf…

ChatGLM3-6B:新一代开源双语对话语言模型,流畅对话与低部署门槛再升级

项目设计集合(人工智能方向):助力新人快速实战掌握技能、自主完成项目设计升级,提升自身的硬实力(不仅限NLP、知识图谱、计算机视觉等领域):汇总有意义的项目设计集合,助力新人快速实…

S-Clustr(影子集群) 重磅更新!黑入工业PLC设备!

公告 项目地址:https://github.com/MartinxMax/S-Clustr 更新预告内容进度SIEMENS S7-200 SMART远程控制进行中 开发人员Blog联系方式提交时间提交内容授权情况ASH_HHhttps://blog.csdn.net/m0_53711047/article/details/133691537?spm1001.2014.3001.5502匿名2023-10-16 2…

Games104现代游戏引擎笔记 面向数据编程与任务系统

Basics of Parallel Programming 并行编程的基础 核达到了上限,无法越做越快,只能通过更多的核来解决问题 Process 进程 有独立的存储单元,系统去管理,需要通过特殊机制去交换信息 Thread 线程 在进程之内,共享了内存…

Python数据容器之(元组)

我们前面所了解的列表是可以修改的,但如果想要传递的信息,不被篡改,列表就不合适了。 元组同列表一样,都是可以封装多个、不同类型的元素在内。 但最大的不同点在于: 元组一旦定义完成,就不可修改 所以…

Windows 11 设置 wsl-ubuntu 使用桥接网络

Windows 11 设置 wsl-ubuntu 使用桥接网络 0. 背景1. Windows 11 下启用 Hyper-V2. 使用 Hyper-V 虚拟交换机管理器创建虚拟网络3. 创建 .wslconfig 文件4. 配置 wsl.conf 文件5. 配置 wsl-network.conf 文件6. 创建 00-wsl2.yaml7. 安装 net-tools 和 openssh-server 0. 背景 …

SSD(Single Shot MultiBox Detector)的复现

SSD 背景 这是一种 single stage 的检测模型,相比于R-CNN系列模型上要简单许多。其精度可以与Faster R-CNN相匹敌,而速度达到了惊人的59FPS,速度上完爆 Fster R-CNN。 速度快的根本原因在于移除了 region proposals 步骤以及后续的像素采样或…

LeetCode(15)分发糖果【数组/字符串】【困难】

目录 1.题目2.答案3.提交结果截图 链接: 135. 分发糖果 1.题目 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果。相邻两个孩子评分更高的孩子会获…

【微服务专题】Spring启动过程源码解析

目录 前言阅读对象阅读导航前置知识笔记正文一、SpringBoot启动过程源码解析1.1 SpringBoot启动过程源码流程图1.2 流程解析补充1.2.1 SpringApplicationRunListeners:SpringBoot运行过程监听器 学习总结感谢 前言 这部分只是个人的自结,方便后面回来看…

RK3588平台开发系列讲解(摄像头篇)USB摄像头驱动分析

🚀返回专栏总目录 文章目录 一. USB摄像头基本知识1.1 内部逻辑结构1.2 描述符实例解析二. UVC驱动框架2.1、设备枚举过程2.2、数据传输过程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 USB摄像头驱动位于 drivers\media\usb\uvc\uvc_driver.c ,我们本篇重点看下…

正版软件|Soundop 专业音频编辑器,实现无缝的音频制作工作流程

关于Soundop Soundop 音频编辑器 直观而专业的音频编辑软件,用于录制、编辑、混合和掌握音频内容。 Soundop 是一款适用于 Windows 的专业音频编辑器,可在具有高级功能的直观灵活的工作区中录制、编辑和掌握音频并混音轨道。音频文件编辑器支持波形和频谱…