天天看點

重載和重寫在jvm運作中的差別(一)

1.重載(overload)方法 

對重載方法的調用主要看靜态類型,靜态類型是什麼類型,就調用什麼類型的參數方法。 

2.重寫(override)方法 

對重寫方法的調用主要看實際類型。實際類型如果實作了該方法則直接調用該方法,如果沒有實作,則在繼承關系中從低到高搜尋有無實作。 

3. 

java檔案的編譯過程中不存在傳統編譯的連接配接過程,一切方法調用在class檔案中存放的隻是符号引用,而不是方法在實際運作時記憶體布局中的入口位址。

基本概念

1.靜态類型與實際類型,方法接收者

1 Human man = new Man();
2 man.foo();      

上面這條語句中,man的靜态類型為Human,實際類型為Man。所謂方法接收者,就是指将要執行foo()方法的所有者(在多态中,有可能是父類Human的對象,也可能是子類Man的對象)。 

2.位元組碼的方法調用指令 

(1)invokestatic:調用靜态方法 

(2)invokespecial:調用執行個體構造器方法,私有方法和父類方法。 

(3)invokevirtual:調用所有的虛方法。 

(4)invokeinterface:調用接口方法,會在運作時再确定一個實作此接口的對象。 

(5)invokedynamic:先在運作時動态解析出調用點限定符所引用的方法,然後再執行該方法。 

前2條指令(invokestatic, invokespecial),在類加載時就能把符号引用解析為直接引用,符合這個條件的有靜态方法、執行個體構造器方法、私有方法、父類方法這4類,這4類方法叫非虛方法。 

非虛方法除了上面靜态方法、執行個體構造器方法、私有方法、父類方法這4種方法之外,還包括final方法。雖然final方法使用invokevirtual指令來調用,但是final方法無法被覆寫,沒有其他版本,無需對方法接收者進行多态選擇,或者說多态選擇的結果是唯一的。

重載overload

重載隻會發生在編譯期,即編譯器時jvm可以通過靜态類型确定符号引用所對應的直接引用。

上面說的靜态類型和動态類型都是可以變化的。靜态類型發生變化(強制類型轉換)時,對于編譯器是可知的,即編譯器知道對象的最終靜态類型。而實際類型變化(對象指向了其他對象)時,編譯器是不可知的,隻有在運作時才可知。

1 //靜态類型變化
2 sr.sayHello((Man) man);
3 sr.sayHello((Woman) man);
4 //實際類型變化
5 Human man = new Man();
6 man = new Woman();      

重載隻涉及靜态類型的選擇。 

測試代碼如下:

1 /**
 2  * Created by fan on 2016/3/28.
 3  */
 4 public class StaticDispatcher {
 5 
 6     static class Human {}
 7     static class Man extends Human {}
 8     static class Woman extends Human {}
 9 
10     public void sayHello(Human human) {
11         System.out.println("Hello guy!");
12     }
13 
14     public void sayHello(Man man) {
15         System.out.println("Hello man!");
16     }
17 
18     public void sayHello(Woman woman) {
19         System.out.println("Hello woman!");
20     }
21 
22     public static void main(String[] args) {
23         StaticDispatcher staticDispatcher = new StaticDispatcher();
24         Human man = new Man();
25         Human woman = new Woman();
26         staticDispatcher.sayHello(man);
27         staticDispatcher.sayHello(woman);
28         staticDispatcher.sayHello((Man)man);
29         staticDispatcher.sayHello((Woman)man);
30     }
31 }      

先看看執行結果: 

重載和重寫在jvm運作中的差別(一)

由此可見,當靜态類型發生變化時,會調用相應類型的方法。但是,當将Man強制類型轉換成Woman時,沒有編譯錯誤,卻有運作時異常。“classCastException”類映射異常。 

看看位元組碼指令: 

javap -verbose -c StaticDispatcher

1 public static void main(java.lang.String[]);
 2   Code:
 3    Stack=2, Locals=4, Args_size=1
 4    0:   new     #7; //class StaticDispatcher
 5    3:   dup
 6    4:   invokespecial   #8; //Method "<init>":()V
 7    7:   astore_1
 8    8:   new     #9; //class StaticDispatcher$Man
 9    11:  dup
10    12:  invokespecial   #10; //Method StaticDispatcher$Man."<init>":()V
11    15:  astore_2
12    16:  new     #11; //class StaticDispatcher$Woman
13    19:  dup
14    20:  invokespecial   #12; //Method StaticDispatcher$Woman."<init>":()V
15    23:  astore_3
16    24:  aload_1
17    25:  aload_2
18    26:  invokevirtual   #13; //Method sayHello:(LStaticDispatcher$Human;)V
19    29:  aload_1
20    30:  aload_3
21    31:  invokevirtual   #13; //Method sayHello:(LStaticDispatcher$Human;)V
22    34:  aload_1
23    35:  aload_2
24    36:  checkcast       #9; //class StaticDispatcher$Man
25    39:  invokevirtual   #14; //Method sayHello:(LStaticDispatcher$Man;)V
26    42:  aload_1
27    43:  aload_2
28    44:  checkcast       #11; //class StaticDispatcher$Woman
29    47:  invokevirtual   #15; //Method sayHello:(LStaticDispatcher$Woman;)V
30    50:  return      

看到,在強制類型轉換時,會有指令checkCast的調用,而且invokevirtual指令的調用方法也發生了變化​

​39: invokevirtual #14; //Method sayHello:(LStaticDispatcher$Man;)V​

​ 。 

虛拟機(準确說是編譯器)在重載時是通過參數的靜态類型而不是實際類型作為判定依據的。 

對于字面量類型,編譯器會自動進行類型轉換。轉換的順序為: 

char-int-long-float-double-Character-Serializable-Object 

轉換成Character是因為發生了自動裝箱,轉換成Serializable是因為Character實作了Serializable接口。

重寫override

重寫發生在運作期,在運作時jvm會先判斷對象的動态類型,而後根據對象的動态類型選擇對應vtable,進而根據符号引用找到對應的直接引用。

如:

BaseClass c = new ChildClass();

則c能通路的函數清單為Method1,Method2,即黃色部分。

重載和重寫在jvm運作中的差別(一)
1 /**
 2  * Created by fan on 2016/3/29.
 3  */
 4 public class DynamicDispatcher {
 5 
 6     static abstract class Human {
 7         protected abstract void sayHello();
 8     }
 9 
10     static class Man extends Human {
11 
12         @Override
13         protected void sayHello() {
14             System.out.println("Man say hello");
15         }
16     }
17 
18     static class Woman extends Human {
19 
20         @Override
21         protected void sayHello() {
22             System.out.println("Woman say hello");
23         }
24     }
25 
26     public static void main(String[] args) {
27         Human man = new Man();
28         Human woman = new Woman();
29         man.sayHello();
30         woman.sayHello();
31         man = new Woman();
32         man.sayHello();
33     }
34 
35 }      

執行結果: 

重載和重寫在jvm運作中的差別(一)

看下位元組碼指令:

1 public static void main(java.lang.String[]);
 2   Code:
 3    Stack=2, Locals=3, Args_size=1
 4    0:   new     #2; //class DynamicDispatcher$Man
 5    3:   dup
 6    4:   invokespecial   #3; //Method DynamicDispatcher$Man."<init>":()V
 7    7:   astore_1
 8    8:   new     #4; //class DynamicDispatcher$Woman
 9    11:  dup
10    12:  invokespecial   #5; //Method DynamicDispatcher$Woman."<init>":()V
11    15:  astore_2
12    16:  aload_1
13    17:  invokevirtual   #6; //Method DynamicDispatcher$Human.sayHello:()V
14    20:  aload_2
15    21:  invokevirtual   #6; //Method DynamicDispatcher$Human.sayHello:()V
16    24:  new     #4; //class DynamicDispatcher$Woman
17    27:  dup
18    28:  invokespecial   #5; //Method DynamicDispatcher$Woman."<init>":()V
19    31:  astore_1
20    32:  aload_1
21    33:  invokevirtual   #6; //Method DynamicDispatcher$Human.sayHello:()V
22    36:  return      

從位元組碼中可以看到,他們調用的都是相同的方法​

​invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V​

​ ,但是執行的結果卻顯示調用了不同的方法。因為,在編譯階段,編譯器隻知道對象的靜态類型,而不知道實際類型,是以在class檔案中隻能确定要調用父類的方法。但是在執行時卻會判斷對象的實際類型。如果實際類型實作這個方法,則直接調用,如果沒有實作,則按照繼承關系從下往上一次檢索,隻要檢索到就調用,如果始終沒有檢索到,則抛異常(難道能編譯通過)。

(1)測試代碼如下:

1 /**
 2  * Created by fan on 2016/3/29.
 3  */
 4 public class Test {
 5 
 6     static class Human {
 7         protected void sayHello() {
 8             System.out.println("Human say hello");
 9         }
10         protected void sayHehe() {
11             System.out.println("Human say hehe");
12         }
13     }
14 
15     static class Man extends Human {
16 
17         @Override
18         protected void sayHello() {
19             System.out.println("Man say hello");
20         }
21 
22 //        protected void sayHehe() {
23 //            System.out.println("Man say hehe");
24 //        }
25     }
26 
27     static class Woman extends Human {
28 
29         @Override
30         protected void sayHello() {
31             System.out.println("Woman say hello");
32         }
33 
34 //        protected void sayHehe() {
35 //            System.out.println("Woman say hehe");
36 //        }
37     }
38 
39     public static void main(String[] args) {
40         Human man = new Man();
41         man.sayHehe();
42     }
43 
44 }      

測試結果如下: 

重載和重寫在jvm運作中的差別(一)

位元組碼指令:

1 public static void main(java.lang.String[]);
 2   Code:
 3    Stack=2, Locals=2, Args_size=1
 4    0:   new     #2; //class Test$Man
 5    3:   dup
 6    4:   invokespecial   #3; //Method Test$Man."<init>":()V
 7    7:   astore_1
 8    8:   aload_1
 9    9:   invokevirtual   #4; //Method Test$Human.sayHehe:()V
10    12:  return      

位元組碼指令與上面代碼的位元組碼指令沒有本質差別。

(2)測試代碼如下:

1 /**
 2  * Created by fan on 2016/3/29.
 3  */
 4 public class Test {
 5 
 6     static class Human {
 7         protected void sayHello() {
 8         }
 9     }
10 
11     static class Man extends Human {
12 
13         @Override
14         protected void sayHello() {
15             System.out.println("Man say hello");
16         }
17 
18         protected void sayHehe() {
19             System.out.println("Man say hehe");
20         }
21     }
22 
23     static class Woman extends Human {
24 
25         @Override
26         protected void sayHello() {
27             System.out.println("Woman say hello");
28         }
29 
30         protected void sayHehe() {
31             System.out.println("Woman say hehe");
32         }
33     }
34 
35     public static void main(String[] args) {
36         Human man = new Man();
37         man.sayHehe();
38     }
39 
40 }      

編譯時報錯: 

重載和重寫在jvm運作中的差別(一)

這個例子說明了:Java編譯器是基于靜态類型進行檢查的。

修改上面錯誤代碼,如下所示:

1 /**
 2  * Created by fan on 2016/3/29.
 3  */
 4 public class Test {
 5 
 6     static class Human {
 7         protected void sayHello() {
 8             System.out.println("Human say hello");
 9         }
10 //        protected void sayHehe() {
11 //            System.out.println("Human say hehe");
12 //        }
13     }
14 
15     static class Man extends Human {
16 
17         @Override
18         protected void sayHello() {
19             System.out.println("Man say hello");
20         }
21 
22         protected void sayHehe() {
23             System.out.println("Man say hehe");
24         }
25     }
26 
27     static class Woman extends Human {
28 
29         @Override
30         protected void sayHello() {
31             System.out.println("Woman say hello");
32         }
33 
34         protected void sayHehe() {
35             System.out.println("Woman say hehe");
36         }
37     }
38 
39     public static void main(String[] args) {
40         Man man = new Man();
41         man.sayHehe();
42     }
43 
44 }      

注意在Main方法中,改成了​

​Man man = new Man();​

​ 

執行結果如下所示: 

重載和重寫在jvm運作中的差別(一)

位元組碼指令如下所示:

1 public static void main(java.lang.String[]);
 2   Code:
 3    Stack=2, Locals=2, Args_size=1
 4    0:   new     #2; //class Test$Man
 5    3:   dup
 6    4:   invokespecial   #3; //Method Test$Man."<init>":()V
 7    7:   astore_1
 8    8:   aload_1
 9    9:   invokevirtual   #4; //Method Test$Man.sayHehe:()V
10    12:  return      

注意上面的位元組碼指令​

​invokevirtual #4; //Method Test$Man.sayHehe:()V​

​ 。