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…...

【项目经理】工作流引擎
项目经理之 工作流引擎 一、业务系统管理目的维护信息 二、组织架构管理目的维护信息 三、角色矩阵管理目的维护信息 四、条件变量管理目的维护信息 五、流程模型管理目的维护信息 六、流程版本管理目的维护信息 七、流程监管控制目的维护信息 系列文章版本记录 一、业务系统管…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...

遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...

iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...