文章目录
反编译的强大也是在我学习完之后知道的,他解决了在平时写代码时我们可能不会去考虑一些细节性的问题,就比如非静态成员是何时赋值的,无参构造方法真的没有参数吗?本文旨在通过反编译对相关知识进行分析。
引子
一道经典的基础面试题:
面试官:说说看在 Java 中定义一个不做事且没有参数的构造方法的作用 ;
我(十分自信的回答道):Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定 的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
面试官: 那你能证明下为啥子类会调用父类的无参构造方法么?
我(表面陷入沉思心里则暗自得意):那我从以下几个方面来说明吧:
首先.在我们日常写代码的时候,要是父类没有无参构造而是直接有其他参数构造,那么子类在定义完后直接就出问题了:
这个便是从表面就可以看出来的十分直观,接下来我们看看他的字节码:
诶,我们可以从字节码中很直观的看到,子类的构造方法中会去调用父类的无参构造方法,所以如果父类没有无参构造方法就会在实例化时因为无法找到对应的父类无参构造所以直接出问题啦!
字符串操作
直接上字节码,一目了然:
首先先对c = a+b说明,他的底层其实是用的StringBuilder的append把俩字符串拼接起来的最终一个tostring转化成字符串c,所以为啥c==D打印的是否,而F是new出来String对象直接在堆里,D是在常量池中。
总结下:像是a,b,D,E这些可以直接确定的字符串就直接放在元空间的常量池中
像是F(new 一个String对象),c这种通过拼接(本质就是new StringBuilder对象)是在堆中。
至于里面的助记符都是干嘛的,别着急,底下会说明。
构造方法:
默认构造方法
public class Son {
String name = "XiaoHeng";
private int x = 3;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
}
上面一个简单类且并没有给构造方法,通过反编译来看其默认构造方法:
通过反编译我们可以知道:
默认构造方法的以下特点:
描述符:无参数无返回值
权限修饰符:public
Code结构:
stack :表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
locals :表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
arg_size :方法参数个数
aload-0: 将索引为0处的引用推送至操作数栈栈顶
invokespecial #12: 调用父类(Object类)的构造方法
ldc #14: 从运行时常量池中推送一个字符串“XiaoHeng”
putfield#16 :把该字符串赋值给name成员
iconst-3 :将int常量3推送至操作数栈
putfiled#18 :把3赋值给成员x
return:返回
LineNumberTable:属性表存放方法的行号信息
LocalVariableTable:属性表中存放方法的局部变量信息。
分析完之后我们发现:
1.实例成员(非静态成员)的赋值是在构造方法中完成的
2.默认无参构造方法中有参数,局部变量表中显示出该参数为this这是因为:
对于Java类中的每一个实例方法(非static方法),其在编译后所生 成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个(this) ,它位于方法的第一个参数位置处;这样,我们就可以在Java的实例方法中使用this来去访问当前对象的属性以及其他方法。
这个操作是在编译期间完成的,即由javac编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,
由JVM在调用实例方法时,自动向实例方法传入该this参数。所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。
多个自定义构造方法
接下来分析多个构造方法:
public class Son {
String name = "XiaoHeng";
private int x = 3;
public Son() {
}
public Son(int para) {
this.x = para;
}
public Son(int para,String name) {
System.out.println("two");
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
}
上面给出了三个构造方法,以下是对应的反编译结果:
public com.mec.hui.javap.Son();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #14 // String XiaoHeng
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: iconst_3
12: putfield #18 // Field x:I
15: return
LineNumberTable:
line 7: 0
line 4: 4
line 5: 10
line 9: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/mec/hui/javap/Son;
public com.mec.hui.javap.Son(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #14 // String XiaoHeng
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: iconst_3
12: putfield #18 // Field x:I
15: aload_0
16: iload_1 //iload_1 将局部变量表中索引为1的元素推送到操作数栈,通过下面的表我们可以知道该元素就是para
17: putfield #18 // Field x:I
20: return
LineNumberTable:
line 11: 0
line 4: 4
line 5: 10
line 12: 15
line 13: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Lcom/mec/hui/javap/Son;
0 21 1 para I
public com.mec.hui.javap.Son(int, java.lang.String);
descriptor: (ILjava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #14 // String XiaoHeng
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: iconst_3
12: putfield #18 // Field x:I
15: getstatic #27 // Field java/lang/System.out:Ljava/io/PrintStream;获取out静态成员变量
18: ldc #33 // String two
20: invokevirtual #35 // Method java/io/PrintStream.println:(Ljava/lang/String;)V,运行时调用虚方法
23: return
LineNumberTable:
line 15: 0
line 4: 4
line 5: 10
line 16: 15
line 17: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this Lcom/mec/hui/javap/Son;
0 24 1 para I
0 24 2 name Ljava/lang/String;
通过上面三个构造方法反编译的结果我们不难发现他们每个都具备完成实例成员赋值和包含this,其实非静态成员在每个构造方法中都会进行赋值这一点很好理解,这样做可以保证无论通过哪一个构造方法来实例化对象都可以完成非静态成员的赋值。
普通方法:
在看下get方法反编译的结果:
public int getX();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #18 // Field x:I
4: ireturn
LineNumberTable:
line 20: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mec/hui/javap/Son;
可以看出,get方法的局部表量表中也有this,他的索引也是位于0处;所以说:对于Java类中的每一个实例方法(非static方法),其在编译后所生成的字节码当中,方法参数的数量总是会比源代码中方法参数的数量多一个(this) ,它位于方法的第一个参数位置处;
静态属性:
在 < clinit>中完成初始化
静态成员:
public class Son {
String name = "XiaoHeng";
private int x = 3;
private static int a = 1;
public Son() {
}
public Son(int para) {
this.x = para;
}
public Son(int para,String name) {
System.out.println("two");
}
反编译的结果:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #13 // Field a:I
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
很明显的看出对静态成员a进行赋值
要是我们在给代码中加入一个静态代码块,看看会发生什么:
public class Son {
String name = "XiaoHeng";
private int x = 3;
private static int a = 1;
static {
System.out.println("static");
}
public Son() {
}
反编译结果如下:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: iconst_1
1: putstatic #13 // Field a:I
4: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #21 // String static
9: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 7: 0
line 10: 4
line 11: 12
LocalVariableTable:
Start Length Slot Name Signature
我们会发现对于静态变量赋值和静态代码块执行再一起,也就是说不管有多少个静态代码块,最终都会合成一个 ,都在之中执行。
静态方法:
public static int geta() {
return a;
}
反编译:
public static int geta();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #13 // Field a:I
3: ireturn
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
静态方法和普通方法的最大区别就是静态方法多了个权限描述符:ACC—STATIC
含有锁的方法:
private void lock(String str) {
synchronized (str) {
System.out.println("synchronized");
}
}
反编译结果:
public void lock(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: getstatic #27 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #45 // String synchronized
9: invokevirtual #35 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_2
13: monitorexit
14: goto 20
17: aload_2
18: monitorexit
19: athrow
20: return
Exception table:
from to target type
4 14 17 any
17 19 17 any
LineNumberTable:
line 27: 0
line 28: 4
line 27: 12
line 30: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Lcom/mec/hui/javap/Son;
0 21 1 str Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/mec/hui/javap/Son, class java/lang/String, class java/lang/String ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 2
}
3: monitorenter 进入一个对象的监视器当中
13: monitorexit。这两条指令说明了进入锁域和出锁域,我们会发现如果正常退出的话接下来就会 14: goto 20
跳转到第20条指令return结束。
但是 接下来还有个18: monitorexit,这也是退出锁域,为什么一个入口对应两个出口呢?
这是因为:当线程A进入某个方法的锁后即持有该monitor的所有权,锁的标志位就会由0置为1,此时其他线程无法进入锁中只能阻塞或者自旋,要是A在锁域中又进入了同一对象的另外一个方法synchronized 锁域中,计数就会由1变成2,即可重入锁,当A从第二个锁域退出后计数由2变为1,等从第一个锁域退出后计数变为0,此时其他线程才可进入。
对比我们上面的lock方法,为了防止线程A在进入锁后出现执行打印语句时异常(sysout是io操作,会产生异常),导致计数1没有办法置为0,其他线程无法进入。所以第二个monitorexit就是表示在异常抛出之前先退出锁域19: athrow,即将1置为0,使得之后其他线程可以进入。
带有锁的方法:
含有锁的方法是方法内部代码有synchronized 块,而带有锁的方法是方法描述上带有synchronized
public synchronized void lock2() {
System.out.println("synchronized2");
}
反编译结果:
public synchronized void lock2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #61 // String synchronized2
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 43: 0
line 44: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/mec/hui/javap/Son;
}
我们会发现带有锁的方法就是权限描述符上多了个 ACC_SYNCHRONIZED
异常处理:
public class Test {
public void test() {
try {
InputStream inputStream = new FileInputStream("11.txt");
ServerSocket serverSocket = new ServerSocket(54199);
serverSocket.accept();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}finally {
System.out.println("over");
}
}
}
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: new #15 // class java/io/FileInputStream
3: dup
4: ldc #17 // String 11.txt
6: invokespecial #19 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #22 // class java/net/ServerSocket
13: dup
14: ldc #24 // int 54199
16: invokespecial #25 // Method java/net/ServerSocket."<init>":(I)V
19: astore_2
20: aload_2
21: invokevirtual #28 // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
24: pop
25: goto 87
28: astore_1
29: aload_1
30: invokevirtual #32 // Method java/io/FileNotFoundException.printStackTrace:()V
33: getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
36: ldc #43 // String over
38: invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
41: goto 95
44: astore_1
45: aload_1
46: invokevirtual #50 // Method java/io/IOException.printStackTrace:()V
49: getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
52: ldc #43 // String over
54: invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
57: goto 95
60: astore_1
61: aload_1
62: invokevirtual #53 // Method java/lang/Exception.printStackTrace:()V
65: getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
68: ldc #43 // String over
70: invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
73: goto 95
76: astore_3
77: getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
80: ldc #43 // String over
82: invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
85: aload_3
86: athrow
87: getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
90: ldc #43 // String over
92: invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
95: return
Exception table:
from to target type
0 25 28 Class java/io/FileNotFoundException
0 25 44 Class java/io/IOException
0 25 60 Class java/lang/Exception
0 33 76 any
44 49 76 any
60 65 76 any
LineNumberTable:
line 12: 0
line 14: 10
line 15: 20
line 17: 25
line 19: 29
line 26: 33
line 20: 44
line 22: 45
line 26: 49
line 23: 60
line 24: 61
line 26: 65
line 25: 76
line 26: 77
line 27: 85
line 26: 87
line 28: 95
LocalVariableTable:
Start Length Slot Name Signature
0 96 0 this Lcom/mec/ex/Test;
10 15 1 inputStream Ljava/io/InputStream;
20 5 2 serverSocket Ljava/net/ServerSocket;
29 4 1 e Ljava/io/FileNotFoundException;
45 4 1 e Ljava/io/IOException;
61 4 1 e Ljava/lang/Exception;
StackMapTable: number_of_entries = 6
frame_type = 92 /* same_locals_1_stack_item */
stack = [ class java/io/FileNotFoundException ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/io/IOException ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 10 /* same */
frame_type = 7 /* same */
}
new :创建一个对象,分配空间
dup:复制操作数栈顶的值并压栈
astore : 将栈顶的引用存入局部变量,也就是把new出来对象的首地址赋值给inputStream
Exception table:异常表
以表中第一条记录为例:
from 0 to 25 target 28 type Class java/io/FileNotFoundException
意思就是:从0到24也就是[0,25) :
0: new #15 // class java/io/FileInputStream
3: dup
4: ldc #17 // String 11.txt
6: invokespecial #19 // Method java/io/FileInputStream."":(Ljava/lang/String;)V
9: astore_1
10: new #22 // class java/net/ServerSocket
13: dup
14: ldc #24 // int 54199
16: invokespecial #25 // Method java/net/ServerSocket."":(I)V
19: astore_2
20: aload_2
21: invokevirtual #28 // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
24: pop
出异常了就会被由28行开始的异常捕获处理,处理的类型为 type Class java/io/FileNotFoundException.
我们会发现0-24还会被any处理,那到底0-25出异常了是被谁处理的呢?
看范围,要是0-24行io异常无法处理,那就由总异常类型来处理
第28行astore 就是把异常赋给e(FileNotFoundException e).
LineNumberTable:行号表,就是反编译的结果和源代码对应关系
例如: line 12: 0 就是源代码中第12行:
InputStream inputStream = new FileInputStream(“11.txt”);
对应 0: new #15 // class java/io/FileInputStream
通过观察含异常方法的反编译结果我们能看出每个异常处理都有
getstatic #37 // Field java/lang/System.out:Ljava/io/PrintStream;
ldc #43 // String over
invokevirtual #45 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
以上三个操作,这就finally 块里打印语句,因为任何一个异常处理完都要执行finally 块,所以finally 的内容就存入每个异常的操作中
通过分析异常总结如下:
Java字节码对于异常的处理方式:
1.统一采用异常表的方式来对异常进行处理。
2.在jdk 1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令方式。
3.当异常处理存在finally语句块时, 现代化的JVM采取的处理方式是将finally语句块的字节码拼接到每一个catch块.换句话说,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。
将捕获改为抛异常:
public class Test {
public void test() throws IOException {
InputStream inputStream = new FileInputStream("11.txt");
ServerSocket serverSocket = new ServerSocket(54199);
serverSocket.accept();
}
}
反编译结果如下:”
public void test() throws java.io.IOException;
descriptor: ()V
flags: ACC_PUBLIC
Exceptions:
throws java.io.IOException
Code:
stack=3, locals=3, args_size=1
0: new #18 // class java/io/FileInputStream
3: dup
4: ldc #20 // String 11.txt
6: invokespecial #22 // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
9: astore_1
10: new #25 // class java/net/ServerSocket
13: dup
14: ldc #27 // int 54199
16: invokespecial #28 // Method java/net/ServerSocket."<init>":(I)V
19: astore_2
20: aload_2
21: invokevirtual #31 // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
24: pop
25: return
LineNumberTable:
line 12: 0
line 14: 10
line 15: 20
line 17: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this Lcom/mec/ex/Test;
10 16 1 inputStream Ljava/io/InputStream;
20 6 2 serverSocket Ljava/net/ServerSocket;
}
我们可以看到多了个Exceptions: throws java.io.IOException对应关系,而并没有发现异常表
静态解析和动态链接:
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析;
另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为Java的多态性。
静态解析的4种情形:
1.静态方法
2.父类方法
3.构造方法
4.私有方法(无法被重写)
以上4类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的。
通过举例理解动态与静态行为
1.方法重载:
public class Text1 {
public void family(Grandpa grandpa) {
System.out.println("is Grandpa");
}
public void family(Father father) {
System.out.println("is Father");
}
public void family(Son son) {
System.out.println("is son");
}
public static void main(String[] args) {
Grandpa grandpa = new Father();
Grandpa grandpa2 = new Son();
Text1 text1 = new Text1();
text1.family(grandpa);
text1.family(grandpa2);
}
}
class Grandpa{
}
class Father extends Grandpa{
}
class Son extends Father{
}
输出结果:
is Grandpa
is Grandpa
反编译:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #44 // class com/mec/ex/Father
3: dup
4: invokespecial #46 // Method com/mec/ex/Father."<init>":()V
7: astore_1
8: new #47 // class com/mec/ex/Son
11: dup
12: invokespecial #49 // Method com/mec/ex/Son."<init>":()V
15: astore_2
16: new #1 // class com/mec/ex/Text1
19: dup
20: invokespecial #50 // Method "<init>":()V
23: astore_3
24: aload_3
25: aload_1
26: invokevirtual #51 // Method family:(Lcom/mec/ex/Grandpa;)V
29: aload_3
30: aload_2
31: invokevirtual #51 // Method family:(Lcom/mec/ex/Grandpa;)V
34: return
LineNumberTable:
line 18: 0
line 19: 8
line 21: 16
line 22: 24
line 23: 29
line 24: 34
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 args [Ljava/lang/String;
8 27 1 grandpa Lcom/mec/ex/Grandpa;
16 19 2 grandpa2 Lcom/mec/ex/Grandpa;
24 11 3 text1 Lcom/mec/ex/Text1;
}
我们会发现,尽管在new的时候调用的是对应的构造方法:
4: invokespecial #46 // Method com/mec/ex/Father."< init >": ()V
12: invokespecial #49 // Method com/mec/ex/Son."": ()V
但是在通过引用调用多次重载的family方法时还是调用的:
26: invokevirtual #51 // Method family:(Lcom/mec/ex/Grandpa;)V
31: invokevirtual #51 // Method family:(Lcom/mec/ex/Grandpa;)V
这是因为
Grandpa grandpa = new Father();
Grandpa grandpa2 = new Son();
在编译期他们的静态类型是已经被确定为Grandpa 类型了不会在变,之后的调用text1.family(grandpa);
text1.family(grandpa2);
根据这两个参数的静态类型就可以找到唯一对应的方法
方法重载是一种静态行为,在编译期可以完全确定
2.方法重写:
public static void main(String[] args) {
Father fa = new Son();
Father fa2 = new Son2();
fa.test(fa);
fa2.test(fa2);
fa = new Son2();
fa.test(fa);
}
}
class Father {
public void test(Father fa) {
System.out.println("Father");
}
}
class Son extends Father{
@Override
public void test(Father fa) {
System.out.println("Son");
}
}
class Son2 extends Father{
@Override
public void test(Father fa) {
System.out.println("Son2");
}
}
结果:
Son
Son2
Son2
反编译:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #16 // class com/mec/ex/Son
3: dup
4: invokespecial #18 // Method com/mec/ex/Son."<init>":()V
7: astore_1
8: new #19 // class com/mec/ex/Son2
11: dup
12: invokespecial #21 // Method com/mec/ex/Son2."<init>":()V
15: astore_2
16: aload_1
17: aload_1
18: invokevirtual #22 // Method com/mec/ex/Father.test:(Lcom/mec/ex/Father;)V
21: aload_2
22: aload_2
23: invokevirtual #22 // Method com/mec/ex/Father.test:(Lcom/mec/ex/Father;)V
26: new #19 // class com/mec/ex/Son2
29: dup
30: invokespecial #21 // Method com/mec/ex/Son2."<init>":()V
33: astore_1
34: aload_1
35: aload_1
36: invokevirtual #22 // Method com/mec/ex/Father.test:(Lcom/mec/ex/Father;)V
39: return
LineNumberTable:
line 6: 0
line 7: 8
line 9: 16
line 10: 21
line 12: 26
line 13: 34
line 14: 39
LocalVariableTable:
Start Length Slot Name Signature
0 40 0 args [Ljava/lang/String;
8 32 1 fa Lcom/mec/ex/Father;
16 24 2 fa2 Lcom/mec/ex/Father;
}
invokevirtual #22 // Method com/mec/ex/Father.test:()V
我们发现尽管invokevirtual 都调用的是Father的test方法,但是输出结果却是实际类型的输出结果
为什么重载和重写都是调用的父类的方法,但是输出结果却大不相同呢?
方法的接受者不同,方法重载时通过第三方类对象去调用它的方法而方法重写调用方法的对象是其本身。
在方法重载中,可以通过第三方对象和参数静态属性确定唯一一个方法,然后去执行他,Grandpa grandpa = new Father();通过text1.family(grandpa);
可以找到唯一的参数类型和方法名与之完全匹配的独一无二的方法然后调用它
而在重写中Father fa = new Son();
fa.test(fa);
会先去Father 中找,并且找到了,但是Son中也能找到完全一致的方法,也就是说test方法在Father 类和Son类都有,而且通过参数Father fa 和方法名去找,会发现Son中的方法和Father 中的方法都符合条件,从静态角度来讲在编译期无法唯一确定,那就只能在运行期通过实际类型去动态确定到底调用谁的方法,很显然调用的子类本身的方法。
通过两者比较,我们发现方法重载是静态行为,在编译期确定,
方法重写是动态行为,在运行期确定。
对一个方法进行调用时,采取先静态后动态的行为
针对于方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table, vtable),
针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table, itable)
子类中默认拥有父类的方法,不是将父类的方法复制一份而是直接将父类虚方法表中对应该方法的入口地址拿过来使用,本质用的还是父类的方法。
几个类似指令的区别:
1.invokeinterface:_ 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该接口的哪个对象的特定方法。
2. invokestatic: 调用静态方法。
3. invokespecial: 调用自己的私有方法、构造方法() 以及父类的方法。
4. invokevirtual: 调用虚方法,运行期动态查找的过程。
5. invokedynamic: 动态调用方法。
泛型擦除:
通过反射就可以证明泛型擦除问题:
我们定义的返回值为泛型是Integer类型的数组,但是可以通过反射往数组里加入String和自定义类型的元素输出也没问题,这就是因为运行期泛型擦除
很清楚的可以看到,两者泛型不同,但是他们的class其实都是一样的
看下字节码里面的new就发现了,根本就不存在泛型(第29行new和第44行new可以看出来后面都没带泛型,54行是个小插曲,就是底层把int转化成Integer)
这就很好的诠释泛型擦除问题。
但是我们如果尝试输出一下通过反射加进去非Int元素,那就会报错
同一个方法,但是把当时注释的一行输出语句去除掉注释,代码仍然是正确的,写完了起码没有任何提示错误,但是当执行时候就会出转化失败异常:
这是因为当我们企图去获取的class对象时,字节码中会有个检查语句( 87: checkcast #25 // class java/lang/Integer)去判断当前元素类型是否和泛型相同,不相同则就会出异常。