多态
示例一:
public class Aoo {
public void testAll() {
testPublic();
testPrivate();
}
public void testPublic() {
System.out.println("Aoo public..");
}
private void testPrivate() {
System.out.println("Aoo private..");
}
}
public class Boo extends Aoo{
@Override
public void testPublic() {
System.out.println("Boo public");
}
private void testPrivate() {
System.out.println("Boo private");
}
public static void main(String[] args) {
Boo boo = new Boo();
boo.testAll();
}
}
运行结果:
Boo public
Aoo private..
问题
子类对象中为什么会出现父类对象的私有方法?
这个问题涉及到java语言中的多态这个基本的特征,具体用到的就是方法的调用方面的知识,这方面的内容和java虚拟相关,需要有一定的基础,没有相关基础的同学可以虚拟机实现方面的信息忽略。
方法调用
方法的调用就是确定被调用方法的版本,不等同于方法的执行。方法的调用涉及三种类型
- 解析
- 分派
- 动态类型语言支持 ps:本文只针对前两种类型,第三种类型以后再写。
在java虚拟机中提供了5中方法调用的字节码指令
- invokestatic 调用静态方法
- invokespecial 调用调用实例构造器、私有方法和父类方法
- invokevirtual 调用所有的虚方法
- invokeinterface 调用接口方法
- invokedynamic 调用用动态方法
解析
对编译器编译时就必须确定调用版本的方法的调用称为解析。这些方法的版本在编译时候已经能确定,并且在运行时是不可以改变的。在java中符合“编译期可知,运行期不可变“原则的方法主要包括静态方法和私有方法两大类,静态方法是属于类的方法,私有方法是在外部不可以被访问的,这两种方法都不可能被重写其他版本的。
只要能被invokestatic和invokespecial调用的方法都是解析阶段可以唯一确定的,符合条件的主要包括静态方法、私有方法、 实例构造方法(ps:实例构造方法,java虚拟中构造方法分实例构造方法和类构造方法)、父类方法(父类的实例化方法(在子类里边使用super关键字调用))。这些方法和被final修饰的方法一起被称为非虚方法(ps:与上面的虚方法相对应,这些方法的特征就是无法再子类重写)。
总结一下:
简单理解 :解析就是对在编译期就确定方法调用的版本的方法的调用,这些方法不能被重写。
分派
java中分派分类可分为:
静态分派、动态分派(根据分派依赖的变量类型分类)
单分派、多分派(分派依据的宗量数分类)
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派典型应用是方法的重载。解析与分派这两者之间的关系并不是二选一的排他关系,它们是在不同层次上去筛选、 确定目标方法的过程。
先解释下什么是静态类型,在下面所示代码中
Animal cat = new Cat();
Animal 称之为静态类型、Cat 称之为实际类型,这两种类型的区别是静态类型是在编译期可知的,实际类型在运行期才知道。静态分派发生在编译阶段,编译期只知道变量的静态类型,编译期在重载时根据变量的静态类型来决定使用哪一个重载版本的方法。
方法重载的优先级
编译期虽然能确定方法重载的版本,但是很多情况下方法的版本并不是唯一的,所以编译期只能选择一个更加适合的版本。
示例二:
public class Overload {
public static void say(char c) {
System.out.println("now ,I am char");
}
public static void say(short c) {
System.out.println("now ,I am short");
}
public static void say(int c) {
System.out.println("now ,I am int");
}
public static void say(long c) {
System.out.println("now ,I am long");
}
public static void say(Character c) {
System.out.println("now ,I am Character");
}
public static void say(Integer c) {
System.out.println("now ,I am Integer");
}
public static void say(Object c) {
System.out.println("now ,I am Object");
}
public static void say(char... cs) {
System.out.println("now ,I am char array");
}
public static void say(Serializable c) {
System.out.println("now ,I am Serializable");
}
public static void main(String[] args) {
say('c');
}
}
运行结果:
now ,I am short
注释掉 say(char c)
结果为:
now ,I am int
'c' 转型为int类型(注意并没有转换为short类型)
注释掉 say(int c)
结果为:
now ,I am long
'c' 转型为long类型
注释掉 say(long c)
结果为:
now ,I am Character
'c' 转型为charde 包装类Character类型
注释掉 say(Character c)
结果为:
now ,I am Serializable
'c' 转型为Character类型实现的接口Serializable类型
注释掉 say(Character c)
结果为:
now ,I am Object
'c' 转型为Object类型
注释掉 say(Object c)
结果为:
now ,I am char array
'c' 转型为char[]类型(在虚拟机中数组类是虚拟机根据实际类型生成的新类,本例中类型为:[c)
方法重载的优先级为:
基本类型--基本类型向上转型直到long--包装类型--接口--object--数组
ps:上面的例子在《深入理解java虚拟中》被称为关于茴香豆的茴有几种写法的研究
,大佬们还是挺有意思的吗,哈哈
动态分派
所有依赖动态类型来定位方法执行版本的分派动作称为动态分派,动态分派与多态的方法重写有关。
动态分派和invokevirtual有关,需要理解动态分派需要理解invokevirtual指令的动态查找过程,invokevirtual指令的运行时解析过程如下:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
虚拟机动态分派的实现
虚拟机中动态分派动作十分的频繁,而且需要动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法。因此基于性能的考虑,并不会在元数据中搜索,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表。
示例三
public class Dispatch{
static class QQ{}
static class_360{}
public static class Father{
public void hardChoice(QQ arg){
System.out.println("father choose qq");
}p
ublic void hardChoice(_360 arg){
System.out.println("father choose 360");
}}p
ublic static class Son extends Father{
public void hardChoice(QQ arg){
System.out.println("son choose qq");
}p
ublic void hardChoice(_360 arg){
System.out.println("son choose 360");
}}p
ublic static void main(String[]args){
Father father=new Father();
Father son=new Son();
father.hardChoice(new_360());
son.hardChoice(new QQ());
}}
虚方法表中存放着各个方法的实际入口地址。 如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。 如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
单分派与多分配
单分派和多分派是根据方法的宗量划分的,方法的接收者
与方法的参数
统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
示例三中,编译阶段方法版本的选择根据(静态分派):
1.静态变量是father还是son
2.方法的参数是QQ还是360
需要依赖方法的参数和方法的接受者这两个参数决定,所以java的静态分派属于多分派。
在运行时虚拟机选择(动态分派)根据只有对象的实际类型,因此java的动态分派属于单分派。
Father father=new Father();
不存在多态,不涉及动态分派
Father son=new Son();
存在多态,涉及动态分派
示例一分析
到目前为止java中方法调用的机制前两种已经讲完了,那么就来分析一下示例一中的问题
public static void main(String[] args) {
Boo boo = new Boo();
boo.testAll();
}
运行mian方法时虚拟机运行的过程
1.编译
2.加载Aoo、Boo两个类
3.执行main方法
我们逐步分析每一步做了什么
- 编译
编译时期分别编译Aoo、Boo两个类
1.1 Aoo的编译
分别编译Aoo中testAll
、testPublic
、testPrivate
三个方法,testAll
方法涉及testPublic
、testPrivate
两个方法的调用,编译过程中只涉及到testAll
中两个方法的版本确定。
1.1.1
testPrivate
方法调用时私有方法属于非虚方法,因此该方法的调用属于解析 因此在编译期就确定了方法调用的版本为Aoo类的testPrivate
,这也就是为什么输出结果中有Aoo private..的原因
1.1.2
testPublic
方法属于虚方法,因此次方法的调用涉及到分派,又因为testPublic
不存在方法的重载所有在编译期也不涉及到静态分派,在编译期不能确定方法调用的版本。
1.2 Boo的编译
Boo中只有main方法中涉及到方法的调用,调用的是testAll
方法,属于虚方法需要在运行期确定调用方法的版本。 - 类的加载
运行main方法属于Boo的方法,因此在运行前先加载Boo类,在加载Boo时候需要先将它的所有父类依次加载 - 执行main方法(运行期)
运行期boo.testAll();
运行前需要确定boo.testAll();
的调用版本涉及到了动态分派
根据动态分派的运行机制
3.1 找到对象的实际类型,实际类型是Boo类型
3.2 在Boo类中找testAll();
方法,没有找到,去Boo类的父类Aoo类中查找,找到Aoo类的testAll()
,进行访问权限校验,testAll()
是public的,校验通过,查找过程结束;同理查找testPublic()的方法的版本,是Boo类的testPublic方法,
这样运行期方法的调用就完成了,之后就是方法的执行了!
文章末尾有示例一反编译后的字节码文件,需要的可以看看。
更正:上一个版本写的关于Boo类的编译时候testAll方法的调用有一些问题,我开始对invokespeical调用父类方法的理解存在一定的问题。
第一次写博客,不足之处希望大家及时指出,留言交流 !
^_^
参考:JVM规范学习:invokespecial
示例一反编译后字节码文件
D:\codes\java\test\Base\bin\org\goblin\base>javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
--------------------------
语法:
javap [ 命令选项 ] class. . .
javap 命令用于解析类文件。其输出取决于所用的选项。若没有使用选项,javap 将输出传递给它的类的 public 域及方法。javap 将其输出到标准输出设备上。
命令选项
-help 输出 javap 的帮助信息。
-l 输出行及局部变量表。
-b 确保与 JDK 1.1 javap 的向后兼容性。
-public 只显示 public 类及成员。
-protected 只显示 protected 和 public 类及成员。
-package 只显示包、protected 和 public 类及成员。这是缺省设置。
-private 显示所有类和成员。
-J[flag] 直接将 flag 传给运行时系统。
-s 输出内部类型签名。
-c 输出类中各方法的未解析的代码,即构成 Java 字节码的指令。
-verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本
-classpath[路径] 指定 javap 用来查找类的路径。如果设置了该选项,则它将覆盖缺省值或 CLASSPATH 环境变量。目录用冒号分隔。
-bootclasspath[路径] 指定加载自举类所用的路径。缺省情况下,自举类是实现核心 Java 平台的类,位于 jrelib下面。
-extdirs[dirs] 覆盖搜索安装方式扩展的位置。扩展的缺省位置是 jrelibext。
--------------------------
D:\codes\java\test\Base\bin\org\goblin\base>javap -v -p Aoo
警告: 二进制文件Aoo包含org.goblin.base.Aoo
Classfile /D:/codes/java/test/Base/bin/org/goblin/base/Aoo.class
Last modified 2017-10-12; size 702 bytes
MD5 checksum d95972036955ba0332899e531bb95843
Compiled from "Aoo.java"
public class org.goblin.base.Aoo
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // org/goblin/base/Aoo
#2 = Utf8 org/goblin/base/Aoo
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/goblin/base/Aoo;
#14 = Utf8 testAll
#15 = Methodref #1.#16 // org/goblin/base/Aoo.testPublic:()V
#16 = NameAndType #17:#6 // testPublic:()V
#17 = Utf8 testPublic
#18 = Methodref #1.#19 // org/goblin/base/Aoo.testPrivate:()V
#19 = NameAndType #20:#6 // testPrivate:()V
#20 = Utf8 testPrivate
#21 = Fieldref #22.#24 // java/lang/System.out:Ljava/io/PrintStream;
#22 = Class #23 // java/lang/System
#23 = Utf8 java/lang/System
#24 = NameAndType #25:#26 // out:Ljava/io/PrintStream;
#25 = Utf8 out
#26 = Utf8 Ljava/io/PrintStream;
#27 = String #28 // Aoo private..
#28 = Utf8 Aoo private..
#29 = Methodref #30.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#30 = Class #31 // java/io/PrintStream
#31 = Utf8 java/io/PrintStream
#32 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V
#35 = String #36 // Aoo public..
#36 = Utf8 Aoo public..
#37 = Utf8 SourceFile
#38 = Utf8 Aoo.java
{
public org.goblin.base.Aoo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/goblin/base/Aoo;
public void testAll();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #15 // Method testPublic:()V
4: aload_0
5: invokespecial #18 // Method testPrivate:()V
8: return
LineNumberTable:
line 6: 0
line 7: 4
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/goblin/base/Aoo;
private void testPrivate();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=1, args_size=1
0: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #27 // String Aoo private..
5: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/goblin/base/Aoo;
public void testPublic();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #35 // String Aoo public..
5: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/goblin/base/Aoo;
}
SourceFile: "Aoo.java"
D:\codes\java\test\Base\bin\org\goblin\base>javap -v -p Boo
警告: 二进制文件Boo包含org.goblin.base.Boo
Classfile /D:/codes/java/test/Base/bin/org/goblin/base/Boo.class
Last modified 2017-10-12; size 777 bytes
MD5 checksum 86b66526e75b65ce679a2726e59e1e80
Compiled from "Boo.java"
public class org.goblin.base.Boo extends org.goblin.base.Aoo
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // org/goblin/base/Boo
#2 = Utf8 org/goblin/base/Boo
#3 = Class #4 // org/goblin/base/Aoo
#4 = Utf8 org/goblin/base/Aoo
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // org/goblin/base/Aoo."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/goblin/base/Boo;
#14 = Utf8 testPublic
#15 = Fieldref #16.#18 // java/lang/System.out:Ljava/io/PrintStream;
#16 = Class #17 // java/lang/System
#17 = Utf8 java/lang/System
#18 = NameAndType #19:#20 // out:Ljava/io/PrintStream;
#19 = Utf8 out
#20 = Utf8 Ljava/io/PrintStream;
#21 = String #22 // Boo public
#22 = Utf8 Boo public
#23 = Methodref #24.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#24 = Class #25 // java/io/PrintStream
#25 = Utf8 java/io/PrintStream
#26 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
#29 = Utf8 testPrivate
#30 = String #31 // Boo private
#31 = Utf8 Boo private
#32 = Utf8 main
#33 = Utf8 ([Ljava/lang/String;)V
#34 = Methodref #1.#9 // org/goblin/base/Boo."<init>":()V
#35 = Methodref #1.#36 // org/goblin/base/Boo.testAll:()V
#36 = NameAndType #37:#6 // testAll:()V
#37 = Utf8 testAll
#38 = Utf8 args
#39 = Utf8 [Ljava/lang/String;
#40 = Utf8 boo
#41 = Utf8 SourceFile
#42 = Utf8 Boo.java
{
public org.goblin.base.Boo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method org/goblin/base/Aoo."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/goblin/base/Boo;
public void testPublic();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #21 // String Boo public
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/goblin/base/Boo;
private void testPrivate();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #30 // String Boo private
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/goblin/base/Boo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #1 // class org/goblin/base/Boo
3: dup
4: invokespecial #34 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #35 // Method testAll:()V
12: return
LineNumberTable:
line 14: 0
line 15: 8
line 16: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 boo Lorg/goblin/base/Boo;
}
SourceFile: "Boo.java"