理解面向對象編程及面向對象編程語言的關鍵就是理解其四大特性:封裝、抽象、繼承、多態。不過,對於這四大特性,光知道它們的定義是不夠的,咱們還要知道每一個特性存在的意義和目的,以及它們能解決哪些編程問題。java
首先,咱們來看封裝特性。封裝也叫做信息隱藏或者數據訪問保護。類經過暴露有限的訪問接口,受權外部僅能經過類提供的方式(或者叫函數)來訪問內部信息或者數據。這句話怎麼理解呢?咱們經過一個簡單的例子來解釋一下。下面這段代碼是金融系統中一個簡化版的虛擬錢包的代碼實現。在金融系統中,咱們會給每一個用戶建立一個虛擬錢包,用來記錄用戶在咱們的系統中的虛擬貨幣量。編程
public class Wallet { private String id; private long createTime; private BigDecimal balance; private long balanceLastModifiedTime; // ...省略其餘屬性... public Wallet() { this.id = IdGenerator.getInstance().generate(); this.createTime = System.currentTimeMillis(); this.balance = BigDecimal.ZERO; this.balanceLastModifiedTime = System.currentTimeMillis(); } public String getId() { return this.id; } public long getCreateTime() { return this.createTime; } public BigDecimal getBalance() { return this.balance; } public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; } public void increaseBalance(BigDecimal increasedAmount) { if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("..."); } this.balance.add(increasedAmount); this.balanceLastModifiedTime = System.currentTimeMillis(); } public void decreaseBalance(BigDecimal decreasedAmount) { if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("..."); } if (decreasedAmount.compareTo(this.balance) > 0) { throw new InsufficientAmountException("..."); } this.balance.subtract(decreasedAmount); this.balanceLastModifiedTime = System.currentTimeMillis(); } }
從代碼中,咱們能夠發現,Wallet 類主要有四個屬性(也能夠叫做成員變量),也就是咱們前面定義中提到的信息或者數據。其中,id 表示錢包的惟一編號,createTime 表示錢包建立的時間,balance 表示錢包中的餘額,balanceLastModifiedTime 表示上次錢包餘額變動的時間。設計模式
咱們參照封裝特性,對錢包的這四個屬性的訪問方式進行了限制。調用者只容許經過下面這六個方法來訪問或者修改錢包裏的數據。數組
String getId()架構
long getCreateTime()編程語言
BigDecimal getBalance()ide
long getBalanceLastModifiedTime()函數
void increaseBalance(BigDecimal increasedAmount)性能
void decreaseBalance(BigDecimal decreasedAmount)學習
之因此這樣設計,是由於從業務的角度來講,id、createTime 在建立錢包的時候就肯定好了,以後不該該再被改動,因此,咱們並無在 Wallet 類中,暴露 id、createTime 這兩個屬性的任何修改方法,好比 set 方法。並且,這兩個屬性的初始化設置,對於 Wallet 類的調用者來講,也應該是透明的,因此,咱們在 Wallet 類的構造函數內部將其初始化設置好,而不是經過構造函數的參數來外部賦值。
對於錢包餘額 balance 這個屬性,從業務的角度來講,只能增或者減,不會被從新設置。因此,咱們在 Wallet 類中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,並無暴露 set 方法。對於 balanceLastModifiedTime 這個屬性,它徹底是跟 balance 這個屬性的修改操做綁定在一塊兒的。只有在 balance 修改的時候,這個屬性纔會被修改。因此,咱們把 balanceLastModifiedTime 這個屬性的修改操做徹底封裝在了 increaseBalance() 和 decreaseBalance() 兩個方法中,不對外暴露任何修改這個屬性的方法和業務細節。這樣也能夠保證 balance 和 balanceLastModifiedTime 兩個數據的一致性。
封裝特性的定義講完了,咱們再來看一下,封裝的意義是什麼?它能解決什麼編程問題?
若是咱們對類中屬性的訪問不作限制,那任何代碼均可以訪問、修改類中的屬性,雖然這樣看起來更加靈活,但從另外一方面來講,過分靈活也意味着不可控,屬性能夠隨意被以各類奇葩的方式修改,並且修改邏輯可能散落在代碼中的各個角落,勢必影響代碼的可讀性、可維護性。好比某個同事在不瞭解業務邏輯的狀況下,在某段代碼中「偷偷地」重設了 wallet 中的 balanceLastModifiedTime 屬性,這就會致使 balance 和 balanceLastModifiedTime 兩個數據不一致。
除此以外,類僅僅經過有限的方法暴露必要的操做,也能提升類的易用性。若是咱們把類屬性都暴露給類的調用者,調用者想要正確地操做這些屬性,就勢必要對業務細節有足夠的瞭解。而這對於調用者來講也是一種負擔。相反,若是咱們將屬性封裝起來,暴露少量的幾個必要的方法給調用者使用,調用者就不須要了解太多背後的業務細節,用錯的機率就減小不少。這就比如,若是一個冰箱有不少按鈕,你就要研究很長時間,還不必定能操做正確。相反,若是隻有幾個必要的按鈕,好比開、停、調節溫度,你一眼就能知道該如何來操做,並且操做出錯的機率也會下降不少。
講完了封裝特性,咱們再來看抽象特性。封裝主要講的是如何隱藏信息、保護數據,而抽象講的是如何隱藏方法的具體實現,讓調用者只須要關心方法提供了哪些功能,並不須要知道這些功能是如何實現的。
在面向對象編程中,咱們常藉助編程語言提供的接口類(好比 Java 中的 interface 關鍵字語法)或者抽象類(好比 Java 中的 abstract 關鍵字語法)這兩種語法機制,來實現抽象這一特性。
對於抽象這個特性,我舉一個例子來進一步解釋一下。
public interface IPictureStorage { void savePicture(Picture picture); Image getPicture(String pictureId); void deletePicture(String pictureId); void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo); } public class PictureStorage implements IPictureStorage { // ...省略其餘屬性... @Override public void savePicture(Picture picture) { ... } @Override public Image getPicture(String pictureId) { ... } @Override public void deletePicture(String pictureId) { ... } @Override public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... } }
在上面的這段代碼中,咱們利用 Java 中的 interface 接口語法來實現抽象特性。調用者在使用圖片存儲功能的時候,只須要了解 IPictureStorage 這個接口類暴露了哪些方法就能夠了,不須要去查看 PictureStorage 類裏的具體實現邏輯。
實際上,抽象這個特性是很是容易實現的,並不須要非得依靠接口類或者抽象類這些特殊語法機制來支持。換句話說,並非說必定要爲實現類(PictureStorage)抽象出接口類(IPictureStorage),才叫做抽象。即使不編寫 IPictureStorage 接口類,單純的 PictureStorage 類自己就知足抽象特性。
之因此這麼說,那是由於,類的方法是經過編程語言中的「函數」這一語法機制來實現的。經過函數包裹具體的實現邏輯,這自己就是一種抽象。調用者在使用函數的時候,並不須要去研究函數內部的實現邏輯,只須要經過函數的命名、註釋或者文檔,瞭解其提供了什麼功能,就能夠直接使用了。好比,咱們在使用 C 語言的 malloc() 函數的時候,並不須要瞭解它的底層代碼是怎麼實現的。
抽象有時候會被排除在面向對象的四大特性以外,當時我賣了一個關子,如今我就來解釋一下爲何。
抽象這個概念是一個很是通用的設計思想,並不僅僅用在面向對象編程中,也能夠用來指導架構設計等。並且這個特性也並不須要編程語言提供特殊的語法機制來支持,只須要提供「函數」這一很是基礎的語法機制,就能夠實現抽象特性、因此,它沒有很強的「特異性」,有時候並不被看做面向對象編程的特性之一。
抽象特性的定義講完了,咱們再來看一下,抽象的意義是什麼?它能解決什麼編程問題?
實際上,若是上升一個思考層面的話,抽象及其前面講到的封裝都是人類處理複雜性的有效手段。在面對複雜系統的時候,人腦能承受的信息複雜程度是有限的,因此咱們必須忽略掉一些非關鍵性的實現細節。而抽象做爲一種只關注功能點不關注實現的設計思路,正好幫咱們的大腦過濾掉許多非必要的信息。
除此以外,抽象做爲一個很是寬泛的設計思想,在代碼設計中,起到很是重要的指導做用。不少設計原則都體現了抽象這種設計思想,好比基於接口而非實現編程、開閉原則(對擴展開放、對修改關閉)、代碼解耦(下降代碼的耦合性)等。咱們在講到後面的內容的時候,會具體來解釋。
換一個角度來考慮,咱們在定義(或者叫命名)類的方法的時候,也要有抽象思惟,不要在方法定義中,暴露太多的實現細節,以保證在某個時間點須要改變方法的實現邏輯的時候,不用去修改其定義。舉個簡單例子,好比 getAliyunPictureUrl() 就不是一個具備抽象思惟的命名,由於某一天若是咱們再也不把圖片存儲在阿里雲上,而是存儲在私有云上,那這個命名也要隨之被修改。相反,若是咱們定義一個比較抽象的函數,好比叫做 getPictureUrl(),那即使內部存儲方式修改了,咱們也不須要修改命名。
學習完了封裝和抽象兩個特性,咱們再來看繼承特性。若是你熟悉的是相似 Java、C++ 這樣的面向對象的編程語言,那你對繼承這一特性,應該不陌生了。繼承是用來表示類之間的 is-a 關係,好比貓是一種哺乳動物。從繼承關係上來說,繼承能夠分爲兩種模式,單繼承和多繼承。單繼承表示一個子類只繼承一個父類,多繼承表示一個子類能夠繼承多個父類,好比貓既是哺乳動物,又是爬行動物。
繼承特性的定義講完了,咱們再來看,繼承存在的意義是什麼?它能解決什麼編程問題?
繼承最大的一個好處就是代碼複用。假如兩個類有一些相同的屬性和方法,咱們就能夠將這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就能夠重用父類中的代碼,避免代碼重複寫多遍。不過,這一點也並非繼承所獨有的,咱們也能夠經過其餘方式來解決這個代碼複用的問題,好比利用組合關係而不是繼承關係。
若是咱們再上升一個思惟層面,去思考繼承這一特性,能夠這麼理解:咱們代碼中有一個貓類,有一個哺乳動物類。貓屬於哺乳動物,從人類認知的角度上來講,是一種 is-a 關係。咱們經過繼承來關聯兩個類,反應真實世界中的這種關係,很是符合人類的認知,並且,從設計的角度來講,也有一種結構美感。
繼承的概念很好理解,也很容易使用。不過,過分使用繼承,繼承層次過深過複雜,就會致使代碼可讀性、可維護性變差。爲了瞭解一個類的功能,咱們不只須要查看這個類的代碼,還須要按照繼承關係一層一層地往上查看「父類、父類的父類……」的代碼。還有,子類和父類高度耦合,修改父類的代碼,會直接影響到子類。
因此,繼承這個特性也是一個很是有爭議的特性。不少人以爲繼承是一種反模式。咱們應該儘可能少用,甚至不用。關於這個問題,在後面講到「多用組合少用繼承」這種設計思想的時候,我會很是詳細地再講解,這裏暫時就不展開講解了。
學習完了封裝、抽象、繼承以後,咱們再來看面向對象編程的最後一個特性,多態。多態是指,子類能夠替換父類,在實際的代碼運行過程當中,調用子類的方法實現。對於多態這種特性,純文字解釋很差理解,咱們仍是看一個具體的例子。
public class DynamicArray { private static final int DEFAULT_CAPACITY = 10; protected int size = 0; protected int capacity = DEFAULT_CAPACITY; protected Integer[] elements = new Integer[DEFAULT_CAPACITY]; public int size() { return this.size; } public Integer get(int index) { return elements[index];} //...省略n多方法... public void add(Integer e) { ensureCapacity(); elements[size++] = e; } protected void ensureCapacity() { //...若是數組滿了就擴容...代碼省略... } } public class SortedDynamicArray extends DynamicArray { @Override public void add(Integer e) { ensureCapacity(); int i; for (i = size-1; i>=0; --i) { //保證數組中的數據有序 if (elements[i] > e) { elements[i+1] = elements[i]; } else { break; } } elements[i+1] = e; ++size; } } public class Example { public static void test(DynamicArray dynamicArray) { dynamicArray.add(5); dynamicArray.add(1); dynamicArray.add(3); for (int i = 0; i < dynamicArray.size(); ++i) { System.out.println(dynamicArray.get(i)); } } public static void main(String args[]) { DynamicArray dynamicArray = new SortedDynamicArray(); test(dynamicArray); // 打印結果:一、三、5 } }
多態這種特性也須要編程語言提供特殊的語法機制來實現。在上面的例子中,咱們用到了三個語法機制來實現多態。
第一個語法機制是編程語言要支持父類對象能夠引用子類對象,也就是能夠將 SortedDynamicArray 傳遞給 DynamicArray。
第二個語法機制是編程語言要支持繼承,也就是 SortedDynamicArray 繼承了 DynamicArray,才能將 SortedDyamicArray 傳遞給 DynamicArray。
第三個語法機制是編程語言要支持子類能夠重寫(override)父類中的方法,也就是 SortedDyamicArray 重寫了 DynamicArray 中的 add() 方法。
經過這三種語法機制配合在一塊兒,咱們就實現了在 test() 方法中,子類 SortedDyamicArray 替換父類 DynamicArray,執行子類 SortedDyamicArray 的 add() 方法,也就是實現了多態特性。
接下來,咱們先來看如何利用接口類來實現多態特性。咱們仍是先來看一段代碼
public interface Iterator { String hasNext(); String next(); String remove(); } public class Array implements Iterator { private String[] data; public String hasNext() { ... } public String next() { ... } public String remove() { ... } //...省略其餘方法... } public class LinkedList implements Iterator { private LinkedListNode head; public String hasNext() { ... } public String next() { ... } public String remove() { ... } //...省略其餘方法... } public class Demo { private static void print(Iterator iterator) { while (iterator.hasNext()) { System.out.println(iterator.next()); } } public static void main(String[] args) { Iterator arrayIterator = new Array(); print(arrayIterator); Iterator linkedListIterator = new LinkedList(); print(linkedListIterator); } }
多態特性講完了,咱們再來看,多態特性存在的意義是什麼?它能解決什麼編程問題?
多態特性能提升代碼的可擴展性和複用性。爲何這麼說呢?咱們回過頭去看講解多態特性的時候,舉的第二個代碼實例(Iterator 的例子)。在那個例子中,咱們利用多態的特性,僅用一個 print() 函數就能夠實現遍歷打印不一樣類型(Array、LinkedList)集合的數據。當再增長一種要遍歷打印的類型的時候,好比 HashMap,咱們只需讓 HashMap 實現 Iterator 接口,從新實現本身的 hasNext()、next() 等方法就能夠了,徹底不須要改動 print() 函數的代碼。因此說,多態提升了代碼的可擴展性。
若是咱們不使用多態特性,咱們就沒法將不一樣的集合類型(Array、LinkedList)傳遞給相同的函數(print(Iterator iterator) 函數)。咱們須要針對每種要遍歷打印的集合,分別實現不一樣的 print() 函數,好比針對 Array,咱們要實現 print(Array array) 函數,針對 LinkedList,咱們要實現 print(LinkedList linkedList) 函數。而利用多態特性,咱們只須要實現一個 print() 函數的打印邏輯,就能應對各類集合數據的打印操做,這顯然提升了代碼的複用性。
除此以外,多態也是不少設計模式、設計原則、編程技巧的代碼實現基礎,好比策略模式、基於接口而非實現編程、依賴倒置原則、裏式替換原則、利用多態去掉冗長的 if-else 語句等等。關於這點,在學習後面的章節中,你慢慢會有更深的體會。
今天的內容就講完了,咱們來一塊兒總結回顧一下,你須要重點掌握的幾個知識點。
一、關於封裝特性
封裝也叫做信息隱藏或者數據訪問保護。類經過暴露有限的訪問接口,受權外部僅能經過類提供的方式來訪問內部信息或者數據。它須要編程語言提供權限訪問控制語法來支持,例如 Java 中的 private、protected、public 關鍵字。封裝特性存在的意義,一方面是保護數據不被隨意修改,提升代碼的可維護性;另外一方面是僅暴露有限的必要接口,提升類的易用性。
二、關於抽象特性
封裝主要講如何隱藏信息、保護數據,那抽象就是講如何隱藏方法的具體實現,讓使用者只須要關心方法提供了哪些功能,不須要知道這些功能是如何實現的。抽象能夠經過接口類或者抽象類來實現,但也並不須要特殊的語法機制來支持。抽象存在的意義,一方面是提升代碼的可擴展性、維護性,修改實現不須要改變定義,減小代碼的改動範圍;另外一方面,它也是處理複雜系統的有效手段,能有效地過濾掉沒必要要關注的信息。
三、關於繼承特性
繼承是用來表示類之間的 is-a 關係,分爲兩種模式:單繼承和多繼承。單繼承表示一個子類只繼承一個父類,多繼承表示一個子類能夠繼承多個父類。繼承主要是用來解決代碼複用的問題。
四、關於多態特性
多態是指子類能夠替換父類,在實際的代碼運行過程當中,調用子類的方法實現。多態能夠提升代碼的擴展性和複用性,是不少設計模式、設計原則、編程技巧的代碼實現基礎。