1. 第19章 方法的調用與返回
1.1 關於構造方法那點事
關於構造方法調用的總結性說明:
l 在一個類中,如果不定義構造方法,則編譯器會默認生成一個無參構造方法;
l 在構造方法中,如果想要調用其他的構造方法(用this調用本類的其它構造方法,用super調用父類的構造方法),必須放在構造方法的第一行;
l 在構造方法中,如果不顯式地調用其它的構造方法,則默認在執行構造方法中任何代碼之前,會先調用父類的默認無參構造方法;
l 注意,在子類的構造方法中,必須調用父類的構造方法(不管是隱式的調用,還是顯式的調用,也不管調用的是無參構造方法,還是有參構造方法),如果不調用,則編譯器將會報錯!
首先來看構造方法的調用。
在一個類中,如果不定義構造方法,則編譯器會默認生成一個無參構造方法。
//此類沒有定義任何構造方法,在編譯之後,編譯器將自動生成一個構造方法 public class ConstructorTest01 {
}
|
用jClassLib觀察,可以看到生成的類文件中有一個<init>方法,它就是無參的構造方法。
此無參構造方法對應的字節碼如下:
0 aload_0 把局部變量區中第0個元素(一個對象引用)壓入操作數棧 1 invokespecial #8 <java/lang/Object.<init>> 調用父類的無參構造方法 4 return 返回 |
可以看到,當我們調用這個構造方法,它將自動調用父類的無參構造方法。
現在,我們自己親自定義這個類的構造方法,如下:
public class ConstructorTest01 {
public ConstructorTest01() {
}
} |
再來觀察這個類的構造方法所對應的字節碼:
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 return |
從上可以看出,不管我們定不定義這個構造方法,字節碼是一樣的。
再看,下面我們在構造方法中增加一個super()調用。
public class ConstructorTest01 { public ConstructorTest01() { super(); } } |
再次觀察生成的字節碼,可以看到,是完全和前面一樣的!
現在,我們在這個構造方法內部乾點事,比如:
public class ConstructorTest01 { public ConstructorTest01() { int i = 1 + 2; } } |
那麼,現在的字節碼將會如何?
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 iconst_3 5 istore_1 6 return |
可以看到,首先是調用父類的構造方法,然後是執行其它的指令。
假如我們企圖改變這個缺省的過程(即在構造方法內部是先調用父類的構造方法,然後再執行其它的指令這個過程),那麼編譯器將會編譯報錯,無法編譯通過,比如下面的代碼是無法編譯通過的:
public class ConstructorTest01 { public ConstructorTest01() { int i = 1 + 2; super(); } } |
super()不能放到其它代碼的後面!
假設我們有多個構造方法(構造方法重載),那麼每個構造方法裏面,不管你有沒有顯式地調用super(),它們都會默認執行父類的無參構造方法!
比如:
public class ConstructorTest01 { public ConstructorTest01() { int i = 1 + 2; }
public ConstructorTest01(int i){ int b = i + 10; } }
|
我們看這二個構造方法的字節碼:
第一個構造方法的字節碼是:
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 iconst_3 5 istore_1 6 return |
第二個構造方法的字節碼是:
0 aload_0 1 invokespecial #8 <java/lang/Object.<init>> 4 iload_1 5 bipush 10 7 iadd 8 istore_2 9 return |
可以看到依然調用了父類的默認無參構造方法。
假設我們在其中一個構造方法裏面調用另外一個構造方法,則在第一個構造方法中就不會調用父類的構造方法了(但第二個構造方法依然會調用父類的構造方法)。如下:
public class ConstructorTest01 { public ConstructorTest01() { this(50); int i = 1 + 2; }
public ConstructorTest01(int i){ int b = i + 10; } } |
第一個構造方法的字節碼如下:
0 aload_0 1 bipush 50 3 invokespecial #8 <cn/com/leadfar/ConstructorTest01.<init>> 6 iconst_3 7 istore_1 8 return |
我們看,上面這個字節碼,它不再調用父類(Object類)中的構造方法!
第二個構造方法的字節碼如下:
0 aload_0 1 invokespecial #17 <java/lang/Object.<init>> 4 iload_1 5 bipush 10 7 iadd 8 istore_2 9 return |
我們可以看到,這第二個構造方法依然調用了父類的默認無參構造方法!
假如父類沒有無參構造方法,而只提供了有參構造方法的話,對於子類來說,必須顯式地調用這個構造方法!
假如父類代碼如下:
public class ConstructorTest01Parent { public ConstructorTest01Parent(String s){ } } |
那麼如下代碼在編譯時將不會通過:
public class ConstructorTest01Child extends ConstructorTest01Parent { } |
剛剛說過,子類必須調用父類的構造方法,現在父類只有一個帶參數的構造方法(無參構造方法就沒有了!)而子類沒有定義構造方法,所以缺省情況下,子類有一個無參構造方法,而且子類的這個無參構造方法,默認將調用父類的無參構造方法,現在父類沒有無參構造方法,所以上述子類在編譯時將會通不過!
把子類代碼改成下面這樣,也依然是不可以的:
public class ConstructorTest01Child extends ConstructorTest01Parent { public ConstructorTest01Child(String s) { } } |
原因很清楚,我們在構造方法代碼中沒有顯式調用任何其它構造方法,所以默認情況下,編譯器會在這個構造方法的第一行插入調用父類默認無參構造方法的代碼,而父類是沒有這個構造方法的,所以,這段代碼編譯不會通過!
下面的代碼纔是正確的:
public class ConstructorTest01Child extends ConstructorTest01Parent { public ConstructorTest01Child() { //因爲父類沒有無參構造方法,所以 //必須顯式的調用父類的構造方法 super(""); } } |
下面的代碼也是正確的:
public class ConstructorTest01Child extends ConstructorTest01Parent { public ConstructorTest01Child(String s) { //因爲父類沒有無參構造方法,所以 //必須顯式的調用父類的構造方法 super(s); } } |
或者下面的代碼:
public class ConstructorTest01 extends ConstructorTest01Parent{ public ConstructorTest01() { this(50); int i = 1 + 2; }
public ConstructorTest01(int i){ super("kkk"); //此行是必須的,因爲父類已經沒有默認構造方法了 int b = i + 10; } } |
當我們調用new關鍵字創建一個對象的時候,需要指定相應的構造方法:
public class ConstructorTest01Client { public static void main(String[] args) { ConstructorTest01 ct01 = new ConstructorTest01(); } } |
上述代碼中,首先創建一個對象,然後把這個對象的引用賦給一個局部變量ct01。
0 new #16 <cn/com/leadfar/ConstructorTest01> 首先在堆中創建了一個對象,並把對象引用壓入操作數棧 3 dup 複製操作數棧頂的數據(這樣操作數棧中有兩個相同的對象引用,一個用於調用其構造方法,另外一個) 4 invokespecial #18 <cn/com/leadfar/ConstructorTest01.<init>> 彈出棧頂的對象引用,並調用其構造方法(構造方法沒有返回值) 7 astore_1 彈出棧頂的對象引用,並存儲在局部變量區索引號爲1的地方(索引號爲0的地方是main方法的參數args的引用) 8 return |
new/dup/invokespecial,是new關鍵字對應的三個指令。
如下例子:
public static void main(String[] args) { MethodCall mc = new MethodCall();
//調用沒有返回值的方法 mc.noReturnValueMethodCall();
//調用有返回值的方法 int i = mc.withIntReturnValueMethodCall();
System.out.println(i); } |
其字節碼是:
0 new #16 <cn/com/leadfar/MethodCall> 3 dup 4 invokespecial #18 <cn/com/leadfar/MethodCall.<init>> 7 astore_1 8 aload_1 9 invokevirtual #19 <cn/com/leadfar/MethodCall.noReturnValueMethodCall> 12 aload_1 13 invokevirtual #22 <cn/com/leadfar/MethodCall.withIntReturnValueMethodCall> 16 istore_2 17 getstatic #26 <java/lang/System.out> 20 iload_2 21 invokevirtual #32 <java/io/PrintStream.println> 24 return |
1.2 實例方法調用
invokespecial和invokevirtual這兩個指令說明了多態的具體實現行爲的一切。如果是invokespecial,則表示調用的是哪個具體的方法,在編譯時已經確定;如果是invokevirtual,表示具體調用的哪個方法,還得看運行時刻(動態綁定)。