笔者从事java开发,所以下文中的内容是基于java语言,但是语言具有共同性,其它的语言也可以借鉴
先进行一个测试
public class Test {
static class A{
public String format(A b){
return "A And A";
}
public String format(D b){
return "A And D";
}
}
static class B extends A{
public String format(A b){
return "B And A";
}
public String format(B b){
return "B And D";
}
}
static class C extends B{ }
static class D extends C{ }
public static void main(String[] args) {
A a = new B();
B b = new B();
B c = new C();
System.out.println(a.format( b ));
System.out.println(a.format( c ));
}
}
答案是
B And A
B And A
如果答成 A and A, A And A
的话,说明你对重写理解有问题,如果答成B And A,B And B
的话,说明对重载理解有问题。如果答成其它的答案,那么说明你必须要看此文了。
如果你的结果与正确答案一致,那么恭喜你,你已经理解了重载与重写。如需了解实现机理,可以直接跳到实现机制一节。
理解重载与重写
重载与重写,可以理解成静态绑定与动态绑定 (认为在java中只有statcic,final,private的方法才是静态绑定,而其它方法都是通过通过 invokevirtual实现的读者,此者可以理解成根据方法签名绑定与根据实例绑定(原谅我自己造的这两个词))。
下面作两者对一对比
项目 | 重载 | 重写 |
---|---|---|
英文解释 | overLoad | override(java中常用) |
调用方法确定时机 | 编译期 | 运行期 |
方法之间的关系 | 在同一个类中 | 在父子类中 |
方法签名 | 必须不一致 | 必须一致 |
重载与重写的实现机制
重载
重载,方法签名(函数名及参数类型及个数)必须不一样,不同的重载方法属于同一个类,调用方与被调用方的匹配依据就是方法签名。
比如类A 编译后,其方法列表有 public String format(A), public String format(D),
(当然真实的编译后的结果不是这样的,为了简单,此处仅以函数名表示)。 在main函数中,通过类A进行调,参数类型分别为B类及C 类,在编译的时候,会去类A中查找相匹配的方法,因为类A中没有 public String format(B) 及public String format(C)
,因此通过向上转型为A类,通过调用public String format(A)
实现,即调用的时候,调用的方法签名是public String format(A)
,验证见下。
将Test类进行反编译,执行javap -c Test.class
. 可得到该类的反编译后的结果,看第29行与第40行,可以看到调用的时候传入的类型参数为A
。
public class com.service.Test {
public com.service.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/service/Test$B
3: dup
4: invokespecial #3 // Method com/service/Test$B."<init>":()V
7: astore_1
8: new #2 // class com/service/Test$B
11: dup
12: invokespecial #3 // Method com/service/Test$B."<init>":()V
15: astore_2
16: new #4 // class com/service/Test$C
19: dup
20: invokespecial #5 // Method com/service/Test$C."<init>":()V
23: astore_3
24: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
27: aload_1
28: aload_2
29: invokevirtual #7 // Method com/service/Test$A.format:(Lcom/service/Test$A;)Ljava/lang/String;
32: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
38: aload_1
39: aload_3
40: invokevirtual #7 // Method com/service/Test$A.format:(Lcom/service/Test$A;)Ljava/lang/String;
43: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: return
}
重写
重写实现的机制是方法表,即每一、个类都有对应的方法表,方法表指向具体的方法实现。
对于上述的类A与类B的方法表可参考如下。(为简单可见,忽略了Object类的toString等方法)
左边是方法表,右边是方法的具体实现, 上面是类A,下面是类B。
因此,我们可以看出,一个类如果重写了父类的方法,则该类的方法区会有具体的实现,并且方法表的索引会指向该实现。如果没有重写父类的方法,则方法表中的索引会指向父类的实现方法。因此,当某对象在调用的时候,只需要查看自己方法表中的数据,即可索引到正确的方法上去。
对测试case的解释
回到最初的测试案例,对于在main函数中声明的对象a
来讲,是B
类对象,因此其执行的依法是从B
方法表中寻找相应的方法。
在调用a.format(b) 或 a.format(c)
时,由于在编译器是通过A
类对象调用的,在A
类中,没有找到参数类型为B
类或C
类的format
方法,因此通过向上转型,找到可以执行的方法是 format(A a)
,因此将调用代码编译为format(A)
(上面反编译中已说明)。
因此最终会在B类的方法表中寻找 public String format(A)
的方法,因此输出了
B And A
B and A