Java方法调用动态绑定(多态性)详解
CONTENTS
- 1. 方法调用绑定
- 2. 尝试重写Private方法
- 3. 字段访问与静态方法的多态
- 4. 构造器内部的多态方法行为
1. 方法调用绑定
我们首先来看下面这个例子:
package com.yyj;enum Tone {LOW, MIDDLE, HIGH;
}class Instrument {public void play(Tone t) {System.out.println("Instrument.play() " + t);}
}class Piano extends Instrument {@Overridepublic void play(Tone t) {System.out.println("Piano.play() " + t);}
}class Guitar extends Instrument {@Overridepublic void play(Tone t) {System.out.println("Guitar.play() " + t);}
}public class Music {public static void tune(Instrument i, Tone t) {i.play(t);}public static void main(String[] args) {Piano p = new Piano();Guitar g = new Guitar();tune(p, Tone.MIDDLE); // 向上转型,输出:Piano.play() MIDDLEtune(g, Tone.HIGH); // Guitar.play() HIGH}
}
在 main() 方法中,我们将 Piano 引用传递给了 tune(),且不需要任何强制类型转换。这是因为 Instrument 中的接口必定存在于 Piano 中,因为 Piano 继承了 Instrument。从 Piano 向上转型到 Instrument 可以“缩小”该接口,但不会小于 Instrument 的完整接口。
那么编译器怎么可能知道这个 Instrument 引用在这里指的是 Piano,而不是 Guitar?为了更深入地了解这个问题,有必要研究一下绑定(binding)这个问题。
将一个方法调用和一个方法体关联起来的动作称为绑定。在程序运行之前执行绑定(如果存在编译器和链接器的话,由它们来实现),称为前期绑定。你之前可能没有听说过这个术语,因为在面向过程语言中默认就是前期绑定的。例如,在 C 语言中只有一种方法调用,那就是前期绑定。
解决这个问题的方案称为后期绑定,这意味着绑定发生在运行时,并基于对象的类型。后期绑定也称为动态绑定或运行时绑定,当一种语言实现后期绑定时,必须有某种机制在运行时来确定对象的类型,并调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但方法调用机制能找到并调用正确的方法体。
Java 中的所有方法绑定都是后期绑定,除非方法是 static 或 final 的(private 方法隐式为 final)。这意味着通常不需要你来决定是否要执行后期绑定,因为它会自动发生。
2. 尝试重写Private方法
看一下下面这段代码:
package com.yyj;public class PrivateOverride {private void f() {System.out.println("Private f()");}public static void main(String[] args) {PrivateOverride p = new Derived(); // 创建Derived对象p.f(); // Private f()}
}class Derived extends PrivateOverride {public void f() { // 你以为重写了父类中的f()System.out.println("Public f()");}
}
可能会很自然地认为输出应该为 Public f(),但 private 方法自动就是 final 的,并且对子类也是隐藏的,所以 Derived 的 f() 在这里是一个全新的方法,它甚至没有重载,因为 f() 的基类版本在 Derived 中是不可见的。
这样的结果就是,只有非 private 的方法可以被重写,但要注意重写 private 方法的假象,它不会产生编译器警告,但也不会执行你可能期望的操作,如果使用了 @Override 注解,那么这个问题就会被检测出来。
3. 字段访问与静态方法的多态
现在你可能会开始认为一切都可以多态地发生,但是,只有普通的方法调用可以是多态的。例如,如果直接访问一个字段,则该访问会在编译时解析:
package com.yyj;class Super {public int x = 0;public int getX() { return x; }
}class Sub extends Super {public int x = 1;@Override public int getX() { return x; }public int getSuperX() { return super.x; }
}public class GetField {public static void main(String[] args) {Super sup = new Sub(); // 向上转型System.out.println("sup.x = " + sup.x + ", sup.getX() = " + sup.getX());Sub sub = new Sub();System.out.println("sub.x = " + sub.x + ", sub.getX() = " + sub.getX() + ", sub.getSuperX() = " + sub.getSuperX());/** sup.x = 0, sup.getX() = 1* sub.x = 1, sub.getX() = 1, sub.getSuperX() = 0*/}
}
当 Sub 对象向上转型为 Super 引用时,任何字段访问都会被编译器解析,因此不是多态的。在此示例中,Super.x 和 Sub.x 被分配了不同的存储空间,因此,Sub 实际上包含两个被称为 x 的字段:它自己的字段和它从 Super 继承的字段。然而,当你在 Sub 中引用 x 时,Super 版本并不是默认的那个,要获得 Super 的字段必须明确地使用 super.x。
现在我们再来看一下静态方法,如果一个方法是静态的,那它的行为就不会是多态的,因为静态方法与类相关联,而不是与单个对象相关联:
package com.yyj;class StaticSuper {public static void staticPrint() {System.out.println("Super staticPrint()");}public void dynamicPrint() {System.out.println("Super dynamicPrint()");}
}class StaticSub extends StaticSuper {public static void staticPrint() {System.out.println("Sub staticPrint()");}@Overridepublic void dynamicPrint() {System.out.println("Sub dynamicPrint()");}
}public class StaticPolymorphism {public static void main(String[] args) {StaticSuper sup = new StaticSub(); // 向上转型StaticSub.staticPrint(); // Sub staticPrint()sup.dynamicPrint(); // Sub dynamicPrint()StaticSuper.staticPrint(); // Super staticPrint()}
}
4. 构造器内部的多态方法行为
构造器调用的层次结构带来了一个难题,对于正在构造的对象,如果在构造器中调用它的动态绑定方法,会发生什么?
在普通方法内部,动态绑定调用是在运行时解析的,这是因为对象不知道它是属于该方法所在的类还是其子类。如果在构造器内调用动态绑定方法,就会用到该方法被重写后的定义。但是,这个调用的效果可能相当出乎意料,因为这个被重写的方法是在对象(即子类对象)完全构造之前被调用的,因为是从外到内(即从基类到子类)执行构造器的,这可能会带来一些难以发现的错误。如下面这段代码所示:
package com.yyj;class A {void f() {System.out.println("A.f()");}A() {System.out.println("A() before A.f()");f(); // 其实是调用子类重写后的f()System.out.println("A() after A.f()");}
}class B extends A {private int x = 1; // 子类对象的默认初始值B(int x) {this.x = x;System.out.println("B(), x = " + x);}@Overridevoid f() {System.out.println("B.f(), x = " + x);}
}public class PolyConstructors {public static void main(String[] args) {new B(5);/** A() before A.f()* B.f(), x = 0* A() after A.f()* B(), x = 5*/}
}
A.f() 是为重写而设计的,这个重写发生在 B 中,但是在 A 的构造器调用了这个方法,而这个调用实际上是对 B.f() 的调用。输出显示,当 A 的构造器调用 f() 时,B.x 的值甚至不是默认的初始值1,而是0。
因此类的完整初始化过程如下:
- 在发生任何其他事情之前,为对象分配的存储空间会先被初始化为二进制零。
- 如前面所述的那样调用基类的构造器,此时被重写的
f()方法会被调用(是的,这发生在B构造器被调用之前),由于第1步的缘故,此时会发现B.x值为零。 - 按声明的顺序来初始化成员。
- 执行子类构造器的主体代码。
这样做有一个好处:一切至少都会初始化为零(或对于特定数据类型来说,是任何与零等价的值),而不仅仅是被视为垃圾。这包括通过组合嵌入在类中的对象引用,这些引用默认为 null。因此,如果忘记初始化该引用,在运行时就会出现异常。
因此,编写构造器时有一个很好的准则:用尽可能少的操作使对象逬入正常状态,如果可以避免的话,请不要调用此类中的任何其他方法。只有基类中的 final 方法可以在构造器中安全调用(这也适用于 private 方法,它们默认就是 final 的)这些方法不能被重写,因此不会产生这种令人惊讶的问题。
相关文章:
Java方法调用动态绑定(多态性)详解
CONTENTS 1. 方法调用绑定2. 尝试重写Private方法3. 字段访问与静态方法的多态4. 构造器内部的多态方法行为 1. 方法调用绑定 我们首先来看下面这个例子: package com.yyj;enum Tone {LOW, MIDDLE, HIGH; }class Instrument {public void play(Tone t) {System.ou…...
【SwiftUI模块】0060、SwiftUI基于Firebase搭建一个类似InstagramApp 2/7部分-搭建TabBar
SwiftUI模块系列 - 已更新60篇 SwiftUI项目 - 已更新5个项目 往期Demo源码下载 技术:SwiftUI、SwiftUI4.0、Instagram、Firebase 运行环境: SwiftUI4.0 Xcode14 MacOS12.6 iPhone Simulator iPhone 14 Pro Max SwiftUI基于Firebase搭建一个类似InstagramApp 2/7部分-搭建Tab…...
代码随想录第50天 | 84.柱状图中最大的矩形
84.柱状图中最大的矩形 //双指针 js中运行速度最快 var largestRectangleArea function(heights) {const len heights.length;const minLeftIndex new Array(len);const maxRigthIndex new Array(len);// 记录每个柱子 左边第一个小于该柱子的下标minLeftIndex[0] -1; //…...
深度学习---卷积神经网络
卷积神经网络概述 卷积神经网络是深度学习在计算机视觉领域的突破性成果。在计算机视觉领域。往往输入的图像都很大,使用全连接网络的话,计算的代价较高。另外图像也很难保留原有的特征,导致图像处理的准确率不高。 卷积神经网络࿰…...
Windows系统下安装CouchDB3.3.2教程
安装 前往CouchDB官网 官网点击download下载msi文件 双击该msi文件,一直下一步 创建个人account 设置cookie value 用于进行身份验证和授权。 愉快下载 点击OK 重启 启动 重启电脑后 打开浏览器并访问以下链接:http://127.0.0.1:5984/ 如果没有问…...
JavaScript基础知识(二)
JavaScript基础知识(二) 一、ES2015 基础语法1.变量2.常量3.模板字符串4.结构赋值 二、函数进阶1. 设置默认参数值2. 立即执行函数3. 闭包4. 箭头函数 三、面向对象1. 面向对象概述2. 基本概念3. 新语法 与 旧语法3.1 ES5 面向对象的知识ES5构造函数原型…...
SQL NULL Values(空值)
什么是SQL NULL值? SQL 中,NULL 用于表示缺失的值。数据表中的 NULL 值表示该值所处的字段为空。 具有NULL值的字段是没有值的字段。 如果表中的字段是可选的,则可以插入新记录或更新记录而不向该字段添加值。然后,该字段将被保存…...
云原生Docker网络管理
目录 Docker网络 Docker 网络实现原理 为容器创建端口映射 查看容器的输出和日志信息 Docker 的网络模式 查看docker网络列表 指定容器网络模式 网络模式详解 host模式 container模式 none模式 bridge模式 自定义网络 Docker网络 Docker 网络实现原理 Docker使用Lin…...
聊聊线程池的预热
序 本文主要研究一下线程池的预热 prestartCoreThread java/util/concurrent/ThreadPoolExecutor.java /*** Starts a core thread, causing it to idly wait for work. This* overrides the default policy of starting core threads only when* new tasks are executed. T…...
VueComponent的原型对象
一、prototype 每一个构造函数身上又有一个prototype指向其原型对象。 如果我们在控制台输入如下代码,就能看到Vue构造函数的信息,在他身上可以找到prototype属性,指向的是Vue原型对象: 二、__proto__ 通过构造函数创建的实例对…...
Redis不止能存储字符串,还有List、Set、Hash、Zset,用对了能给你带来哪些优势?
文章目录 🌟 Redis五大数据类型的应用场景🍊 一、String🍊 二、Hash🍊 三、List🍊 四、Set🍊 五、Zset 📕我是廖志伟,一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO…...
Python OpenCV通过灰度平均值进行二值化处理以减少像素误差
Python OpenCV通过灰度平均值进行二值化处理以减少像素误差 前言前提条件相关介绍实验环境通过灰度平均值进行二值化处理以减少像素误差固定阈值二值化代码实现 灰度平均值二值化代码实现 前言 由于本人水平有限,难免出现错漏,敬请批评改正。更多精彩内容…...
[Golang]多返回值函数、defer关键字、内置函数、变参函数、类成员函数、匿名函数
函数 文章目录 函数多返回值函数按值传递、按引用传递类成员函数改变外部变量变参函数defer和追踪说明一些常见操作实现 使用defer实现代码追踪记录函数的参数和返回值 常见的内置函数将函数作为参数闭包实例闭包将函数作为返回值 计算函数执行时间使用内存缓存来提升性能 参考…...
【剑指Offer】:删除链表中的倒数第N个节点(此题是LeetCode上面的)剑指Offer上面是链表中的倒数第K个节点
给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点 示例 1: 输入:head [1,2,3,4,5], n 2 输出:[1,2,3,5] 示例 2: 输入:head [1], n 1 输出:[] 示例 3:…...
acwing第 126 场周赛 (扩展字符串)
5281. 扩展字符串 一、题目要求 某字符串序列 s0,s1,s2,… 的生成规律如下: s0 DKER EPH VOS GOLNJ ER RKH HNG OI RKH UOPMGB CPH VOS FSQVB DLMM VOS QETH SQBsnDKER EPH VOS GOLNJ UKLMH QHNGLNJ Asn−1AB CPH VOS FSQVB DLMM VOS QHNG Asn−1AB,其…...
Milvus 介绍
Milvus 介绍 Milvus 矢量数据库是什么?关键概念非结构化数据嵌入向量向量相似度搜索 为什么是 Milvus?支持哪些索引和指标?索引类型相似度指标(Similarity metrics) 应用示例Milvus 是如何设计的?开发者工具API访问Milvus 生态系统工具 本页…...
Linux绝对路径和相对路径
在 Linux 中,简单的理解一个文件的路径,指的就是该文件存放的位置。 只要我们告诉 Linux 系统某个文件存放的准确位置,那么它就可以找到这个文件。指明一个文件存放的位置,有 2 种方法,分别是使用绝对路径和相对路径。…...
Linux:firewalld防火墙-基础使用(2)
上一章 Linux:firewalld防火墙-介绍(1)-CSDN博客https://blog.csdn.net/w14768855/article/details/133960695?spm1001.2014.3001.5501 我使用的系统为centos7 firewalld启动停止等操作 systemctl start firewalld 开启防火墙 systemct…...
【每日一练】20231023
统计每个字符出现的次数相关问题 方法一:map的put方法遍历 public class Test {public static void main(String[] args) {StringBuilder sb new StringBuilder("");Random ran new Random();for(int i0;i<2000000;i) {sb.append((char) (a ran.n…...
【项目经理】工作流引擎
项目经理之 工作流引擎 一、业务系统管理目的维护信息 二、组织架构管理目的维护信息 三、角色矩阵管理目的维护信息 四、条件变量管理目的维护信息 五、流程模型管理目的维护信息 六、流程版本管理目的维护信息 七、流程监管控制目的维护信息 系列文章版本记录 一、业务系统管…...
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
电脑插入多块移动硬盘后经常出现卡顿和蓝屏
当电脑在插入多块移动硬盘后频繁出现卡顿和蓝屏问题时,可能涉及硬件资源冲突、驱动兼容性、供电不足或系统设置等多方面原因。以下是逐步排查和解决方案: 1. 检查电源供电问题 问题原因:多块移动硬盘同时运行可能导致USB接口供电不足&#x…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
