目录
碎碎念
ChatGPT 中出现的问题
那么正确答案应该是什么呢?
分派的相关知识点总结:
分派是什么?
静态分派与动态分派:
Java语言是静态多分派,动态单分派的;
静态分派:静态重载多分派:
动态分派:动态重写单分派:
多分派类型与单分派类型
例题
例题一:重载方法匹配优先级(基本类型):
请分析如下程序的运行结果:
运行结果:
解题关键:
例题二:重载方法匹配优先级(引用类型):
请分析如下程序的运行结果:
运行结果:
解题关键:
例题三:动态分派
请分析如下程序的运行结果:
运行结果:
解题关键:
例题四:动态分派
请分析如下程序的运行结果:
运行结果:
解题关键:
例题五:单分派和多分派:
请分析如下程序的运行结果:
运行结果:
解题关键:
例题六:【用友笔试】
根据下面这个程序的内容,判断哪些描述是正确的:( )
运行结果:
解题关键:
ChatGPT的出现引发的思考
乐一下,让ChatGPT扮演服务端开发人员,看他自己对ChatGPT有什么看法:
参考文献
碎碎念
近期英子姐推荐了一本有关JVM的书,所以最近在看这个,发现,之前看的好多八股都是从这里出来的,收益颇多(虽然不一定能记住,但是看了总比不看强,或许看多了就记住了);
这本书的名字叫:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》,本来是边看,不懂的就跟ChatGPT讨论,ChatGPT绝大多数时候都是靠谱的,直到第八章 虚拟机字节码执行引擎中的8.3 方法调用中的8.3.2分派这一节出现了一点问题,ChatGPT似乎有的时候并不能搞懂Java的动态分派;
PS:本来这个文章是周四晚上立的,准备周五写的flag,但是由于种种原因拖到了周日晚上,好在是在临近周一之前完成了
ChatGPT 中出现的问题
书中讲完动态分派之后,举了两个例子,ChatGPT就是在这第二个例子上翻车了,翻车实况见下图:
发现他说的有有问题之后,又让他改了两次,但可以,ChatGPT并没有把握住机会(doge)
那么正确答案应该是什么呢?
正确的输出结果应该是:
I am Son, i have $0
I am Son, i have $4
This gay has $2
原因如下:
- 输出两句都是“I am Son”,这是因为Son类在创建的时候,首先隐式调用了Father的构造函数,而Father构造函数中对showMeTheMoney()的调用是一次虚方法调用,实际执行的版本是Son::showMeTheMoney()方法,所以输出的是“I am Son”;
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- (如果有多个父类,那么接近上层的优先级越低)
- 如果你想简单的理解,那就记住上面的话,动态重写多分派,因为是重写,所以这里用动态分派,所以接近上层的优先级越低,所以这里调用的是Son类中重写的方法;
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- 而这时候虽然父类的money字段已经被初始化成2了,但Son::showMeTheMoney()方法中访问的却是子类的money字段,这时候结果自然还是0,因为它要到子类的构造函数执行时才会被初始化;
- 这里可能有人会乱套,乱套不要怕,直接IDEA debug!
- 走完父类的构造方法,那就回到子类中继续往下走,这个应该没啥问题,就正常的赋值,调用重写方法;
- 至此,new Son() 结束;
- main()的最后一句通过静态类型访问到了父类中的money,输出了2;
那么让我们一起重新复习一下分派的知识点吧!
分派的相关知识点总结:
分派是什么?
- Java中的分派(Dispatch)指的是根据方法的接收者和参数的实际类型,选择正确的方法实现的过程;
- Java中的分派主要包括静态分派和动态分派两种类型。
- 静态分派(Static Dispatch):
- 发生在编译期间,由编译器根据方法接收者和参数的静态类型确定具体调用的方法实现。例如,如果在代码中定义了一个方法,它的参数是Object类型,但在调用时传入了一个String类型的实例,编译器会选择Object类型的方法实现。
- 动态分派(Dynamic Dispatch):
- 发生在运行期间,由Java虚拟机根据方法接收者的实际类型确定具体调用的方法实现。例如,如果在代码中定义了一个父类和一个子类,它们都有一个同名的方法,在运行时调用子类实例的方法时,Java虚拟机会选择子类的方法实现。
- 静态分派(Static Dispatch):
- Java中的分派是基于多态的概念实现的。多态指的是同一操作作用于不同的对象,可以有不同的解释和不同的实现方式。在Java中,通过使用继承和重写方法实现多态。
静态分派与动态分派:
Java语言是静态多分派,动态单分派的;
- 如果是重载方法之间的选择,则是使用静态类型;
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- (如果有多个父类,那么接近上层的优先级越低)
- 如 A a = new B(); 会使用类型B去查找重写的方法,使用类型A去查找重载的方法;
- 静态分派发生在编译期间,根据参数的静态类型来决定选择哪个重载方法;
- 动态分派发生在运行期间,根据对象的实际类型来决定调用哪个重写方法;
静态分派:静态重载多分派:
- 静态分派(《Thinking In Java》中称之为静态绑定(前期绑定)):
- 所有依赖静态类型来定位方法执行版本(版本即哪一个方法)的分派动作,静态分派的最典型的应用就是方法重载;
- 静态类型在编译期是可知的;
- 1)基本类型(包装类型):
- 以char为例,按照char>int>long>double>float>double>Character>Serializable>Object>...(变长参数,将其视为一个数组元素)
- 变长参数的重载优先级最低;
- (注意char到byte或short之间的转换时不安全的)
- 基本类型与基本类型之间存在自动类型转换;
- 基本类型到包装类型之间存在自动装箱;
- java.lang.Serializable是java.lang.Character类实现的一个接口;
- Character是绝对不会转型为Integer的,它只能安全地转型为它实现的接口或父类;Character还实现了另外一个接口java.lang.Comparable<Character>,如果同时出现两个参数分别为Serializable和 Comparable<Character>的重载方法,那它们在此时的优先级是一样的;编译器无法确定要自动转型为哪种类型,会提示“类型模糊”(Type Ambiguous),并拒绝编译;但是如果绕过Javac编译器,自己去构造出表达相同语义的字节码,将会发现这是能够通过Java虚拟机的类加载校验,而且能够被Java虚拟机正常执行的,但是会选择Serializable还是Comparable<Character>的重载方法则并不能事先确定,这是《Java虚拟机规范》所允许的;
- 【注意】有一些在单个参数中能成立的自动转型,如char转型为int,在变长参数中是不成立的;
- 2)引用类型:
- 则需要根据继承关系进行匹配,注意只跟其编译时类型即静态类型相关;
- 如果是重载方法之间的选择,则是使用静态类型;
动态分派:动态重写单分派:
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- (如果有多个父类,那么接近上层的优先级越低)
- 动态分派(《Thinking In Java》中称之为动态绑定(后期绑定)):
- 在运行期根据实际类型确定执行版本的分派过程称为动态分派,这是重写的实际本质,在重写过程中并不是唯一的版本,而是选择更加合适的版本(如果有多个父类,那么接近上层的优先级越低);
多分派类型与单分派类型
- 多分派类型:
- 根据一个以上的宗量(方法的接受者与方法的参数统称为方法的宗量)进行方法的选择方法的分派类型;其中静态分派属于多分派类型;即Father father = new Son(); father.overloadMethod(param),中overloadMethod()方法的选择是要根据静态类型Father与方法的参数param共同确定的;
- 单分派类型:
- 动态分配属于单分派类型,即只会根据实际类型Son选择方法;
- 总结:
- 静态多分派,动态单分派的语言;
或许看完很抽象,所以应该结合例子看,例子如下!
例题
例题将包括5个书上的,和一个笔试题
例题一:重载方法匹配优先级(基本类型):
请分析如下程序的运行结果:
package org.fenixsoft.polymorphic;
public class Overload {
public static void sayHello(Object arg) {
System.out.println("hello Object");
}
public static void sayHello(int arg) {
System.out.println("hello int");
}
public static void sayHello(long arg) {
System.out.println("hello long");
}
public static void sayHello(Character arg) {
System.out.println("hello Character");
}
public static void sayHello(char arg) {
System.out.println("hello char");
}
public static void sayHello(char... arg) {
System.out.println("hello char ...");
}
public static void sayHello(Serializable arg) {
System.out.println("hello Serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
运行结果:
hello char
解题关键:
你只需要记住上面说的基本类型静态分派的顺序即可:
以char为例,按照char>int>long>double>float>double>Character>Serializable>Object>...(变长参数,将其视为一个数组元素)
例题二:重载方法匹配优先级(引用类型):
请分析如下程序的运行结果:
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
运行结果:
hello,guy!
hello,guy!
解题关键:
- Java语言是静态多分派,动态单分派的;
- 如果是重载方法之间的选择,则是使用静态类型;
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- (如果有多个父类,那么接近上层的优先级越低)
- 如A a = new B(); 会使用类型B去查找重写的方法,使用类型A去查找重载的方法;
例题三:动态分派
请分析如下程序的运行结果:
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
运行结果:
man say hello
woman say hello
woman say hello
解题关键:
- 在Java里面只有虚方法存在,字段永远不可能是虚的,换句话说,字段永远不参与多态,哪个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的那个字段;当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段;
例题四:动态分派
请分析如下程序的运行结果:
public class FieldHasNoPolymorphic {
static class Father {
public int money = 1;
public Father() {
money = 2;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Father, i have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son() {
money = 4;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Son, i have $" + money);
}
}
public static void main(String[] args) {
Father gay = new Son();
System.out.println("This gay has $" + gay.money);
}
}
运行结果:
I am Son, i have $0
I am Son, i have $4
This gay has $2
解题关键:
- 输出两句都是“I am Son”,这是因为Son类在创建的时候,首先隐式调用了Father的构造函数,而Father构造函数中对showMeTheMoney()的调用是一次虚方法调用,实际执行的版本是Son::showMeTheMoney()方法,所以输出的是“I am Son”;
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- (如果有多个父类,那么接近上层的优先级越低)
- 如果你想简单的理解,那就记住上面的话,动态重写多分派,因为是重写,所以这里用动态分派,所以接近上层的优先级越低,所以这里调用的是Son类中重写的方法;
- 如果是父类与子类之间的重写方法的选择,则是使用动态类型;
- 而这时候虽然父类的money字段已经被初始化成2了,但Son::showMeTheMoney()方法中访问的却是子类的money字段,这时候结果自然还是0,因为它要到子类的构造函数执行时才会被初始化;
- 这里可能有人会乱套,乱套不要怕,直接IDEA debug!
- 走完父类的构造方法,那就回到子类中继续往下走,这个应该没啥问题,就正常的赋值,调用重写方法;
- 至此,new Son() 结束;
- main()的最后一句通过静态类型访问到了父类中的money,输出了2;
例题五:单分派和多分派:
请分析如下程序的运行结果:
public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
运行结果:
father choose 360
son choose qq
解题关键:
- 在Java语言中,方法的选择过程包括两个阶段:静态分派和动态分派;
- 静态分派发生在编译期间,根据参数的静态类型来决定选择哪个重载方法;而动态分派发生在运行期间,根据对象的实际类型来决定调用哪个重写方法;
- 在本例中,静态分派选择的目标方法签名是hardChoice(QQ)和hardChoice(_360),但实际执行的方法取决于运行时对象的实际类型;
- 因此,father.hardChoice(new _360())调用了Father类中的hardChoice(_360)方法,而son.hardChoice(new QQ())调用了Son类中的hardChoice(QQ)方法;
- 由于动态分派的目标方法只与接收者的实际类型有关,而与参数的类型无关,因此Java语言的动态分派属于单分派类型;
例题六:【用友笔试】
根据下面这个程序的内容,判断哪些描述是正确的:( )
public class Test {
public static void main(String args[]) {
String s = "tommy";
Object o = s;
sayHello(o); //语句1
sayHello(s); //语句2
}
public static void sayHello(String to) {
System.out.println(String.format("Hello, %s", to));
}
public static void sayHello(Object to) {
System.out.println(String.format("Welcome, %s", to));
}
}
- A. 这段程序有编译错误
- B. 语句1输出为:Hello, tommy
- C. 语句2输出为:Hello, tommy
- D. 语句1输出为:Welcome, tommy
- E. 语句2输出为:Welcome, tommy
- F. 根据选用的Java编译器不同,这段程序的输出可能不同
运行结果:
正确答案: C D
解题关键:
相信懂了书里面5个较难例子的你,肯定做对啦,所以也就不讲啦!
ChatGPT的出现引发的思考
ChatGPT作为一个超强的AI,或许能取代一部分程序员,但是正所谓”智者千虑必有一失,愚者千虑必有一得“,在学习和生活中要不断思考,深挖才会有更多的价值!
并且现在ChatGPT已经能输出图片,并且也接入了一些聊天软件
可以看出,虽然ChatGPT能够根据文字描述生成大体符合要求的图片,但是一些细节问题仍有较大的发展空间,之前我还担心有了ChatGPT我的研究生方向:人脸超分辨率恢复与重建会不会变的没有意义,现在看来,会有意义,至少在我毕业之前仍会存在异议,从上图中可以看出,虽然ChatGPT能够生成大概的场景,但是对人脸五官的细节恢复十分差劲;
乐一下,让ChatGPT扮演服务端开发人员,看他自己对ChatGPT有什么看法:
参考文献
- 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》
- ChatGPT