《Java核心技術》第10版讀書筆記之Chap5(2)——方法呼叫過程、final、型別轉換、abstract與訪問識別符號
方法呼叫過程
假設在原始碼中有這樣一行:
manager.setBonus(2300);
下面來看看javac編譯器是如何處理的:
- 檢查根據物件型別和函式名稱,在該類成員方法及其父類中有呼叫權的成員方法中尋找到所有名字匹配的方法。在本例中,manager的型別,假定為CManager類,其父類為CEmployee。編譯器會枚舉出CManager類中名為setBonus的方法以及CEmployee中名為setBonus的public方法(注:父類的私有方法子類不可訪問)
- 從中挑選出引數個數匹配、型別最為吻合的(優先考慮型別匹配,然後是經過強制轉換後可以匹配的),如果找不到或者找到多個都報編譯時錯誤。
- 找到最佳匹配方法後,根據方法的修飾關鍵字決定是動態繫結(執行時查物件方法表決定)還是靜態繫結(直接生成呼叫函式的指令)
- 如果該方法由private、static或final關鍵字修飾,或者是構造方法,則生成靜態繫結程式碼;
- 其餘情況下,生成動態繫結程式碼,在執行時通過物件例項的引用找到其對應的方法表(類似C++的虛擬函式表)。
final關鍵字
final關鍵字可以用於修飾類、成員方法和成員變數,其主要用途:
- 修飾一個類,表明該類不可再被繼承;
- 修飾成員方法,表明該方法不可被子類覆寫(override)
- 修飾成員變數,表明該變數的值在類物件建立後不能被改動
final關鍵字修飾類時,該類不再允許被繼承(即該類就為最終類)
public final class CChild extends CBase {
//...
}
final關鍵字修飾成員方法時,則該類的子類中無法再覆寫(override)該方法。
public class CBase {
public final void setID(int id) {
//...
}
}
在此例中,CBase的任何子類都不能對setID方法進行覆寫了。如果某個類中有一個名為func1的final方法,我們來進行如下分析:
其父類中若有func1方法且沒設定final屬性,雖然其子類的func1()方法被設定了final屬性,但為了實現多型效果,依然會使用invokevirtual(按虛擬函式呼叫)的方式進行呼叫。
其子類不可能覆寫該方法,所以可以在編譯器靜態繫結。雖然理論上如此,但Java8 SE中依然使用的是invokevirtual(按虛擬函式呼叫)的方式進行呼叫。
public class CNo1 { public final void func4() { System.out.println("CNo1::func4"); } } public class CDemo1 { public static void main(String[] args) { CNo1 n = new CNo1(); n.func4(); } } //javap看到的結果 Code: stack=2, locals=2, args_size=1 0: new #2 // class CNo1 3: dup 4: invokespecial #3 // Method CNo1."<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #4 // Method CNo1.func4:()V 12: return
final關鍵字修飾成員變數時,表示該變數在物件構造後就不允許被改變了。
public class CTest {
private static final double PI = 3.141592653;
//...
}
final類中的所有方法將自動成為final方法,但其中的成員變數不會自動變成final型別變數。
型別轉換
在型別轉換方面,Java拋棄了C++定義的那套複雜的語法,返璞歸真的使用了C的型別轉換形式:
型別1 名稱 = (型別1) 其他型別的物件;
一般只有兩類轉換是正常的:
- 數值型別的轉換(如浮點型轉換為整型)
- 有繼承關係類間的轉換
關於向上和向下轉型
- 所謂向上轉型,即將子類物件轉換成父類物件。
- 這是實現多型的一種常見手法。
- 在Java中,此類形式無需使用顯示型別轉換。
- 所謂向下轉型,即將父類物件轉換成子類物件。
- 這中情況可能存在危險:如果被轉換的物件真的是父類的,轉換後呼叫子類特有的方法或訪問子類特有的成員變數,則越界了。
- 在Java中,必須顯示的使用型別轉換的語法表明編寫者瞭解該風險並堅持使用。
- 即便使用型別轉換語法瞞過了編譯器,在執行時執行該轉換時也會被JVM發現並丟擲ClassCastException異常
- 通常,向下轉型僅用於以下情景:恢復子類物件所具有的身份。即一個子類物件可能由於實現多型等考量被向上轉型為了父類的型別。而一旦再次使用子類特有的成員方法或成員變數時,才可以用向下轉型的方法為其“平反昭雪”,恢復子類型別的身份。
- 通常而言,不要使用向下轉型,即便是為了給子類“平反昭雪”。因為通常這意味著類的繼承關係設定的不合理。一個設計良好的繼承關係框架應該不需要任何的“平反昭雪”。
為了保證型別轉換的安全,JVM也提供了類似C++的RTTI機制,判明轉換是否能成功:
boolean bRet = 類物件 instanceof 類;
只有bRet返回true時,才能安全地進行轉型。注意,如果類物件為null,則表示式返回false。
再強調一次,向下轉型不是一個好的設計,應該避免使用!
與C++向下轉型的對比
Manager* boss = dynamic_cast<Manager*>(staff);
if (boss)
{
//轉型成功,進行後續操作...
}
C++是將轉型與判斷過程都集中在dynamic_cast過程中了,如果不成功,返回的boss為NULL,否則為物件的指標。
if (staff instanceof Manager) {
Manager boss = (Manager) staff;
//執行後續操作
}
Java中轉換是否能成功的判定由instanceof完成,而型別轉換則會直接進行轉換,如果轉換失敗,要麼是編譯時報錯,要麼執行時拋異常。所以在轉換前,必須先用instanceof投石問路。
abstract關鍵字與抽象類
在C++裡有純虛擬函式的概念,而包含純虛擬函式的類無法例項化。在Java中,也有類似的語法:用abstract關鍵字修飾成員方法,則該方法成為一個抽象方法,抽象方法無需有實現,只要有宣告即可。注意:包含抽象方法的類必須也要加上abstract關鍵字成為抽象類。
以著名的Shape為例,世界上有圓形、矩形等等,他們都是圖形,但誰也無法定義“圖形”,因為這是一個抽象的概念,因此,Shape類無需例項化。但所有圖形都有一個draw的行為,即在畫布上畫出自己,因此考慮將其作為Shape類的成員方法。但由於Shape類是個抽象類,因此也沒必要實現draw的動作,故該方法在Shape類中被定義為了抽象方法。
public abstract class Shape {
public abstract boolean draw(Cloth cloth);
}
需要注意的是:
- 抽象類不能例項化物件,但依然可以用該型別引用子類物件從而實現多型。如:
ArrayList<Shape> shapelists = new ArrayList<Shape>();
shapelists.add(new Circle(3, 2, 1));
shapelists.add(new Rectangle(6, 6, 4, 2));
//...
for (Shape currentShape : shapelists) {
currentShape.draw(cloth);
}
- 在抽象類中依然可以定義成員變數和非抽象的成員函式。
- 在繼承抽象類後,可選擇實現其中全部的抽象方法,此後,該子類可以被例項化。
- 在繼承抽象類後,也可以選擇不實現其中的某些抽象方法,這樣,該子類也需被定義為抽象類,並且不能被例項化。
- 即便一個類中沒有抽象方法,也可以被定義為抽象類,此時,該類無法被例項化。
再議訪問標識
在C++中,訪問標識要和繼承型別結合起來,所以一共有9種情況。記得某一年期末考試題還考這玩意兒…
而到了Java中,由於繼承型別全部是公有繼承了,再配合三個關鍵字,一共實現了4種類型。三個關鍵字怎麼實現了4種訪問控制級別呢?當然是不加這三個關鍵字預設有一種嘍。
- private:僅類內可見,繼承後的子類都不可見。
- public:到處都可見
- protected:在類內、所有子類(只有公有繼承)和包內都可見
- 預設(神馬都不加):包內可見