Thinking In Java Part04(final/多態)

1、空白final
	Java允許生成“空白final”,所謂空白final是指被聲明爲final但又未給定初值的域。無論什麼 情況,編譯器都確保空白final在使用前被初始化。但是,空白final在關鍵字final的使用上提供了更大的靈活性,爲此,一個類中的final域可以做到根據對象而有所不同,卻又保持其恆大不變的特性。
2、final方法
	使用final方法的原因有2個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義。這是出於設計的考慮:想要確保在繼承中使方法行爲保持不變,並且不會被覆蓋。
	過去建議使用final方法的第二個原因是效率。在Java的早期實現中,如果將一個方法指明爲final,就是同意編譯器將針對該方法的所有調用都轉爲內嵌調用。當編譯器發現一個final方法調用命令時,它會根據自己的謹慎判斷,跳過插入程序代碼這種正常方式而執行方法調用機制(將參數壓入棧,跳至方法代碼處並執行,然後跳回並清理棧中的參數,處理返回值),並且以方法體中的實際代碼的副本來替代方法調用。這將消除方法調用的開銷。當然 ,如果一個方法很大,你的程序代碼會膨脹,因而可能看不到內嵌帶來的任何性能提高,因爲,所帶來的性能提高會因爲花費於方法內的時間量而被縮減。
	虛擬機(特別是hotspot技術)可以探測這些情況,並優化去掉這些效率反而降低的額外的內嵌調用。因而不再需要使用final方法來進行優化。在使用Java SE 5/6時,應該讓編譯器和JVM去處理效率問題,只有在想要明確禁止覆蓋時,纔將方法設置爲final的。
3、final和private關鍵字
	類中所有的private方法都隱式指定爲是final的。由於無法取用private方法,所以也就無法覆蓋它,可以對private方法添加final修飾詞,但這並不能給該方法增加任何額外的意義。
4、覆蓋
	覆蓋只在某方法是基類的接口的一部分時纔出現。必須能將一個對象向上轉型爲它的基本類型並調用相同的方法。如果某方法爲private,他就不是基類的接口的一部分。它僅是一些隱藏於類中的程序代碼,只不過是具有相同的名稱而已。如果在導出類中以相同的每次生成一個public、protected或包訪問權限方法的話,該方法就不會產生在基類中出現的“僅具有相同名稱”的情況。此時你並沒有覆蓋該方法,僅是生成了一個新的方法。由於private無法觸及而且能有效隱藏,所有除了把它看成是因爲它所歸屬的類的組織結構的原因而存在外,其他任何事物都不需要考慮它。
5、final類 沒有子類
	當將某個類的整體定義爲final時(通過將關鍵字置於它的定義之前),就表名了你不打算繼承該類,而且也不允許別人這樣做。換句話說,你對該類的設計永不需要做任何變動,或者出於安全的考慮,你不希望它有子類。
	final類中可以給方法添加final修飾詞,但這不會增添任何意義。
6、有關final的忠告
	如果將一個方法指定爲final,可能會妨礙其他程序員在項目中通過繼承來複用你的類,而這知識因爲你沒有想到它會以那種方式被運用。
	Java標準庫中的Java 1.0/1.1中Vector被廣泛運用,人們可能會想要繼承並覆蓋如此繼承並好用的類,但是設計者認爲這不太合適。這裏有2個原因。第一:stack繼承自Vector,就是說Stack是個Vector,這從邏輯的的觀點看是不正確的。儘管如此,Java的設計者們自己仍舊繼承了Vector。在以這種方式創建Stack時,他們意識到了Final方法顯得過於嚴苛了。
	第二個原因:Vector的許多最重要的方法如addElement()和elementAt()是同步的。這將導致很大的執行開銷,可能會抹殺final所帶來的好處。這種情況增強了人們關於程序員無法正確猜測優化應當發生於何處的觀點。幸運的是我們現代Java容器庫用ArrayList替代了Vector。
	hashTable也是一個重要的java1.0/1.1標準類庫,而且不含任何final方法,方法相對於Vector中的方法要簡潔很多。對於使用者來說,這是一個本不該如此輕率的事物,這種不規則的情況只能使用戶付出更多的努力。現代Java的容器用HashMap替代了HashTable
7、多態
	在面向對象的程序設計語言中,多態是繼數據抽象和繼承之後的第三種基本特徵。
	多態通過分離做什麼和怎麼做,從另一角度將接口和實現分離開來。多態不但能夠改善代碼的組織結構和可讀性,還能創建可擴展的程序————即無論在項目最初創建時還是在需要添加新功能時都可以“生長”的程序。。
	“封裝”通過合併特徵和行爲來創建新的數據類型。“實現隱藏”則通過將細節“私有化”把接口和實現分離開來。而多態的作用則是消除類型之間的耦合關係。繼承允許將對象視爲它自己本身的類型或其基類型來加以處理。它允許將多種類型(從同一基類導出的)視爲同一類型來處理,而同一份代碼也就可以毫無差別地允許在這些不同類型之上了。多態方法調用允許一種類型表現出與其他類型之間的區別,只要它們都是從同一基類導出的。這種區別是根據方法行爲的不同而表示出來的,雖然這些方法都可以通過一個基類來調用。
	多態 也稱作動態綁定、後期綁定或運行時綁定。
8、方法調用綁定
	將一個方法調用同一個方法主體關聯起來被稱作綁定。若在程序執行前進行綁定(如果有的話,由編譯器和連接程序實現),叫做前期綁定。C只有一種方法調用就是前期綁定。
	爲了解決具體調用哪一個子類的方法,解決的辦法爲後期綁定,它的含義就是 在運行時根據對象的類型進行綁定。後期綁定 也叫做動態綁定或運行時綁定。後期綁定需要在對象中安置某種“類型信息”來找到正確的方法體,並加一調用。
	Java中除了static方法和final方法(private屬於final方法)之外,其他都是後期綁定。通常情況下,我們不必判斷釋放應該進行後期綁定——它會自動發生。
	爲什麼要將某個方法聲明爲final?它不僅可以防止其他人覆蓋該方法。更重要的一點或許是:這樣可以有效地“關閉”動態綁定,或者說告訴編譯器不需要對其進行動態綁定。這樣,編譯器可以爲final方法調用生成更有效的代碼。然而,大多數情況下對程序的整體性能不會有什麼改觀,最好根據設計而不是實體提高性能的目的來使用final。
9、多態的缺陷:“覆蓋”私有方法
	由於private方法被自動認爲是final方法,而且對導出類是屏蔽的。因此,在這種情況下,子類中的private方法就是一個全新的方法;既然基類的private方法在子類中不可見,因此甚至也不能被重載【1.8編譯報錯】
	只有非private方法纔可以被覆蓋,但是還需要密切注意覆蓋private方法的現象。在導出類中,對於基類中的private方法,最好採用不同的名字。
10、多態的缺陷:域與靜態方法
	只有普通的方法調用是可以多態的。如果直接訪問某個域,這個訪問就將在編譯器進行解析。
	任何域訪問操作都將由編譯器解析,因此不是多態的。如果同一個域變量在父分類和子類同時存在,子類在多態方式創建的會會同時擁有父類和子類的域變量。爲了得到父類的域變量,必須顯式指明super.域對象。
	儘管看起來容易混淆,但是在實踐中,我們通常會將所有的域設置成private,因此不能直接訪問它們,其副作用是隻能調用方法 訪問它們。而且我們不會對基類中的域和導出類的域賦予相同的名字,這種做法容易令人混淆。
	如果某個方法方法是靜態的,它的行爲就不具有多態性。靜態方法是與類,而並非與單個的對象相關聯的。
11、構造器和多態
	通常構造器不同於其他種類的方法。構造器並不具有多態性(他們實際上是static方法,只不過static聲明是隱式)。
12、構造器的調用順序
	基類的構造器總是在導出類的構造過程中被調用,而且按照繼承層次逐漸向上鏈接,以使每個基類的構造器都能得到調用。這樣做是有意義的,因爲構造器具有一項特殊任務:檢查對象是否被正確地構造。導出類只能訪問它自己的成員,不能訪問基類的成員(基類成員通常是private類型)。只有基類的構造器才具有恰當的知識和權限來當自己的元素進行初始化。因此,必須令所有構造器都得到調用,否則就不可能正確地構造完整對象。這正是編譯器爲什麼要強制每個導出類部分都必須調用構造器的原因。在導出類的構造器主體中,如果沒有明確指定調用某個基類構造器,他就會默默地調用默認構造器。如果不存在默認構造器,編譯器會報錯(若某個類沒有構造器,編譯器會自動合成一個默認構造器)
	調用構造器的順序:
		12.1、調用基類構造器。這個步驟會不斷低反覆遞歸下去,首先是構造這種層次結構的根,然後是下一層導出類,等等,直到最底層的導出類。
		12.2、按聲明順序調用成員的初始化方法
		12.3、調用導出類構造器的主體。
	構造器的調用順序是很重要的。當進行繼承時,我們已經知道基類的一切,並且可以訪問基類中任何聲明爲public和protected的成員。這意味着在導出類中,必須假定基類的所有成員都是有效的。一種標準方法是,構造動作一經發生,那麼對象所有部分的全體成員都會得到構建。然後,在構造器內部,我們必須確保所要使用的成員都已經構建完畢。爲確保這一目的,唯一的辦法就是首先調用基類構造器。那麼在進入導出類構造器時,在基類中可供我們訪問的成員都已經得到初始化。此外,知道構造器 中所有成員都有效也是因爲,當成員對象在類內進行定義的時候,只要有可能,就應該對它們進行初始化(也就是,通過組合方法將對象置於類內)。若遵循這一規則,那麼就能保證所有基類成員已經當前對象的成員對象都被初始化了。但遺憾的是,這一做法不適用於所有情況。
13、構造器內部的多態方法的行爲
	在一般的方法內部,動態綁定的調用是在運行時才決定的,因爲對象無法知道它是屬於方法按所在的那個類,還是屬於那個類的導出類。
	如果要調用構造器內部的一個動態綁定方法,就要用到那個方法被覆蓋後的定義。然後,這個調用的效果可能難於預料,因爲被覆蓋的方法的對象被完全構造之前就會被調用。這可能會造成一些難於發現的隱藏錯誤。
	構造器的工作事件上是創建對象。在任何構造器內部,整個對象可能只是部分形成——我們只知道基類對象已經初始化。如果構造器只是在構建對象過程中的一個步驟,並且該對象所屬的類是從這個構造器所屬的類導出的,那麼導出部分在當前構造器正在被調用的時空仍舊是沒有被初始化的。然而,一個動態綁定的方法調用卻會向外深入到繼承層次結構內部,它可以調用導出類裏的方法。如果我們是在構造器內部這樣做,那麼就可能會調用某個方法,而這個方法所操作的成員可能還未進行初始化。
	Father.draw()方法設計爲將要被覆蓋,這種覆蓋是在Son中發生的。但是Father的構造器會調用這個方法,結構導致了對Son.darw()的調用。但是輸出結果我們會發現當Father的構造器調用draw()方法時,rad不是默認值,而是0.
	初始化的實際過程:
		13.1、在其他任何事物發生之前,將分配給對象的存儲空間初始化成二進制的零。
		13.2、如前所屬那樣調用基類構造器。此時,調用被覆蓋後的draw()(要在Son構造器之前調用),由於步驟1的緣故,我們此時會發現rad的值爲0.
		13.3、安裝聲明的順序調用成員的初始化方法。
		13.4、調用導出類的構造器主體。
		這樣做有一個優點,那就是所有東西都至少初始化成零(或者是某些特殊數據類型中與 “零”等價的值),而不是僅僅留作垃圾。其中 包括通過“組合”而嵌入一個類內部的對象引用,其值爲null。如果忘記爲該引用進行初始化,就會在運行時出現異常。
		編寫構造器有一條有效的準則:用盡可能簡單的方法使對象進入正常狀態;如果可以的話,避免調用其他方法。在構造器內唯一能夠安全調用的那些方法是基類中的final方法(也適用於private方法,他們自動屬於final方法)這些方法不能被覆蓋,因此也就不會出現上述問題。
14、用繼承進行設計
	繼承在編譯時就需要知道確切類型
	中間對象包含了對一個基類的引用,基類被初始化其中一個超類。超類的特有方法會產生某種特殊行爲。既然引用在運行時可以與另一個超類重新綁定起來,所以基類的另一個超類可以替換初始化的超類,然後這個方法也會產生不同的行爲,這樣,我們在運行期間獲得了動態靈活性(這也稱作動態模式)。於此相反,我們不能在允許期間決定繼承不同的對象,因爲他要求在編譯期間完全確定下來。
	一條同樣的準則:用繼承表達行爲間的差異,並用字段表達狀態上的變化。通過繼承得到了兩個不同的類,用於表達方法的差異,而中間對象通過運用組合使自己的狀態發生變化。在這種情況下,這種狀態的改變也就產生了行爲的改變。

構造器內部的多態方法的行爲

public class Father {
    Father() {
        System.out.println("before draw");
        draw();
        System.out.println("after draw");
    }

    void draw() {

    }

    public static void main(String[] args) {
        new Son(5);
    }
}

class Son extends Father {
    int rad = 1;

    Son() {
        System.out.println("init");
    }

    Son(Integer i) {
        rad = i;
        System.out.println("rad+=" + rad);
    }
    @Override
    void draw(){
        System.out.println("rad+=" + rad);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章