在還不清楚怎樣物件導向?一文中,已經簡單介紹了物件導向的基本思想和三大特性,但是不夠詳細。本文再來具體探究一下物件導向。
1. 概述
1.1. 程式導向程式設計
程式導向程式設計(Procedure Oriented Programming,POP)是一種以過程為中心的程式設計思想,開發人員在解決問題時更專注於過程。
當我們遇到一個問題時,只需要分析出第1步要做什麼、第2步要做什麼……直到解決問題,然後把這些步驟一步步地實現即可。
比如,我現在要編寫一個程式導向的程式來模擬「我要讓我的好朋友行小觀去幫我買瓶水」這個問題,如下:
- 給行小觀10塊錢
- 告訴行小觀去哪買水
- 行小觀找到我要的那瓶水
- 付錢,找零錢
- 把水帶回來給我
在從「給行小觀錢」到「行小觀把水給我」的整個過程,我和行小觀都專注於完成每一個步驟(事件),行小觀只是一個執行我事先描述好的步驟的無思想的工具人而已。
如果我還有其他問題需要行小觀幫忙,那麼我就還得把如何完成這些問題的詳細步驟全都告訴他,這麼麻煩那我還找別人幫忙幹什麼呢?還不如我自己去做。而且有些問題我自己也不會做,那怎麼辦?所以行小觀並不是一個合格的工具人。
1.2. 物件導向程式設計
與程式導向程式設計不同,物件導向程式設計(Object Oriented Programming,OOP)是一種以物件為中心的程式設計思想,物件是現實世界中的一個個事物的實體。
物件包含了使用者可以使用(公開)的功能部分和對使用者隱藏的實現部分。
在開發過程中,我們可以使用功能部分來解決問題,但是並不關心功能是怎樣實現的。
還是上面的那個買水的例子,使用物件導向來實現:
- 我給行小觀10塊錢,讓他幫我買瓶水
- 行小觀把水和零錢帶回來給我
在這個例子中,我只需要給行小觀錢,然後他就能幫我買水。我相信我請行小觀有「買水的能力」,他一定能幫我買到水。至於去哪買?怎麼買?行小觀自己知道,我並不關心他是怎麼買到水的,因為我的目的很簡單:「我想要一瓶水」。
如果我有其他問題需要行小觀幫忙,無論這些問題我會不會做,直接告訴他就好了,他會幫我完成。
我只專注於問題本身,具體的操作我並不關心。現在行小觀是一個合格的工具人了。
2. 類和物件
2.1. 二者之間的關係
這裡通過一個大家耳熟能詳的神話——女媧造人,來說明類和物件之間的關係。
相傳女媧以泥土仿照自己摶土造人,創造並構建人類社會。
在這個神話裡,「女媧」是一個藍圖、模板,「人」是依據該藍圖被創造出來的個體。
「女媧」可以看做類(Class),「人」可以看做物件(Object)。
跳出神話,來到真實世界。
我們目能所及的事物都可以看做是「物件」,比如說你用的桌子、坐的椅子、玩的電腦、養的狗……這些一個個真實存在,你能摸到的物品都是物件。
狗有千千萬……高的、矮的、胖的、瘦的、黑色的、白色的等各不相同,但是總能在這些不同的狗之中找到相同的特性,這些不同品種的狗我們把它統稱為「狗」。「狗」即為類,而我們養的真實存在的狗為物件。
總結一下:
-
類是對一類具有共同特徵的事物的抽象,是一類事物的統稱,是一個抽象概念(比如“人類”這個名詞)。
-
物件是這類事物相對應的具體存在的實體,是一個具體存在的事物(比如“行小觀”這個具體的人)。
-
類是建立單個物件時的藍圖、模板。
2.2. 類
當我們說到「狗」這個類的時候,會很自然地想到和狗相關的一些特點和習性。
比如,名字、品種、顏色、年齡等,這些是屬性。
還有,吠叫、看門等,這些是行為。
一個類包括了屬性和行為,行為可以操縱屬性。
對應到程式碼中,屬性即為成員變數,行為即為成員方法,成員方法可以操縱成員變數。
下面是一個具體的類:
程式2-1
/**
* 狗類
* @author Xing Xiaoguan
*/
public class Dog {
//屬性——成員變數
String name;
int age;
int legs;
//行為——成員方法
//行為——吠叫
public void say() {
System.out.println("我是" + name + "汪汪汪");
}
//行為——看門
public void watchDoor() {
System.out.println("趕走陌生人");
}
}
2.3. 物件
類是一個抽象概念,而物件則是一個具體的例項。
以狗為例,我們養的不可能是「一類狗」,而是在和一隻「具體的狗」玩耍,比如說哮天犬。
回到女媧(類)造人(物件)這個神話中,人是以女媧為模板被造出來的,女媧造人的過程,即由類構造物件的過程稱為建立類的例項(instance)。
程式2-2
public static void main(String[] args) {
Dog dog = new Dog();//建立類的例項,物件dog
dog.name = "哮天犬";
dog.age = 2;
dog.legs = 4;
dog.say();
}
對於被建立出來的物件而言,它們都不一樣,每一個特定的物件(例項)都有一組特定的屬性值(成員變數),這些屬性值的集合就是這個物件的當前狀態,只要物件使用者通過行為(成員方法)向該物件傳送訊息,這些狀態就可能被改變。
上面這句話怎麼理解?
現在有兩隻狗(兩個物件):哮天犬和哮地犬,這兩個物件的名字、年齡等屬性不同,即當前狀態不同。每隻狗都有一個行為:可以「每過一年,年齡增長1歲」,當通過該行為向哮天犬傳送訊息時,哮天犬的狀態就被改變了。
可以看出,一個物件由狀態(state)和行為(behavior)組成,物件在成員變數中儲存狀態,通過成員方法公開其行為。
研究一個物件,我們要去關注它處於什麼狀態?具有哪些行為?
物件的三個主要特性:
- 物件的行為——可以對物件施加哪些操作(方法)?
- 物件的狀態——當施加那些方法時,物件如何響應?
- 物件標識——如何辨別具有相同狀態與行為的不同物件?
3. 三大特性
3.1. 封裝
封裝(encapsulation)是Java物件導向的三大特行之一。
一個物件具有屬性和行為,封裝把其屬性和行為組合在了一起,但是為什麼需要封裝?
上文已經介紹了,一個物件由其狀態和行為組成。我們回看程式2-1
,雖然這段程式碼表示出了Dog
類,但是有一個很大的問題:建立出來的Dog物件的狀態很容易被改變。
比如我們可以直接修改程式2-2
中的物件的狀態:
dog.name = "哮地犬";
dog.legs = 3;
你的狗的名字被不懷好意的人給改了,腿也少了一條!這種事情是危險、可怕的!
我們希望別人能夠“知道”自己的狗叫什麼名字、有幾條腿等資訊,但是又要防止不懷好意的人隨便“傷害”自己的狗,怎麼辦呢?答案是封裝!
將物件的狀態和行為封裝起來,使用該物件的使用者只能通過物件本身提供的方法來訪問該物件的狀態。前面也說過,物件的當前狀態可能會改變,但是這種改變不是物件自發的,必須通過呼叫物件本身提供的方法來改變。如果不通過呼叫方法就能改變物件狀態,只能說明封裝性被破壞了。
換句話說,我們將物件的屬性(狀態)對外隱藏起來,這些狀態能否被訪問或修改,由物件自己來決定,決定的方式就是「給物件的使用者提供可呼叫的方法,使用者通過這些方法來進行訪問和修改」。
程式2-1
可以改進為:
程式3-1
/**
* 封裝後的狗類
* @author Xing Xiaoguan
*/
public class Dog {
private String name;
private int age;
private int legs;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public int getLegs() {
return legs;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
//行為——吠叫
public void say() {
System.out.println("我是" + name + "汪汪汪");
}
//行為——看門
public void watchDoor() {
System.out.println("趕走陌生人");
}
}
乍一看多了許多程式碼,其實就多了兩個部分:
- 使用
private
修飾符來修飾成員變數,將其私有化,確保只能在本類內被訪問到,實現隱藏。 - 給成員變數提供了對應的訪問方法(
getter
方法)和修改方法(setter
方法)。
其中我們給使用者能夠訪問或修改的成員變數都編寫上對應的setter或getter方法,如name
。使用者不能訪問或修改的成員變數不寫setter或getter方法即可。
/**
* 例項化一隻小狗,並和它玩
*/
public class Play {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("哮天犬");;
dog.setAge(2);
dog.say();
dog.watchDoor();
}
}
現在,使用者不能直接訪問或修改物件的狀態,必須通過提供的方法。物件並沒有給legs
變數提供setter
方法,這樣使用者就只能訪問狗有幾條腿,但是不能修改。狗腿被人“偷”了的事情也不會再發生了。
而且,當我們使用setter方法修改成員變數時,可以進行其他的操作,如錯誤檢查。比如設定age
變數時:
//狗的平均壽命為10~15年,太大了不合理
public void setName(String name) {
if (name > 0 && name < 30)
this.name = name;
}
現在,Dog
類就比較安全了。
由上面的程式碼可以看出,如果要訪問或修改成員變數,需要:
- 成員變數是私有的
- 一個公有的getter方法
- 一個公有的setter方法
封裝還有一個優點就是:對外隱藏了具體實現,這樣的好處就是:我們可以修改內部實現,除了修改了該類的方法外,不會影響其他程式碼。
封裝使物件對外變成了一個“黑箱”,使用者只會使用,但不清楚內部情況。
前面買水的例子也體現了封裝思想:行小觀買水的方式有很多,走路去、騎車去、甚至找比人幫忙,但是他改變買水的方式並不會對我造成影響。
3.2. 繼承
生活中除了狗,還有許多其他動物,比如貓、兔子……
程式3-2
/**
* 貓類
* @author Xing Xiaoguan
*/
public class Cat {
private String name;
private int age;
private int legs;
private String owner;//主人
//getters and setters……
//行為——叫
public void say() {
System.out.println("我是" + name + "喵喵喵");
}
//行為——捉老鼠
public void catchMouse() {
System.out.println("捉到一隻老鼠");
}
}
程式2-5
/**
* 兔子類
* @author Xing Xiaoguan
*/
public class Rabbit {
private String name;
private int age;
private int legs;
private String home;//住址
//getters and setters……
//行為——叫
public void say() {
System.out.println("我是" + name + "咕咕咕");
}
//行為——搗藥
public void makeMedicine() {
System.out.println("在" + home + "搗藥");
}
}
寫完這兩個類,發現有許多屬性和方法是重複的,如果需要再寫100個動物的類,那得
這些動物形態各異,但是它們都被統稱為“動物”,也就是說,我們仍能在它們身上找出相同的特點,比如它們都有名字、年齡、腿、能發出聲音……
前面介紹類的時候已經說了,類是對一類具有共同特徵的事物的抽象,所以此時我們還能從狗、貓、兔子這些類中再抽象出一個類——動物類。
程式3-3
/**
* 動物類
* @author Xing Xiaoguan
*/
public class Animal {
private String name;
private Integer age;
private Integer legs;
public void say() {
System.out.println("我是"+name+"發出聲響");
}
//setters and getters……
}
這個更抽象的類就是父類,而狗、貓、兔子類是子類。子類可以使用extends
關鍵字繼承父類的屬性和方法,這意味著相同的程式碼只需要寫一遍。
程式3-4
/**
* 狗類繼承父類
* @author Xing Xiaoguan
*/
public class Dog extends Animal{
//行為——吠叫
public void say() {
System.out.println("我是" + getName() + "汪汪汪");
}
//行為——看門
public void watchDoor() {
System.out.println("趕走陌生人");
}
}
程式3-5
/**
* 貓類繼承父類
* @author Xing Xiaoguan
*/
public class Cat extends Animal {
private String owner;
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
//行為——喵喵叫
public void say() {
System.out.println("我是" + getName() + "喵喵喵");
}
//行為——捉老鼠
public void catchMouse() {
System.out.println("捉到一隻老鼠");
}
}
程式3-6
/**
* 兔子類繼承父類
* @author Xing Xiaoguan
*/
public class Rabbit extends Animal {
private String home;
public String getHome() {
return home;
}
public void setHome(String home) {
this.home = home;
}
//行為——叫
public void say() {
System.out.println("我是" + getName() + "咕咕咕");
}
//行為——搗藥
public void makeMedicine() {
System.out.println("在" + home + "搗藥");
}
}
觀察上面的子類和父類,可以發現:
- 子類不用再重複父類中已有的屬性和方法,能通過繼承獲取到。
- 子類比父類的功能更加豐富,子類可以擁有自己的屬性和方法
- 子類如果感覺繼承自父類的方法不合適,可以重寫父類的方法的實現過程,注意返回值和形參不能改變。
使用繼承的好處:
- 提高程式碼的複用性,不用再寫那麼多重複程式碼了。
- 使程式碼便於維護,當我們需要修改某個公用方法時,不需要一個個類去修改,只需修改父類的該方法即可。
3.3. 多型
看下面一段程式碼,我們來直觀體驗什麼是多型。
程式3-7
public static void main(String[] args) {
Animal dog = new Dog();
dog.setName("哮天犬");
dog.say(dog.getName());
Animal cat = new Cat();
cat.setName("加菲貓");
cat.say();
Animal rabbit = new Rabbit();
rabbit.setName("玉兔");
rabbit.say();
}
執行,輸出:
我是哮天犬汪汪汪
我是加菲貓喵喵喵
我是玉兔咕咕咕
Dog
、Cat
和Rabbit
都是繼承了Animal
父類。Dog
、Cat
和Rabbit
都重寫了Animal
的say(String name)
方法。- 在建立例項物件時,我們使用父類引用指向子類的物件。
使用多型應當注意一下幾點:Animal dog = new Dog()
-
在多型中,子類物件只能呼叫父類中定義的方法,不能呼叫子類中獨有的方法。
比如
dog
不能呼叫watchDoor()
方法。 -
在多型中,子類可以呼叫父類的所有方法。
-
在多型中,子類如果重寫了父類的方法,那麼子類呼叫該方法時,呼叫的是子類重寫的方法。
在上面的程式碼中,狗、貓、兔子物件都執行了say方法,但是輸出不同。
由此看出,不同的物件的同一行為具有不同的表現形式,這就是多型。
在實際的本例中,我們可以理解為:動物Animal
,他們都會叫出聲。如果是狗,則叫的是汪汪汪;如果是貓,則叫的是喵喵喵;如果是兔子,則叫的是咕咕咕。
4. 總結
物件導向思想使我們在程式設計更加貼近現實世界,類是現實世界的抽象,物件則是一個個具體的事物。
封裝使一個個具體的事物更加獨立,繼承則使一些類似的事物之間具有聯絡,而多型則使事物的行為更加靈活多樣。
物件導向程式設計提高了軟體的重用性、靈活性、擴充套件性。
如有錯誤,還請指正
參考資料:
The Java Tutorials
維基百科
百度百科
Java核心技術 卷1
文章首發於公眾號「行人觀學」