1. 程式人生 > >從一隻貓初識Java面向物件程式設計

從一隻貓初識Java面向物件程式設計

*本文較長,建議使用目錄

Java是一個面向物件的語言,在Java中一切皆物件

面向物件本身就是一個程式設計思想的飛躍,我們這個世界本身就是由物件所組成,這個世界一切皆物件,所以我們需要面向物件程式設計

一個Java程式可以認為是一系列物件的集合,而這些物件通過呼叫彼此的方法來協同工作。

Java作為一種面嚮物件語言。有類,物件,方法,例項等基本概念,有封裝,繼承,多型三大特性,有過載,重寫,封裝,繼承,抽象,介面等重要方法。本文將以貓為例子,對以上概念及方法展開一個生動的描述。

基本概念:物件,類,方法,例項

  • 物件(object):物件是類的一個例項,有狀態和行為。
    比如我們說一隻叫Michael的貓,這是一隻具體的貓,有毛,有四隻爪子是它的狀態,同時它還有叫,吃等行為,貓咪Michael就是現實中的一個物件。

    這裡寫圖片描述

  • 類(class):類是一個模板,它描述一類物件的行為和狀態。
    比如下圖這隻貓,它不指向任何一隻具體的貓,但是我們知道這張圖代表了貓,因為它描述了一隻貓的基本特性,比如外形,姿態等,雖然它不代表Michael,也不代表其它任何一隻實際生活中的貓,但我們知道現實中的貓都滿足這張圖的基本特徵。

    這裡寫圖片描述

  • 方法(method):方法就是行為,一個類可以有很多方法。邏輯運算、資料修改以及所有動作都是在方法中完成的。
    比如對於一隻貓,我們可以描述它哭的整個行為,貓哭時哭喪著臉,眼睛裡流出眼淚,這就是貓哭的行為,哭就是貓的一個方法。

    這裡寫圖片描述

  • 例項變數:Java的成員變數分為靜態變數和例項變數,靜態變數是隻屬於一個類的,而每個物件都有獨特的例項變數,物件的狀態由這些例項變數的值決定,只有當一個物件被建立起來時,例項變數才會分配空間。
    比如貓的毛色,對於每隻貓而言毛色是不一樣的,我們在定義貓這個類時規定了每隻貓都有貓色,但對於具體每隻貓而言毛色只有在定義了一個具體的貓時才能被確定,它不像腿數那樣在我們描述貓時就確定為是4。像上面那個圖,Michael的毛色是灰色,那麼灰色就是Michael這個物件的毛色例項變數的值。

我們知道了類是對於一類物件共有特性的描述,而物件是符合類的一個具體例項。物件的特性符合類中規定的特性,而類描述的方法和例項變數分別可以被物件所表達出來,方法和例項變數分別描述了物件的行為和屬性。

那麼我們在實際的Java語言中又怎麼理解這些概念呢?

在Java中我們定義一個類,類中可以有成員變數和方法。

成員變數分為一開始就隨類分配了儲存空間的靜態變數,用static修飾,和定義了一個具體物件才分配儲存空間的例項變數,不用static修飾。

方法相當於類中定義的函式,可以實現類中的邏輯與運算功能。

當我們例項化一個類,我們將一個引用指向一個物件。該類的物件可以呼叫類中非私有的方法或變數。

物件之間協同工作,實現邏輯和功能上的需求,這就是面向物件程式設計。

也就是說,通過對類的例項化,我們最後的目標還是定義並實現物件,否則就是走上了面向過程的老路。

下面看一個例子,我寫了一隻名叫Michael的貓(該例子寫了兩個類,如果寫內部類的話不允許有靜態變數):

//貓類
public class Cat {
    //靜態變數
    static int i = 1;
    //貓的尾巴數
    public void catTail(){
        System.out.println("This cat has "+i+" tail");
    }

    //成員變數毛色
    String s;
    //貓的毛色
    public void catHair(){
        System.out.println("This cat's hair is "+this.s);
    }

    //貓哭的行為
    public void cry(){
        System.out.println("Cat crys");
    }
}

//test類
public class Test{

    public static void main(String[] args) {
        //一隻叫Michael的貓    
        Cat Michael = new Cat();
        //設定成員屬性
        Michael.s = "Gray";
        //呼叫方法
        Michael.catTail();
        Michael.catHair();
        Michael.cry();    
    }
}

輸出結果:

Compiling Test.java.......
-----------OUTPUT-----------
This cat has 1 tail
This cat's hair is Gray
Cat crys
[Finished in 0.8s]

可以看到,我們定義了Cat類,類中有靜態變數 i 和成員變數 s,成員變數在我們初始化物件以後才呼叫賦值。我們例項了一個叫Michael的物件,該物件可以呼叫Cat類的各種方法,最終我們模擬了這個叫Michael的貓。

我們通過對多個物件的定義和協調它們彼此的呼叫關係,實現物件間的協同工作以達到最後的結果,這就是面向物件的程式設計思想。

面向物件三大特性和重要方法

面向物件程式設計有三大特性:封裝,繼承,多型。

在Java中我們會圍繞這三個特性實現我們的程式碼,在程式碼實現中我們會面對封裝繼承多型過載重寫抽象介面等概念。

這些概念是我們在面向物件程式設計中的重要方法,熟練掌握它們,對掌握面向物件程式設計具有重要意義。

下面我們還是以一隻貓為例來闡述這些概念。

過載

首先說說方法的語法定義:

訪問修飾符 返回值型別 方法名(引數列表){
    方法體
}

由於Java是一個強型別的語言,不像Python傳啥都能收,那麼我們會面臨一個問題,如果幾種方法除了資料型別以外其他的邏輯功能都相等,那我們怎麼辦?用xxx_int(),xxx_string()嗎?這顯然不符合物件本身的特性!

但是不要緊,我們有方法過載

所謂方法過載,就是一個類中,一系列方法的名字相同,但是引數列表不同,我們對於不同的資料型別都可以使用同一個方法,對於不同引數數的情況也可以使用同一個方法,這就大大提高我們對物件的描述能力。

舉個例子,比如有人想知道一隻貓的各種資訊,比如貓有幾條尾巴,貓是公的還是母的。我們根據不同情況會給出不同的回答,但是我們的回答輸入的引數並不一樣,這個時候使用引數過載就可以用一個方法名解決所有問題。

還是上一段程式碼:

public class Test{
    //貓的類(內部類)
    public class Cat {
        //回答貓的資訊的方法
        public void catInfo(int a){
            System.out.println("This cat has "+a +" tail");
        }

        //方法過載,資料型別不同
        public void catInfo(boolean a){
            if(a)
                System.out.println("This cat is male");
            else
                System.out.println("This cat is female");
        }

        public void catInfo(String a){
            System.out.println("This cat's hair is "+a);
        }

        //方法過載,引數數目不同
        public void catInfo(int a, String b){
            System.out.println("This cat is "+ a+ " years old and it's quite "+b);
        }

    }

    public static void main(String[] args) {
        Test a = new Test();

        Cat Michael = a.new Cat();

        //引數列表不同,呼叫同一方法名
        Michael.catInfo(1);

        Michael.catInfo(true);

        Michael.catInfo("gray");

        Michael.catInfo(3,"fat");

    }
}

輸出結果:

Compiling Test.java.......
-----------OUTPUT-----------
This cat has 1 tail
This cat is male
This cat's hair is gray
This cat is 3 years old and it's quite fat
[Finished in 0.8s]

可以看到,同一個物件,同樣的方法catInfo,對於不同的輸入引數列表,輸出了對應的不同結果,這就是方法過載。

也許對於這個例子你還覺得不滿意,覺得這個可以多起幾個方法名,但在實際工程中你總會碰到多起名字不恰當的時候,方法過載既方便自己,也方便你的協作開發者,這是一個很良好的習慣。

封裝

什麼叫封裝,為什麼要封裝?

封裝就相當於是你類的一個保護殼,假設你的手機沒了那個外殼,就整合在一塊板子上,你怕不怕摔?怕不怕串線?怕不怕短路?

同樣的道理,類也一樣,如果沒有封裝,類中的資料或方法可以被其他物件隨意訪問,隨意修改,誰還敢保證其可靠性?

我們對類的封裝,就相當於把類的程式碼封在一個保護殼裡,只有介面露在外面,而且你必須用對的上的接口才能呼叫它,這樣我們就可以確保類中程式碼的安全性與可靠性。

舉個例子吧,你在寫程式碼,出去上了個廁所,電腦沒關上,結果你家貓爬上鍵盤,最後你回來程式碼發現一團糟了。
這裡寫圖片描述

我們還是用程式碼說話(為啥要用三個類而不能用內部類,我們下面再解釋):

//程式碼類
public class Code {
    //程式碼正確性
    boolean javaCode;
}

//貓類
public class Cat {
    //貓改了你的程式碼
    public boolean catTrick(boolean a){
        return !a;
    }
}

public class Test{

    public static void main(String[] args) {
        //一隻叫Michael的貓
        Cat Michael = new Cat();
        //你寫了程式碼
        Code computer = new Code();
        //程式碼正確無誤
        computer.javaCode = true;
        //貓改了你的程式碼
        computer.javaCode = Michael.catTrick(computer.javaCode);

        System.out.println(computer.javaCode);

    }

}

輸出結果:

Compiling Test.java.......
-----------OUTPUT-----------
false
[Finished in 1.1s]

可以看到一個令人遺憾的結果,我們的Code類,它的屬性被另一個類的方法給改變了,這是我們不想看到的。設想一下,當你在和別人協作完成一項大型過程時,別人不小心把你寫的類的成員篡改了,這是多麼可怕的一件事。

那麼現在我們用getter和setter方法給我們的Code加一個“保護套”。

//程式碼類
public class Code {
    //程式碼正確性(private封裝了)
    private boolean javaCode;
    //get方法
    public boolean getJavaCode(){
        return javaCode;
    }
    //set方法
    public void setJavaCode(boolean newJavaCode){
        javaCode = newJavaCode;
    }

}

//貓類
public class Cat {
    //貓改了你的程式碼
    public boolean catTrick(boolean a){
        return !a;
    }
}

public class Test{

    public static void main(String[] args) {
        //還是那隻叫做Michael的貓
        Cat Michael = new Cat();
        //你又寫了份程式碼,這次你保護起來了
        Code computer = new Code();
        //你寫好了程式碼
        computer.setJavaCode(true);
        //貓又想改你的程式碼
        computer.javaCode = Michael.catTrick(computer.javaCode);

        System.out.println(computer.getJavaCode());

    }

}

現在的輸出結果:

Compiling Test.java.......
Test.java:11: 錯誤: javaCode可以在Code中訪問private
        computer.javaCode = Michael.catTrick(computer.javaCode);
                ^
Test.java:11: 錯誤: javaCode可以在Code中訪問private
        computer.javaCode = Michael.catTrick(computer.javaCode);
                                                     ^
2 個錯誤
[Finished in 0.7s]

很遺憾,這次貓咪慘敗,沒有Code類的get,set方法親自帶路,Cat類根本碰不到Code類中的私有成員,更不要說篡改了。這相比原來的直接訪問,安全性大大提高。

如果我們這裡把Code和Cat寫兩個內部類,結果會怎麼樣呢?結果是內部類和外圍之間的private可以相互訪問,無法被封裝。這個也很好理解,你要是認為一個人是外人,你還把他放在家裡,他不還是想動什麼就動什麼嗎?

所以說,如果你要封裝,就請把它們分開,並用private修飾,這樣它們相互間就訪問不到了。

再補充一下其他修飾符的作用範圍:

訪問修飾符 本類 同包 子類 其他
private Y N N N
預設 Y Y N N
protected Y Y Y N
public Y Y Y Y

繼承

類的繼承實際上就是類的衍生關係,我們上面說的Michael是貓,但具體來說,它是英國短毛貓。英國短毛貓當然也是貓,但是除了貓的基本特徵,英國短毛貓有許多屬於自己的特點,比如毛短而密,頭大臉圓。

那麼我們怎麼來描述英短呢?難道再按照貓的特徵全部複製一遍嗎?這樣顯然顯得很麻煩,而且不符合物件的描述特點。但通過類的繼承,我們可以很容易解決這個問題。

繼承的語法其實就是在子類定義時宣告

class 子類 extends 父類

這裡子類擁有父類除private以外的所有屬性和方法,並且可以擁有自己的屬性和方法。子類同時可以重寫實現父類的方法,關於這個和過載的差異,我們下面會討論。

需要注意的是:Java 中的繼承是單繼承,一個類只有一個父類(介面可以實現多繼承)。

還是上程式碼舉個例子吧!貓我們定義有毛色屬性和哭的屬性,那麼對於英短而言,它的毛短而密,而且眼睛是圓的,那麼怎麼描述英短這個類呢?

//貓類
public class Cat {
    //毛色
    String hair;
    public void catHair(){
        System.out.println("This cat's hair is "+this.hair);
    }

    //貓會哭
    public void cry(){
        System.out.println("Cat crys");
    }
}

//英國短毛貓類
public class BritishShorter extends Cat{
    //英短的毛(方法重寫)
    public void catHair(){
        System.out.println("British shorter's hair is short and dense");
    }

    //英短的眼睛
    public void catEyes(){
        System.out.println("British Shorter's eyes are round");
    }
}


public class Test{

    public static void main(String[] args) {
        //一隻叫Michael的英國短毛貓
        BritishShorter Michael = new BritishShorter();
        //毛色是藍色
        Michael.hair = "blue";
        //呼叫毛色方法
        Michael.catHair();
        //貓哭
        Michael.cry();
        //英短的眼睛
        Michael.catEyes();

    }
} 

輸出結果:

Compiling Test.java.......
-----------OUTPUT-----------
British shorter's hair is short and dense
Cat crys
British Shorter's eyes are round
[Finished in 2.2s]

可以看到,英短這個子類不但可以呼叫自己的方法,還可以呼叫父類Cat中的方法和屬性。但是這裡有個問題,同樣命名為catHair方法,英短的物件Michael只調用了英短的catHair,而沒有呼叫Cat的catHair方法,這就是方法重寫

方法重寫

方法重寫就是在子類裡對父類某個方法進行覆蓋,當呼叫時子類物件只會呼叫子類方法而不會呼叫父類方法。

當然我們要注意,在方法重寫時,重寫的方法與原父類的方法在返回值型別引數型別及個數,和方法名上都必須保持一致。

此外,對於重寫和過載,下面區別了兩者的差別:

  • 方法過載定義在同一個類中,具有相同的方法名,但是引數列表不同,物件在呼叫方法時,根據傳入引數列表的不同,呼叫不同的方法。
  • 方法重寫定義在子類中子類和父類中具有同樣的方法名同樣的返回值型別同樣的引數型別及個數,父類物件無法呼叫子類方法,子類物件在呼叫重寫方法時只會呼叫子類方法。

此外,繼承的初始化順序是先初始化父類再初始化子類

final&super

還有兩個問題,一個如果我不想任何類來繼承我怎麼辦?一個是如果方法重寫了我父類子類方法都想呼叫方法怎麼辦?

對於前一個問題,只要用final關鍵字修飾類或方法就好了,final關鍵字可以修飾類、方法、屬性和變數:

  • final 修飾類,該類不允許被繼承,為最終類;
  • final 修飾方法,該方法不允許被覆蓋(重寫);
  • final 修飾屬性:該屬性不會進行隱式的初始化(類的初始化屬性必須有值或在構造方法中賦值(但只能選其一));
  • final 修飾變數,該變數的值只能賦一次值,即變為常量。

對於後一個問題,只能說:明明還要用為什麼要覆蓋……
當然如果你硬要這麼做,可以這麼試試,在子類新建一個方法用super呼叫父類屬性或者方法

//貓類
public class Cat {
    //毛色
    String hair;
    public void catHair(){
        System.out.println("This cat's hair is "+this.hair);
    }
}

//英國短毛貓類
public class BritishShorter extends Cat{
    //英短的毛(方法重寫)
    public void catHair(){
        System.out.println("British shorter's hair is short and dense");
    }

    //super呼叫父類方法
    public void superCatHair(){
        super.catHair();
    }
}


public class Test{

    public static void main(String[] args) {
        //一隻叫Michael的英國短毛貓
        BritishShorter Michael = new BritishShorter();
        //毛色是藍色
        Michael.hair = "blue";
        //呼叫毛色方法
        Michael.catHair();
        //呼叫super方法    
        Michael.superCatHair();
    }
} 

輸出結果:

Compiling Test.java.......
-----------OUTPUT-----------
British shorter's hair is short and dense
This cat's hair is blue
[Finished in 0.8s]

super關鍵字在子類內部使用,代表父類物件。要訪問父類屬性時使用super.屬性名;要訪問父類方法時使用super.bark();子類構造方法時呼叫父類的構造方法,在子類的構造方法體裡最前面的位置:super()。

繼承最大程度上消除了程式碼的冗餘,提高了對物件的描述能力,並在迭代的開發中方便了修改與更新,熟練掌握繼承是寫面嚮物件語言的基本功。

多型

某種意義上而言,多型是基於封裝和繼承上建立起的重要概念。

什麼是多型的定義?

多型是指允許不同類的物件對同一訊息做出響應。即同一訊息可以根據傳送物件的不同而採用多種不同的行為方式。多型也稱作動態繫結(dynamic binding),是指在執行期間判斷所引用物件的實際型別,根據其實際的型別呼叫其相應的方法。

那麼怎麼對多型有一個較直觀的理解呢?多型可以看作是對於同一事件的不同反應。比如說都是摸動物(只是舉個例子),如果你摸一隻比較黏人的貓,比如像折耳,它會就很高興地接受;但如果你摸比較冷僻的貓,像波斯貓,它就直接躲開了;如果你摸藏獒,可能就直接咬住院了。在摸之前你不知道會發生什麼,但具體摸了動物得到的反應是不同的,這就是一種執行時多型。

直接地說,就是父類可以呼叫不同子類,在執行時判斷指向物件型別,根據物件型別採取不同的行為。摸動物作為同一個方法名,在面對動物的不同子類物件時結果不同。

向上轉型

這裡必須說明一個概念:向上轉型。什麼是“向上轉型”呢

父類class 父類引用 = new 父類物件;
子類class 子類引用 = new 子類物件;
父類class 父類引用 = new 子類物件;  //向上轉型

在上面的例子中,前兩個我們都知道,無非是建立一個父類物件或子類物件,而對第三個,一個父類引用指向了一個子類物件,這就是向上轉型。

可能對此會有一個疑問:明明是指向了下面子類的物件,為什麼還是“向上”轉型?

還是來段程式碼來解釋多型和向上轉型吧(這個實在想不出什麼很直感的例子,主要關注語法吧)。貓渴了會喝水,但是英國短尾貓渴了會喝咖啡,那麼對於這個連續的行為“渴→喝”,下面的呼叫順序是什麼呢?
這裡寫圖片描述

//父類
public class Cat {
    //貓渴了
    public void catThirsty(){
        System.out.println("Cat is thirsty");
        catDrink();  //渴了要喝
    }
    //貓渴了喝水
    public void catDrink(){
        System.out.println("Cat drink water");
    }
}

//子類    
public class BritishShorter extends Cat{
    //英短渴了
    public void catThirsty(String a){
        System.out.println("British shorter is thirsty");
        catDrink();  //渴了要喝
    }
    //英短喝咖啡
    public void catDrink(){
        System.out.println("British Shorter drink coffee");
    }

}

public class Test{
    public static void main(String[] args) {
        Cat Michael = new BritishShorter();
        Michael.catThirsty();

    }
} 

輸出結果:

Compiling Test.java.......
-----------OUTPUT-----------
Cat is thirsty
British Shorter drink coffee
[Finished in 0.8s]

多型在上例中體現得非常巧妙:一個父類引用指向一個子類物件,在先呼叫了父類方法catThirsty,然後對於父類catThirsty方法內呼叫的catDrink方法選擇了呼叫重寫過的子類方法。

向上轉型的答案從上面的例子也可以看出:明明是指向的子類物件,卻只能訪問父類的方法和屬性,而且對於子類中非重寫的方法是調用不了的,這就是向上轉型了。也就是說向上轉型在執行時,會遺忘子類物件中與父類物件中不同的方法,也會覆蓋與父類中相同的方法一一重寫

有人會說:catThirsty不是重寫了嗎!重新複習一遍,引數列表不同不是重寫!子類catThirsty就是一個父類沒有的方法,既不是過載也不是重寫。

那還有人可能會問:那我子類引用指向父類物件是向下轉型嗎?不行!子類引用不能指向父類物件,子類直接繼承父類中非私有的方法和屬性。

多型的實現

那麼怎麼實現多型呢?

Java 實現多型有三個必要條件:繼承重寫向上轉型

Java中多型的實現方式:繼承父類進行方法重寫抽象類和抽象方法介面實現

繼承實現多型

對於重寫,我們之前也說了,上面的例子也是方法重寫的例子。

還是堅持上面三個必要條件:繼承,重寫,向上轉型。

抽象實現多型

抽象這種方法是不完整的,抽象僅有宣告而沒有方法體。

抽象方法宣告語法如下:

abstract void f();  //f()方法時抽象方法

包含抽象方法的類叫做抽象類,抽象類在定義類時,前面會加abstract關鍵字。

比如下面這個例子:

public abstract class name_of_class {
    public abstract void function_1(); 
    public abstract void function_2(); 
}

抽象類中的方法沒有定義行為,以該類作為父類,子類重寫方法,便可以實現向上轉型實現多型。

那麼抽象有什麼意義呢,明明什麼都沒有做?

抽象一是可以規範程式碼,約束子類要實現的方法,實現系統性的開發;二是從多個具有相同特徵的類中抽象出一個抽象類,以這個抽象類作為子類的模板,也避免了子類設計的隨意性。

注意:

  • 用 abstract 修飾符定義抽象方法,只用宣告,不需要實現
  • 包含抽象方法的類就是抽象類
  • 抽象類中可以包含普通的方法,也可以沒有抽象方法
  • 抽象類不能直接例項化

介面實現多型

介面(interface)在抽象又近一步,介面產生一個完全抽象類,根本不提供任何方法體(抽象類可以有普通方法)。

介面的宣告語法格式如下:

修飾符 interface 介面名稱 [extends 其他的類名] {
        // 宣告變數
        // 抽象方法
}

例子:

interface name_of_interface {
        //static final型別,必須初始化
        int variable_1 = 5;

        public void function_1();
        public void function_2();
}

注意:

  • 介面不能用於例項化物件
  • 介面中所有的方法是抽象方法
  • 介面成員是 static final 型別
  • 介面支援多繼承

介面繼承方法:

修飾符 class 類名 implements 介面1,介面2{
    ...
}

與抽象相同,介面通過繼承和抽象可以實現重寫和向上轉型,進而實現多型。

總的來說

下篇主要集中於Java面向物件的三大特性

  • 封裝確保了類的資料安全性,提高了類的內聚性,降低了類間耦合性,過載在類中降低了程式碼冗餘性,提高了物件描述能力。
  • 繼承是子類繼承父類,重寫可用子類方法覆蓋父類方法。繼承消除了程式碼冗餘性,提高了可修改性。
  • 多型是父類指向子類,多型既保留了父類的特性,還可以呼叫子類的強大功能。重寫,抽象和介面可以實現多型。

寫在最後

本文是我學習java的一個學習筆記與自我總結,以我目前的水平只能寫到這樣了。尤其是多型,由於目前體會不深,寫的還是不夠透徹,以後水平提高了有機會會對一些問題再專題討論。也希望這篇博文對同樣初學面向物件的同學有幫助,如果有錯誤,歡迎斧正與討論!
這裡寫圖片描述

下面附上一些部落格與網站,想對上面內容有較深入瞭解的可以看看這些優秀的文章和教程。

參考資料