1. 程式人生 > >Java程式設計思想(七)—— 複用類

Java程式設計思想(七)—— 複用類

        在OOP語言中,複用程式碼一般有兩種方式:一種是組合,它只需在新的類中產生現有類的物件,它複用了現有程式程式碼的功能;還有一種是繼承,它按照現有類的型別來建立新類,而無需改變現有類的形式,它複用了現有類的形式。

一、組合語法

        示例如下:

//充電器類
public class Charger {
    private Wire wire = new Wire();
    private Plug plug = new Plug();

    @Override
    public String toString() {
        return "Charger = " + wire + " + " + plug;
    }

    public static void main(String[] args) {
        Charger charger = new Charger();
        System.out.println(charger);
        //列印結果:Charger = Wire Initialization + Plug Initialization
    }
}

//電線類
class Wire{
    private String s;

    Wire(){
        s = "Wire Initialization";
    }

    @Override
    public String toString() {
        return s;
    }
}

//插頭類
class Plug{
    private String s;

    Plug(){
        s = "Plug Initialization";
    }

    @Override
    public String toString() {
        return s;
    }
}

        在上例中,有一個特殊的方法:toString()方法,每一個非基本型別的物件都有一個toString()方法,當編譯器需要一個String而你卻只有一個物件時,該方法便會被呼叫。如果你不去顯式的定義該方法的話,它將使用超類Object中的toString()方法,每個類都會預設繼承Object類。 

二、繼承語法

        在Java中,每個類都會隱式的繼承Object類,如果要繼承其他已經存在的類,則要使用extends關鍵字。示例如下:

//動物類
public class Animal {
    private String category;
    private int age;

    public void run(){
        System.out.println("run");
    }

    public void eat(){
        System.out.println("eat");
    }
}

class Dog extends Animal{

    //呼叫父類的run方法
    @Override
    public void run() {
        super.run();
    }

    //重寫父類的eat方法
    @Override
    public void eat() {
        System.out.println("quickly eat");
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.run();
        dog.eat();
        //列印結果如下:
        //run
        //quickly eat
    }
}

1、繼承結構的初始化順序

        示例如下:

//父類
public class Parent {
    Parent(){
        System.out.println("Parent Initialization");
    }
}

//子類
class Son extends Parent{
    Son(){
        System.out.println("Son Initialization");
    }
}

//子類的子類
class Grandson extends Son{
    Grandson(){
        System.out.println("Grandson Initialization");
    }

    public static void main(String[] args) {
        Grandson grandson = new Grandson();
        //列印結果如下:
        //Parent Initialization
        //Son Initialization
        //Grandson Initialization
    }
}

        從上例中可以看出來,繼承結構的類初始化順序是從基類開始,然後一級級往下,一直到最後的子類。 

2、用帶引數的構造器進行初始化

        預設情況下,編譯器會呼叫類的預設無參建構函式進行初始化,但是,如果沒有預設無參建構函式,那麼就必須使用關鍵字super來顯式地編寫呼叫基類構造器的語句,並且配以適當的引數列表。示例如下:

//產品類
public class Product {
    Product(int i){
        System.out.println("Product Initialization i = " + i);
    }
}

//手機類
class MobilePhone extends Product{
    MobilePhone(int i){
        super(i);
        System.out.println("MobilePhone Initialization i = " + i);
    }
}

//蘋果手機
class IPhone extends MobilePhone{
    IPhone(){
        super(12);
        System.out.println("IPhone Initialization");
    }

    public static void main(String[] args) {
        IPhone iPhone = new IPhone();
        //列印如果如下:
        //Product Initialization i = 12
        //MobilePhone Initialization i = 12
        //IPhone Initialization
    }
}

三、代理

        Java並沒有提供對代理的直接支援,它是繼承與組合之間的中庸之道,為了不暴露被繼承類的所有成員方法,我們可以使用代理的方式來解決這個問題。

1、靜態代理

        靜態代理,主要包含兩個例項,被代理類和代理類,兩者都要實現公共的介面,把被代理類組合到代理類中,在被代理類的本身功能上,加上代理類的處理邏輯,達到增強功能的效果。靜態代理只能事先編寫好程式碼,經過統一編譯過才能執行。示例如下:

① 建立公共的介面

package com.thinkinjava.chapter7;

/**
 * author Alex
 * date 2018/11/7
 * description 圖書館服務的介面
 */
public interface LibraryService {

    /**
     * 增加圖書
     * @param bookName
     */
    void addBook(String bookName);

    /**
     * 刪除圖書
     * @param bookName
     */
    void deleteBook(String bookName);
}

② 建立介面的實現類

package com.thinkinjava.chapter7;

/**
 * author Alex
 * date 2018/11/7
 * description 實現圖書館服務的介面,被代理類
 */
public class LibraryServiceImpl implements LibraryService{
    private int bookNum;

    LibraryServiceImpl(int bookNum){
        this.bookNum = bookNum;
        System.out.println("初始化被代理類,賦予其基本功能,圖書館現存圖書:" + bookNum);
    }

    @Override
    public void addBook(String bookName) {
        System.out.println("增加圖書:" + bookName + ",增加之後圖書存量:" + ++bookNum);
    }

    @Override
    public void deleteBook(String bookName) {
        System.out.println("刪除圖書:" + bookName + ",刪除之後圖書存量:" + --bookNum);
    }
}

 ③ 建立介面實現類的代理類

package com.thinkinjava.chapter7;

/**
 * author Alex
 * date 2018/11/7
 * description 實現圖書館服務介面,代理類
 */
public class LibraryServiceProxy implements LibraryService{
    private LibraryServiceImpl libraryService;

    LibraryServiceProxy(LibraryServiceImpl libraryService){
        this.libraryService = libraryService;
        System.out.println("初始化代理類,代理圖書館實現類,增強其功能或隱藏其細節");
    }

    @Override
    public void addBook(String bookName) {
        System.out.println("新增之前...");
        libraryService.addBook(bookName);
        System.out.println("新增之後...");
    }

    @Override
    public void deleteBook(String bookName) {
        System.out.println("刪除之前...");
        libraryService.deleteBook(bookName);
        System.out.println("刪除之後...");
    }
}

④ 建立測試類

package com.thinkinjava.chapter7;

/**
 * author Alex
 * date 2018/11/7
 * description 測試圖書館服務的類
 */
public class LibraryServiceTest {
    public static void main(String[] args) {
        LibraryServiceImpl libraryService = new LibraryServiceImpl(1000);
        LibraryServiceProxy proxy = new LibraryServiceProxy(libraryService);

        proxy.addBook("《圍城》");
        proxy.deleteBook("《紅樓夢》");
        //列印結果如下:
        //初始化被代理類,賦予其基本功能,圖書館現存圖書:1000
        //初始化代理類,代理圖書館實現類,增強其功能或隱藏其細節
        //新增之前...
        //增加圖書:《圍城》,增加之後圖書存量:1001
        //新增之後...
        //刪除之前...
        //刪除圖書:《紅樓夢》,刪除之後圖書存量:1000
        //刪除之後...
    }
}

四、結合使用組合和繼承

1、確保正確的清理

        如果你想要某些類清理一些東西,就必須顯式的編寫一個特殊的方法來做這件事情,並要確保客戶端程式設計師知曉他們必須要呼叫這一方法。在編寫清理方法時,必須要將這一清理動作置於finally子句之中,以預防異常的出現,放在finally子句中的程式碼總是要執行的。在執行類的特定清理動作時,其順序應該同生成順序相反。示例如下:

package com.thinkinjava.chapter7;

//形狀類
public class Shape {
    Shape(int i){
        System.out.println("Shape Initialization");
    }
    void erase(){
        System.out.println("Erasing Shape");
    }
}

class Circle extends Shape{
    Circle(int i){
        super(i);
        System.out.println("Circle Initialization");
    }
    void erase(){
        System.out.println("Erasing Circle");
        super.erase();
    }
}

class Line extends Shape{
    private int begin,end;
    Line(int begin,int end){
        super(begin);
        System.out.println("Line Initialization " + begin + " " + end);
    }
    void erase(){
        System.out.println("Erasing Line");
        super.erase();
    }
}

class CADSystem extends Shape{
    private Circle circle;
    private Line[] line = new Line[2];
    CADSystem(int i){
        super(i);
        this.circle = new Circle(i);
        for(int j = 0;j<line.length;j++){
            line[j] = new Line(i,i*i);
        }
        System.out.println("CADSystem Initialization");
    }
    void erase(){
        for(int j = 0;j<line.length;j++){
            line[j].erase();
        }
        circle.erase();
        super.erase();
        System.out.println("Erasing CADSystem");
    }
}

class ShapeTest{
    public static void main(String[] args) {
        CADSystem system  = new CADSystem(50);
        try {
            //other code
        }finally {
            system.erase();
        }
        //列印如果如下:
        //Shape Initialization
        //Shape Initialization
        //Circle Initialization
        //Shape Initialization
        //Line Initialization 50 2500
        //Shape Initialization
        //Line Initialization 50 2500
        //CADSystem Initialization
        //Erasing Line
        //Erasing Shape
        //Erasing Line
        //Erasing Shape
        //Erasing Circle
        //Erasing Shape
        //Erasing Shape
        //Erasing CADSystem
    }
}

2、名稱遮蔽

        如果Java的基類擁有某個已被多次過載的方法名稱,那麼在匯出類中重新定義該方法名稱並不會遮蔽其在基類中的任何版本。因此,無論是在該層或者它的基類中對方法進行定義,過載機制都可以正常工作。

        在Java5中增加了一個@Override註解,它並不是關鍵字,但是可以把它當作關鍵字使用,當你想要重寫某個方法,而不是過載時,就可以新增些註解。這時,如果你不小心進行了過載,那麼編譯器將會報錯來提醒你。示例如下:

package com.thinkinjava.chapter7;

public class House {
    void doSomething(char c){
        System.out.println("House doSomething(char) " + c);
    }

    void doSomething(float f){
        System.out.println("House doSomething(float) " + f);
    }
}

class BigHouse extends House{
    void doSomething(int i){
        System.out.println("BigHouse doSomething(int) " + i);
    }

    /*@Override 使用了此註解只能進行方法的重寫,而不能進行過載
    void doSomething(double obj){
        System.out.println("BigHouse doSomething(double)" + obj);
    }*/
}

class HouseTest{
    public static void main(String[] args) {
        BigHouse bigHouse = new BigHouse();
        bigHouse.doSomething('c');
        bigHouse.doSomething(1.2f);
        bigHouse.doSomething(1);
        //列印結果如下:
        //House doSomething(char) c
        //House doSomething(float) 1.2
        //BigHouse doSomething(int) 1
    }
}

五、在組合和繼承之間的選擇

        組合和繼承都允許在新的類中放置子物件,組合是顯式地做,而繼承則是隱式地做。

        組合技術通常用於想在新類中使用現有類的功能而非它的介面這種情形。即,在新類中嵌入某個物件,讓其實現所需要的功能,但新類的使用者看到的只是為新類所定義的介面,而非所嵌入物件的介面,這樣就需要在新類中嵌入一個現有類的private物件。

        在繼承的時候,則是使用某個現有類來開發一個它的特殊版本,這意味著你在使用一個通用類,併為了某種特殊需要而將其特殊化。

        因此,繼承是一個is-a(是什麼)的關係,而組合則是has-a(有什麼)的關係。

六、protected關鍵字

        使用關鍵字protected所修飾的成員,就使用者來說,它是private的,但對於繼承此類的匯出類或者其他位於同一包的類來說,它是可以訪問的。示例如下:

package com.thinkinjava.chapter7;

public class Protected {
    private String name;
    Protected(String name){
     this.name = name;
        System.out.println(this.name);
    }
    protected void setName(String name){
        this.name = name;
        System.out.println(this.name);
    }
}

class ProtectedSon extends Protected{
    ProtectedSon(String name){
        super(name);
    }
    public void setName(String name){
        //放在不同的包也可以訪問
        setName(name);
    }
}

class ProtectedTest{
    public static void main(String[] args) {
        Protected pro = new Protected("Tom");
        pro.setName("Alice");
        //列印結果如下:
        //Tom
        //Alice
    }
}

七、向上轉型

        先看如下示例:

package com.thinkinjava.chapter7;

//樂器類
public class Instrument {
    public void play(Instrument instrument){
        System.out.println("開始彈奏樂器");
    }
}

//鋼琴類
class Piano extends Instrument{
    private void play(){
        System.out.println("開始使用鋼琴彈奏樂器");
    }

    public static void main(String[] args) {
        Piano piano = new Piano();
        Instrument instrument = new Instrument();
        instrument.play(piano);//鋼琴是一種樂器,所以能向樂器類傳送的訊息自然可以向鋼琴類傳送
        //列印結果:開始彈奏樂器

        Instrument instrument1 = new Piano();//父類的物件指向子類的例項
        instrument1.play(instrument1);//呼叫的是父類的方法
        //列印結果:開始彈奏樂器 
    }
}

        從上例中可以看出來,由於繼承可以確保基類中所有的方法在匯出類中也同樣有效,所以可以向基類傳送的所有資訊也同樣可以嚮導出類傳送。而這種將匯出類轉換成基類引用的動作,稱之為向上轉型。 

        由匯出類轉換成基類,在繼承圖中是向上移動的,因此一般稱之為向上轉型。由於向上轉型是由一個較專用型別向較通用型別轉換,所以總是很安全的。匯出類是基類的一個超集,它可能比基類包含有更多的方法,但它必須具有基類中所含有的方法。

八、final關鍵字

1、final域

        在Java中,final可以用來修飾常量,常量的修飾一般採用如下形式:

public static final int BOOK_NUM = 100;

        定義為public則可以用於包之外;static強調每個類只有一份 ;final說明它是一個常量,不能被修改;一個既是static又是final的域佔據一段不能改變的儲存空間;常量名一般使用大寫字母,多個單詞使用下劃線進行分隔;final域在定義時必須進行初始化。

        對於基本資料型別,final使用數值保持恆定不變;而對於引用資料型別,final使用引用恆定不變,但物件其自身卻是可以改變的,陣列也一樣,它是物件。示例如下:

package com.thinkinjava.chapter7;

public class FinalFiled {

    private static final Student stu = new Student();

    public static void main(String[] args) {
        stu.setAge(10);
        System.out.println(stu.getAge());
        stu.setAge(20);
        System.out.println(stu.getAge());
    }

}

class Student{
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

        還有就是,Java允許在引數列表中將引數指明為final, 這意味著你無法在方法中更改引用所指向的物件,跟上面的示例效果一樣的。

2、final方法

        使用final方法的原因有兩個。

        第一,是把方法鎖定,以防任何繼承類修改它的含義,這是設計時想要確保在繼承中使用方法行為保持不變,並且不會被覆蓋。

        第二,是提高效率。在Java的早期實現中,如果將一個方法指明為final,就是同意編譯器針對該方向的所有呼叫都轉為內嵌呼叫。當編譯器發現一個final方法呼叫命令時,它會根據自己的謹慎判斷,跳過插入程式程式碼這種正常方式而執行方法呼叫機制(將引數壓入棧,跳至方法程式碼處將執行,然後跳回並清理棧中的引數,處理返回值),並且以方法體中的實際程式碼的副本來替代方法呼叫,這將消除方法呼叫的開銷。當然,如果一個方法很大,你的程式程式碼就會膨脹,因而可能看不到內嵌所帶來的任何效能方面的提高,因為所帶來的效能提高會因為花費於方法內的時間量而被縮減。在最新的Java版本中,已經不再需要使用final方法進行優化了,我們應該讓編譯器和JVM處理效率問題,只有在想要明確禁止覆蓋時,才將方法置為final方法。

注意:final方法不可重寫,但可以過載。

3、final類

        如果將某個類修飾為final,那麼這個類是不可以被繼承的,final類的域可以自己選擇為final或者不是final。然而,由於final類禁止繼承,所以final類中的所有方法都隱式的指定為final的,因為無法覆蓋它們。