java編程思想個人筆記3(ch8:多態, ch9:接口)

ch8: 多態

在面向對象的程序設計語言中,多態(polymorphism)是繼數據抽象和繼承之後的第三個基本特徵
多態通過分離做什麼和怎麼做,從另一個角度將接口和實現分離開來。多態不但能夠改善代碼的組織結構和可讀性,還能夠創建可擴展的程序——即無論是在項目最初創建時還是在需要添加新功能時都可以“生長”程序。

向上轉型

多態一般應用最多的或者最表象的就是基類有很多實現類或者說是子類,通過基類或者是子類向上轉型創建父類對象,隱藏了實現細節,同時也更具靈活性,具備更好的可擴展性。

  • “覆蓋”私有方法
    當然在這種情況下,也存在着一定的隱患,複寫父類方法時,父類方法是private或者final時,子類以爲覆寫了該方法,實際上對於父類來說,用子類向上轉型的方式創建的子類該“覆寫”方法是不可見的,調用的還是父類的這個private或者final方法,所以好的習慣是:

    在覆寫父類的方法時,最好添加@Override註解,以便於在編譯時或者ide提示時發現。

  • 域和靜態方法
    多態只是針對普通的方法調用,對於域和靜態是不具備多態性的。
    例如,如果直接訪問某個域(field),這個訪問就將在編譯期解析。
    對於域來說,一般都不會出現這個問題,首先,各自類中的域一般是private私有的,其次一般父類和子類不會同時定義相同的名字。示例如下:

// Direct field access is determined at compile time.
class Super{
    public int field = 0;
    public int getField() { return field;}
}
class Sub extends Super {
    public int field = 1;
    public int getField() { return field; }
    public int getSuperField() { return super.field; }
}
public class FieldAccess {
    public static void main(String[]args) {
        Super sup = new Sub(); //Upcast
        System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField();
        Sub sub = new Sub();
        System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ",sub.getSuperField() = " + sub.getSuperField();
    }
}
/* Output:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*///:~

如果某個方法是靜態的,它的行爲就不具有多態性。

構造器和多態

通常,構造器不同於其他種類的方法。涉及到多態時仍是如此。構造器不具有多態性,它們實際上是static方法,只不過該static聲明是隱式的。

  • 構造器的調用順序
    基類的構造器總是在導出類的構造過程被調用,而且是按照繼承層次逐漸向上鏈接,以使每個基類的構造器都能夠得到調用。這樣做是有意義的,因爲構造器具有一項特殊任務:檢查對象是否被正確的構造。導出類只能訪問它自己的成員,不能訪問基類的成員(基類成員通常是private的)。只有基類的構造器才具有恰當的知識和權限來對自己的元素進行初始化。因此必須令所有構造器都得到調用,否則就不可能正確的構造完整的對象。這正是編譯器爲什麼要強制每個導出類部分都必須調用構造器的原因。在導出類的構造器主體中,如果沒有明確指定調用某個基類構造器,它就會“默默”地調用默認構造器。如果不存在默認構造器,編譯器就會報錯。
  • 構造器內部的多態方法行爲
    在一般的方法內部,動態綁定的調用是在運行時才決定的,因爲對象無法知道它屬於方法所在的類,還是屬於那個類的導出類。
    如果要調用構造器內部的一個動態綁定方法,就要用到那個方法的被覆蓋後的定義。然而,這個調用效果可能相當難以預料,因爲被覆蓋的方法在對象被完全初始化之前就會被調用。這個可能會造成一些難以發現的隱藏錯誤。
    從概念上講,構造器的工作實際上是創建對象。在任何構造器內部,整個對象可能只是部分形成——只知道基類對象已經進行了初始化。如果構造器只是在構建對象過程中的一個步驟,並且該對象所屬的類是從這個構造器所屬的類導出的,那麼導出的部分在當前構造器正在被調用的時刻仍舊是沒有被初始化的。然而,一個動態綁定的方法調用卻會向外深入到繼承層次結構內部,它可以調用導出類裏的方法。所以如果在構造器內部這樣做,那麼就可能會調用某個方法,而這個方法所操縱的成員可能還未進行初始化——這肯定會招致災難。如下例子:
// Constructors and polymorphism
// don't produce what you might expect.
class Glyph {
    void draw() { print("Glyph.draw()");}  //print方法是一個工具方法,其實就是一個System.out.println方法
    Glyph() {
        print("Glyph() before draw()");
        draw();
        print("Glyph() after draw()");
        }
}
class RoundGlyph extends Glyph {
    private int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        print("RoundGlyph.RoundGlyph(). radius = " + radius);
    }
    void draw() {
        print("RoundGlyph.draw(), radius = " + radius);
    }
}
public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}
/* output:
Glyph() before draw()
RoundGlyph.draw(). radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*//:~

可以看出基類構造器中調用的子類覆蓋的方法並未返回打印出默認值1,這就是上面所說的問題。
初始化的實際過程是:
1. 在其他任何事物發生之前,將分配給對象的存儲空間初始化成二進制的0.
2. 調用基類構造器
3. 按照聲明順序調用成員的初始化方法
4. 調用導出類的構造器主體。

綜上,編寫構造器時有一條有效的準則:

用儘可能簡單的方法使對象進入正常狀態;如果可以的話避免調用其他方法。
如果確實需要調用其他方法的話,這些方法應該是private或者final的子類無法繼承覆蓋的。

用繼承進行設計

當考慮複用基類屬性時,可以優先考慮“組合”而不是繼承關係,從而提高程序的靈活性。這個“組合”的對象中又以基類對象作爲域(field)最佳。


ch9: 接口

接口和內部類爲我們提供了一種將接口與實現分離的更加結構化的方法。

對於接口來說,所有的域(field)都是隱式final static的,所以可以不用在前面新增這些限定符,方法(method)都是public的,不能指定爲其他類型的,並且這些方法只有聲明,沒有具體實現,到java8以後,爲了兼容老的代碼,因爲很多項目都是使用了這些接口,想要在在原來的接口中新增方法可以添加方法,可以使用default修飾爲方法添加默認實現,這樣,老的代碼升級到新的jdk對於這些接口也不用動,默認方法實現可以使接口更加靈活。

接口就是高度抽象出來的超類,通過多態實現高靈活度組合調用,還有就是對於策略模式、代理模式等,大多也是利用了接口實現的。

再說一下,繼承與接口的區別,接口可以多實現(implement),域和方法都是默認public static的,對於繼承,只能單繼承,所以很多情況下應該考慮使用接口,而不是使用類的方式作爲超類

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章