理解面向對象編程及面向對象編程語言的關鍵就是理解其四大特性:封裝、抽象、繼承、多態。不過,對於這四大特性,光知道它們的定義是不夠的,咱們還要知道每一個特性存在的意義和目的,以及它們能解決哪些編程問題。java
封裝也叫做信息隱藏或者數據訪問保護。類經過暴露有限的訪問接口,受權外部僅能經過類提供的方式(或者叫函數)來訪問內部信息或者數據。python
對於封裝這個特性,咱們須要編程語言自己提供必定的語法機制來支持。這個語法機制就是訪問權限控制。private
、public
等關鍵字就是 Java
語言中的訪問權限控制語法。private
關鍵字修飾的屬性只能類自己訪問,能夠保護其不被類以外的代碼直接訪問。若是 Java
語言沒有提供訪問權限控制語法,全部的屬性默認都是 public
的,那任意外部代碼均可以經過相似 wallet.id=123
; 這樣的方式直接訪問、修改屬性,也就沒辦法達到隱藏信息和保護數據的目的了,也就沒法支持封裝特性了。編程
封裝的意義是什麼?它能解決什麼編程問題?設計模式
若是咱們對類中屬性的訪問不作限制,那任何代碼均可以訪問、修改類中的屬性,雖然這樣看起來更加靈活,但從另外一方面來講,過分靈活也意味着不可控,屬性能夠隨意被以各類奇葩的方式修改,並且修改邏輯可能散落在代碼中的各個角落,勢必影響代碼的可讀性、可維護性。好比某個同事在不瞭解業務邏輯的狀況下,在某段代碼中「偷偷地」重設了 wallet
中的 balanceLastModifiedTime
屬性,這就會致使 balance
和 balanceLastModifiedTime
兩個數據不一致。數組
除此以外,類僅僅經過有限的方法暴露必要的操做,也能提升類的易用性。若是咱們把類屬性都暴露給類的調用者,調用者想要正確地操做這些屬性,就勢必要對業務細節有足夠的瞭解。而這對於調用者來講也是一種負擔。相反,若是咱們將屬性封裝起來,暴露少量的幾個必要的方法給調用者使用,調用者就不須要了解太多背後的業務細節,用錯的機率就減小不少。這就比如,若是一個冰箱有不少按鈕,你就要研究很長時間,還不必定能操做正確。相反,若是隻有幾個必要的按鈕,好比開、停、調節溫度,你一眼就能知道該如何來操做,並且操做出錯的機率也會下降不少。架構
封裝主要講的是如何隱藏信息、保護數據,而抽象講的是如何隱藏方法的具體實現,讓調用者只須要關心方法提供了哪些功能,並不須要知道這些功能是如何實現的。編程語言
在面向對象編程中,咱們常藉助編程語言提供的接口類(好比 Java
中的 interface
關鍵字語法)或者抽象類(好比 Java
中的 abstract
關鍵字語法)這兩種語法機制,來實現抽象這一特性。ide
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 { // ...省略其餘屬性... public void savePicture(Picture picture) { ... } public Image getPicture(String pictureId) { ... } public void deletePicture(String pictureId) { ... } public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... } }
在上面的這段代碼中,咱們利用 Java
中的 interface
接口語法來實現抽象特性。調用者在使用圖片存儲功能的時候,只須要了解 IPictureStorage
這個接口類暴露了哪些方法就能夠了,不須要去查看 PictureStorage
類裏的具體實現邏輯。函數
實際上,抽象這個特性是很是容易實現的,並不須要非得依靠接口類或者抽象類這些特殊語法機制來支持。換句話說,並非說必定要爲實現類(PictureStorage
)抽象出接口類(IPictureStorage
),才叫做抽象。即使不編寫 IPictureStorage
接口類,單純的 PictureStorage
類自己就知足抽象特性。性能
之因此這麼說,那是由於,類的方法是經過編程語言中的「函數」這一語法機制來實現的。經過函數包裹具體的實現邏輯,這自己就是一種抽象。調用者在使用函數的時候,並不須要去研究函數內部的實現邏輯,只須要經過函數的命名、註釋或者文檔,瞭解其提供了什麼功能,就能夠直接使用了。好比,咱們在使用 C
語言的 malloc()
函數的時候,並不須要瞭解它的底層代碼是怎麼實現的。
抽象有時候會被排除在面向對象的四大特性以外,爲何呢?
抽象這個概念是一個很是通用的設計思想,並不僅僅用在面向對象編程中,也能夠用來指導架構設計等。並且這個特性也並不須要編程語言提供特殊的語法機制來支持,只須要提供「函數」這一很是基礎的語法機制,就能夠實現抽象特性、因此,它沒有很強的「特異性」,有時候並不被看做面向對象編程的特性之一。
抽象的意義是什麼?它能解決什麼編程問題
若是上升一個思考層面的話,抽象及其前面講到的封裝都是人類處理複雜性的有效手段。在面對複雜系統的時候,人腦能承受的信息複雜程度是有限的,因此咱們必須忽略掉一些非關鍵性的實現細節。而抽象做爲一種只關注功能點不關注實現的設計思路,正好幫咱們的大腦過濾掉許多非必要的信息。
除此以外,抽象做爲一個很是寬泛的設計思想,在代碼設計中,起到很是重要的指導做用。不少設計原則都體現了抽象這種設計思想,好比基於接口而非實現編程、開閉原則(對擴展開放、對修改關閉)、代碼解耦(下降代碼的耦合性)等。咱們在講到後面的內容的時候,會具體來解釋。
換一個角度來考慮,咱們在定義(或者叫命名)類的方法的時候,也要有抽象思惟,不要在方法定義中,暴露太多的實現細節,以保證在某個時間點須要改變方法的實現邏輯的時候,不用去修改其定義。舉個簡單例子,好比 getAliyunPictureUrl()
就不是一個具備抽象思惟的命名,由於某一天若是咱們再也不把圖片存儲在阿里雲上,而是存儲在私有云上,那這個命名也要隨之被修改。相反,若是咱們定義一個比較抽象的函數,好比叫做 getPictureUrl()
,那即使內部存儲方式修改了,咱們也不須要修改命名。
繼承是用來表示類之間的 is-a
關係,好比貓是一種哺乳動物。從繼承關係上來說,繼承能夠分爲兩種模式,單繼承和多繼承。單繼承表示一個子類只繼承一個父類,多繼承表示一個子類能夠繼承多個父類,好比貓既是哺乳動物,又是爬行動物。
爲了實現繼承這個特性,編程語言須要提供特殊的語法機制來支持,好比 Java
使用 extends
關鍵字來實現繼承,C++
使用冒號(class B : public A
),Python
使用 paraentheses()
,Ruby
使用 <
。不過,有些編程語言只支持單繼承,不支持多重繼承,好比 Java
、PHP
、C#
、Ruby
等,而有些編程語言既支持單重繼承,也支持多重繼承,好比 C++
、Python
、Perl
等。
爲何 Java
不支持多重繼承呢?
Java
不支持多重繼承的緣由:
多重繼承有反作用:鑽石問題(菱形繼承)。
假設類B
和類C
繼承自類A
,且都重寫了類A
中的同一個方法,而類D
同時繼承了類B
和類C
,那麼此時類D
會繼承B
、C
的方法,那對於B
、C
重寫的A
中的方法,類D
會繼承哪個呢?這裏就會產生歧義。
考慮到這種二義性問題,Java
不支持多重繼承。
繼承存在的意義是什麼?它能解決什麼編程問題?
繼承最大的一個好處就是代碼複用。假如兩個類有一些相同的屬性和方法,咱們就能夠將這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就能夠重用父類中的代碼,避免代碼重複寫多遍。不過,這一點也並非繼承所獨有的,咱們也能夠經過其餘方式來解決這個代碼複用的問題,好比利用組合關係而不是繼承關係。
若是咱們再上升一個思惟層面,去思考繼承這一特性,能夠這麼理解:咱們代碼中有一個貓類,有一個哺乳動物類。貓屬於哺乳動物,從人類認知的角度上來講,是一種 is-a
關係。咱們經過繼承來關聯兩個類,反應真實世界中的這種關係,很是符合人類的認知,並且,從設計的角度來講,也有一種結構美感。
繼承的概念很好理解,也很容易使用。不過,過分使用繼承,繼承層次過深過複雜,就會致使代碼可讀性、可維護性變差。爲了瞭解一個類的功能,咱們不只須要查看這個類的代碼,還須要按照繼承關係一層一層地往上查看「父類、父類的父類……」的代碼。還有,子類和父類高度耦合,修改父類的代碼,會直接影響到子類。
因此,繼承這個特性也是一個很是有爭議的特性。不少人以爲繼承是一種反模式,咱們應該儘可能少用,甚至不用。
多態是指,子類能夠替換父類,在實際的代碼運行過程當中,調用子類的方法實現。對於多態這種特性,純文字解釋很差理解,咱們仍是看一個具體的例子。
public class DynamicArray { private static final int DEFAULT_CAPACITY = 10; private int size = 0; private int capacity = DEFAULT_CAPACITY; private 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(); for (int 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[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()
方法,也就是實現了多態特性。
對於多態特性的實現方式,除了利用」繼承加方法重寫」這種實現方式以外,咱們還有其餘兩種比較常見的的實現方式,一個是利用接口類語法,另外一個是利用 duck-typing
語法。不過,並非每種編程語言都支持接口類或者 duck-typing
這兩種語法機制,好比 C++
就不支持接口類語法,而 duck-typing
只有一些動態語言才支持,好比 Python
、JavaScript
等。
接下來,先來看如何利用接口類來實現多態特性。
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
是一個接口類,定義了一個能夠遍歷集合數據的迭代器。Array
和 LinkedList
都實現了接口類 Iterator
。咱們經過傳遞不一樣類型的實現類(Array
、LinkedList
)到 print(Iterator iterator)
函數中,支持動態的調用不一樣的 next()
、hasNext()
實現。
具體點講就是,當咱們往 print(Iterator iterator)
函數傳遞 Array
類型的對象的時候,print(Iterator iterator)
函數就會調用 Array
的 next()
、hasNext()
的實現邏輯;當咱們往 print(Iterator iterator)
函數傳遞 LinkedList
類型的對象的時候,print(Iterator iterator)
函數就會調用 LinkedList
的 next()
、hasNext()
的實現邏輯。
剛剛講的是用接口類來實現多態特性。再來看下,如何用 duck-typing
來實現多態特性。咱們仍是先來看一段代碼。這是一段 Python
代碼。
class Logger: def record(self): print(「I write a log into file.」) class DB: def record(self): print(「I insert data into db. 」) def test(recorder): recorder.record() def demo(): logger = Logger() db = DB() test(logger) test(db)
從這段代碼中能夠發現,duck-typing
實現多態的方式很是靈活。Logger
和 DB
兩個類沒有任何關係,既不是繼承關係,也不是接口和實現的關係,可是隻要它們都有定義了 record()
方法,就能夠被傳遞到 test()
方法中,在實際運行的時候,執行對應的 record()
方法。
也就是說,只要兩個類具備相同的方法,就能夠實現多態,並不要求兩個類之間有任何關係,這就是所謂的 duck-typing
,是一些動態語言所特有的語法機制。而像 Java
這樣的靜態語言,經過繼承實現多態特性,必需要求兩個類之間有繼承關係,經過接口實現多態特性,類必須實現對應的接口。
多態特性存在的意義是什麼?它能解決什麼編程問題?
多態特性能提升代碼的可擴展性和複用性。爲何這麼說呢?咱們回過頭去看講解多態特性的時候,舉的第二個代碼實例(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
語句等等。
1. 關於封裝特性
封裝也叫做信息隱藏或者數據訪問保護。類經過暴露有限的訪問接口,受權外部僅能經過類提供的方式來訪問內部信息或者數據。它須要編程語言提供權限訪問控制語法來支持,例如 Java
中的 private
、protected
、public
關鍵字。封裝特性存在的意義,一方面是保護數據不被隨意修改,提升代碼的可維護性;另外一方面是僅暴露有限的必要接口,提升類的易用性。
2. 關於抽象特性
封裝主要講如何隱藏信息、保護數據,那抽象就是講如何隱藏方法的具體實現,讓使用者只須要關心方法提供了哪些功能,不須要知道這些功能是如何實現的。抽象能夠經過接口類或者抽象類來實現,但也並不須要特殊的語法機制來支持。抽象存在的意義,一方面是提升代碼的可擴展性、維護性,修改實現不須要改變定義,減小代碼的改動範圍;另外一方面,它也是處理複雜系統的有效手段,能有效地過濾掉沒必要要關注的信息。
3. 關於繼承特性
繼承是用來表示類之間的 is-a
關係,分爲兩種模式:單繼承和多繼承。單繼承表示一個子類只繼承一個父類,多繼承表示一個子類能夠繼承多個父類。爲了實現繼承這個特性,編程語言須要提供特殊的語法機制來支持。繼承主要是用來解決代碼複用的問題。
4. 關於多態特性
多態是指子類能夠替換父類,在實際的代碼運行過程當中,調用子類的方法實現。多態這種特性也須要編程語言提供特殊的語法機制來實現,好比繼承、接口類、duck-typing
。多態能夠提升代碼的擴展性和複用性,是不少設計模式、設計原則、編程技巧的代碼實現基礎。
參考:理論二:封裝、抽象、繼承、多態分別能夠解決哪些編程問題?