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

【项目经理】工作流引擎
项目经理之 工作流引擎 一、业务系统管理目的维护信息 二、组织架构管理目的维护信息 三、角色矩阵管理目的维护信息 四、条件变量管理目的维护信息 五、流程模型管理目的维护信息 六、流程版本管理目的维护信息 七、流程监管控制目的维护信息 系列文章版本记录 一、业务系统管…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...
【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error
在前端开发中,JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作(如 Promise、async/await 等),开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝(r…...