1. 程式人生 > >Java程式設計思想 第七章:複用類

Java程式設計思想 第七章:複用類

1. 組合用法

程式碼複用是所有語言中都具有的一種特性,我們不必編寫功能相同或相似的程式碼,只需要將原有的程式碼直接拿來或者是在原有程式碼的基礎上進行改動即可。改動的方式有兩種,一種是組合的方式,另一種是繼承的方式。組合方式的使用很常見,前邊的例子也用到過,就是比如說我們新建了一個類,在這個類中引用了其它類的例項物件,這種就是組合。類中的變數域是成員變數,正如第二章所說,當成員變數為基本型別時,比如int,如果我們沒有給它顯式的賦值時,編譯器會自動的將其初始化為0,同時對於物件的引用,編譯器會將其初始化為null,當然這時如果你試圖呼叫物件的任意方法,編譯器會丟擲錯誤。編譯器大多數都不會為物件引用建立預設的物件,這一點是很有必要的,因為可以減少不必要的負擔。所以我們可以通過以下方式初始化這些引用,為他們找到指定的物件。

  1. 在定義物件的地方,也就是物件引用建立的時候直接指定物件,這意味著物件的初始化發生在構造器呼叫之前。
  2. 在類的構造器中,如第5章所述,我們可以通過構造器的形式,也就是new,初始化一個物件。
  3. 就在這樣使用這些物件之前,也就是所謂的延時載入,當我們必須要使用這個物件的時候,發現沒有初始化該物件呢,那麼進行初始化,這種方式可以提供系統性能,降低資源消耗,在必須用到的時候才建立。
  4. 使用例項初始化,這種與構造器的相同之處都是在程式碼中顯式的去建立一個物件,區別在於一個呼叫了構造器方法,一個賦值了一個例項。

下面分別舉例說明四種初始化的方式:

  1. 在定義物件的地方,我們在類中定義了String的引用,直接初始化賦值:
class Construct{
	public String cont = "Hello World";
}
  1. 在類的構造器中,如第五章的構造方法初始化:
class Construct{
	public String cont;
	Construct(String cont){
		this.cont = cont;
	}
}
public class ConstructTest {
	Construct construct = new Construct("Hello");
}
  1. 延遲載入,我們需要用到這個物件引用了,判斷一下是否為null,然後再決定是否初始化
public class ConstructTest {
	public static String s;
	public static void main(String[] args) {
		if(s==null){
			s=new String("Hello");
		}
	}
}
  1. 使用例項進行初始化,如下邊這種,我們沒有使用構造方法而是直接賦值了個例項進行初始化
public class ConstructTest {
	public static String s;
	public static void main(String[] args) {
		s="Hello";
	}
}

2. 繼承語法

繼承是所有面向物件語言的不可缺少的一部分,就像前面所說,我們建立了一個類,就是在繼承,如果沒有顯式的說明繼承自哪個類,那麼就是繼承自Object類。或者我們可以顯示的使用extends關鍵字告訴編譯器我們要繼承哪個類,這樣我們會自動的繼承父類中的所有域和方法,當然不包括private修飾的域和方法。

class Father{
    public String Name;
    public int age;
    private double money;
    public void doPrint(){
        System.out.println("Hello World");
    }
}
public class Son extends Father {
    public static void main(String[] args) {
        Son son = new Son();
        son.Name = "Zhang San";
        son.doPrint();
        //money是父類私有的域,所以子類不能進行訪問
        //son.money = 2014;
    }
}

2.1 初始化基類

匯出類的本質:從外部來看,它就像是一個與基類相同介面的新類,或許還會有一些額外的方法和域。但繼承並不只是複製基類的介面。當建立一個匯出類的物件時,該兌現包含了一個基類的子物件。這個子物件與你用基類之間建立的物件時一樣的。

對基類的子物件的正確初始化也是至關重要的,而且也僅有一種方法來保證這一點,在構造器中呼叫基類構造器來執行初始化,而基類構造器具有執行基類初始化所需要的所有知識和能力,Java會自動在匯出類的構造器中插入對基類構造器的呼叫。

2.2 帶引數的構造器

如果沒有預設的基類構造器,或者想要呼叫一個帶引數的基類構造器,就必須使用super關鍵字顯示的呼叫基類構造器的語句。

class ParaFather{
    public String Name;
    public int age;
    private double money;
    ParaFather(double money){
        this.money = money;
    }
    public double getMoney() {
        return money;
    }
}
public class ParaSon extends ParaFather {
    ParaSon(double money) {
        super(money);
        // TODO Auto-generated constructor stub
    }

    public static void main(String[] args) {
        ParaSon son = new ParaSon(2014);
        son.Name = "Zhang San";
        System.out.println(son.getMoney());
    }
}

執行結果:

2014.0

3. 代理

Java中還有一種基於組合與繼承之間的關係,但是實際上Java並沒有對它提供直接支援。我從文中理解到,這種方式主要是避免使用繼承的時候將父類的所有方法都暴露了出來,具體的做法是,使用一個組合,組合內部定義要使用類的物件,然後組合內部再提供一個方法(我們稱作方法A),這個方法實現直接使用前邊的物件呼叫要使用的類方法(方法B)即可。方法A我們稱作是個代理,方法B則是我們實際要呼叫的方法。概括說來就是內部提供一個可以呼叫另一個類的方法的方法。

public class Delegation {
    public DelegationClass delegationClass=new DelegationClass();
    
    public void delegationPrint(){
        delegationClass.print();
    }

    public void delegationSay(){
        delegationClass.say();
    }

    public void delegationClean(){
        delegationClass.clean();
    }
}

class DelegationClass{
    public void print(){
        System.out.println("print");
    }
    public void say(){
        System.out.println("say");
    }
    public void clean(){
        System.out.println("clean");
    }
}

4.結合組合和繼承

同時使用組合和繼承是很常用的事情,一般就是在一個子類中引用一個其它的類例項物件即可。諸如下邊這種形式:

public class CombineAndExtend extends ExtendFather{
    public Mother mother=new Mother();
}

class ExtendFather{
    //---
}
class Mother{
    //---
}

4.1 確保正確清理

4.2 名稱遮蔽

@Override

使用@Override表示覆蓋。

這裡我想詳細解釋一下過載和覆蓋的區別:

  • 過載:同一各類中,方法名相同,引數的列表或返回值不同,與返回型別無關!
  • 覆蓋:子類繼承父類而且這是必須的。在子類裡面重新定義這個方法。

5. 在組合和繼承之間選擇

組合和繼承都允許在新類中放置子物件,組合是顯式的,而繼承是隱式的。一般情況下,使用組合的地方多出是想使用其物件,就是在新類內部引用一個物件,然後利用這個物件完成一系列功能。而繼承的使用場景主要是新類想使用原來類的一部分介面這種。在使用組合和繼承的時候,要注意把我訪問許可權控制,因為一個完善的訪問許可權控制可以使程式更加健壯穩定。

6. protected關鍵字

前文說到訪問許可權控制,在一般情況下,為了保證不被他人修改自己的私有域,父類的只有自己可以有權使用的域跟方法會被設定為private,而在一些特殊情況下或許我們會放寬這種約束,即我的東西其他人都不可以用,但是我的子類可以用,這時候就是前文所說的protected關鍵字的作用了。被protected關鍵字修飾的域,可以被子類訪問。

7.向上轉型

在繼承的關係中,由於子類繼承了父類所有的方法,所以發給父類方法的所有訊息,子類都可以接收,注意這裡是發給父類方法的所有訊息,而不是子類呼叫了繼承的方法。編譯器會知道傳遞的引數引用屬於哪一種型別,並將其子類物件引用的型別轉換成父類,這個過程稱作向上引用。

class Instrument {
    public void play() {}
    static void tune(Instrument i) {
        // ...
        i.play();
    }
}

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
    public static void main(String[] args) {
        Wind flute = new Wind();
        Instrument.tune(flute); // Upcasting
    }
}

向上轉型的概念來源於子類繼承父類,子類在下,父類在上,由子類轉換成父類,所以稱作是向上轉型。因為子類是父類的超集,所以向上轉型總是安全的,相對也有向下轉型,將在後文介紹。對於組合和繼承,組合關係跟加方便且便於理解也常用,繼承的關係雖然重要但不是說一定要多用。

在這裡插入圖片描述

8. Final關鍵字

final關鍵字與static同等重要,final顧名思義就是最終的,不可改變的。所以Java中final關鍵字也是指“這是無法改變的”。一般無法改變主要是出於兩種理由,設計或者效率,這兩種相差比較遠,所以容易誤用。這裡主要從三個方面說明final關鍵字的用法。

1.final資料

當我們需要告訴編譯器這一塊資料區域不得改變的時候,比如一個固定的常量,或者一個在執行時候被初始化的值而我們卻不希望它被改變,那麼就可以使用final來修飾。比如類庫中的某個成員變數,提供給開發者使用,但是不允許開發者修改這個變數在程式執行過程中的值,那麼就可以使用final修飾。

2.final引數

Java中允許方法使用final修飾的引數,這意味著這個引數指向的物件不可以被修改。

3.final方法

使用final方法的原因有兩個,第一個是把它鎖定,以防任何繼承它的類修改這個方法。第二個原因是提高效率,如果一個方法被宣告為final方法,那麼編譯器會跳過這個方法的常規呼叫方式(引數入棧,跳到方法的執行部分,然後返回清理棧中引數,最後返回結果),並且以方法體中的實際程式碼副本代替方法呼叫,這樣減小了方法呼叫的開銷,但在方法過大的時候卻不實用,因此在JDK1.5之後,取消了該種形式的作用,final方法只用來鎖定防止其它類修改。

4.final類

類中所有的private域都被隱式的注為private,因為不允許其它人修改。還有一種是final類,被final類修飾的方法我們認為這個類不能被其它人修改,也不能被繼承,當你想達到這樣的目的時,可以使用final類。

9. 初始化及類載入

在載入的過程中,發現這個類有基類,於是他繼續載入基類,如果還有基類那麼繼續載入另一個基類,因為子類中可能會用到基類的內容。然後基類中的static域初始化,這時第一行列印。然後子類中的static域初始化,這時第二行執行。這時必要的類資源已經載入完畢,可以進行物件建立,第三行列印。初始化物件,首先物件中的所有基本型別被賦初始值0,物件被賦值為null,這是通過將物件記憶體設定為二進位制零實現的。然後基類的建構函式被呼叫,這裡是自動呼叫的,也可以通過super方法直接呼叫,這時第四行被列印。基類構造器和子類構造器執行相同的方式經過相同的過程。基類構造器執行完畢之後,例項變數按順序被建立,最後執行子類的構造方法,所以最後兩行被列印。