1. 面向對象五大基本特徵java
(1)萬物皆對象。你能夠將對象想象成一種特殊的變量。它存儲數據,但能夠在你對其「發出請求」時執行自己的操做。理論上講,你老是能夠從要解決的問題身上抽象出概念性的組件,而後在程序中將其表示爲一個對象。git
(2)程序是一組對象,經過消息傳遞來告知彼此該作什麼。要請求調用一個對象的方法,你須要向該對象發送消息。程序員
(3)每一個對象都有本身的存儲空間,可容納其餘對象。或者說,經過封裝現有對象,可製做出新型對象。因此,儘管對象的概念很是簡單,但在程序中卻可達到任意高的複雜程度。github
(4)每一個對象都有一種類型。根據語法,每一個對象都是某個「類」的一個「實例」。其中,「類」(Class)是「類型」(Type)的同義詞。一個類最重要的特徵就是「能將什麼消息發給它?」編程
(5)同一類全部對象都能接收相同的消息。這實際是別有含義的一種說法,你們不久便能理解。因爲類型爲「圓」(Circle)的一個對象也屬於類型爲「形狀」(Shape)的一個對象,因此一個圓徹底能接收發送給"形狀」的消息。這意味着可以讓程序代碼統一指揮「形狀」,令其自動控制全部符合「形狀」描述的對象,其中天然包括「圓」。這一特性稱爲對象的「可替換性」,是OOP最重要的概念之一。數組
對象更簡潔的描述:一個對象具備本身的狀態,行爲和標識。這意味着對象有本身的內部數據(提供狀態)、方法 (產生行爲),並彼此區分(每一個對象在內存中都有惟一的地址)。安全
2. 接口數據結構
每一個對象僅能接受特定的請求。咱們向對象發出的請求是經過它的「接口」(Interface)定義的,對象的「類型」或「類」則規定了它的接口形式。「類型」與「接口」的對應關係是面向對象程序設計的基礎。編程語言
3. 服務提供工具
軟件設計的基本原則是高內聚:每一個組件的內部做用明確,功能緊密相關。
每一個對象都提供了一組緊密的服務。在良好的面向對象設計中,每一個對象功能單一且高效。這樣的程序設計能夠提升咱們代碼的複用性,同時也方便別人閱讀和理解咱們的代碼。只有讓人知道你提供什麼服務,別人才能更好地將其應用到其餘模塊或程序中。
4. 封裝
咱們能夠把編程的側重領域劃分爲研發和應用。應用程序員調用研發程序員構建的基礎工具類來作快速開發。研發程序員開發一個工具類,該工具類僅嚮應用程序員公開必要的內容,並隱藏內部實現的細節。這樣能夠有效地避免該工具類被錯誤的使用和更改,從而減小程序出錯的可能。彼此職責劃分清晰,相互協做。當應用程序員調用研發程序員開發的工具類時,雙方創建了關係。應用程序員經過使用現成的工具類組裝應用程序或者構建更大的工具庫。若是工具類的建立者將類的內部全部信息都公開給調用者,那麼有些使用規則就不容易被遵照。由於前者沒法保證後者是否會按照正確的規則來使用,甚至是改變該工具類。只有設定訪問控制,才能從根本上阻止這種狀況的發生。
所以,使用訪問控制的緣由有如下2點:
(1)讓應用程序員不要觸摸他們不該該觸摸的部分。(請注意,這也是一個哲學決策。部分編程語言認爲若是程序員有須要,則應該讓他們訪問細節部分。);
(2)使類庫的建立者(研發程序員)在不影響後者使用的狀況下完善更新工具庫。例如,咱們開發了一個功能簡單的工具類,後來發現能夠經過優化代碼來提升執行速度。假如工具類的接口和實現部分明確分開並受到保護,那咱們就能夠輕鬆地完成改造。
Java 有三個顯式關鍵字來設置類中的訪問權限:public
(公開),private(私有),
protected
(受保護)。這些訪問修飾符決定了誰能使用它們修飾的方法、變量或類。
public
(公開) 表示任何人均可以訪問和使用該元素;
private
(私有) 除了類自己和類內部的方法,外界沒法直接訪問該元素。private
是類和調用者之間的屏障。任何試圖訪問私有成員的行爲都會報編譯時錯誤;
protected
(受保護) 相似於 private
,區別是子類(下一節就會引入繼承的概念)能夠訪問 protected
的成員,但不能訪問 private
成員;
default
(默認) 若是你不使用前面的三者,默認就是 default
訪問權限。default
被稱爲包訪問,由於該權限下的資源能夠被同一包(庫組件)中其餘類的成員訪問
5. 複用
一個類經建立和測試後,理應是可複用的。
代碼和設計方案的複用性是面向對象程序設計的優勢之一。咱們能夠經過重複使用某個類的對象來達到這種複用性。
同時,咱們也能夠將一個類的對象做爲另外一個類的成員變量使用。新的類能夠是由任意數量和任意類型的其餘對象構成。
這裏涉及到「組合」和「聚合」的概念:
上圖中實心菱形指向「 Car 」表示 組合 的關係;若是是 聚合 關係,可使用空心菱形。
(注:組合和聚合都屬於關聯關係的一種,只是額外具備總體-部分的意義。至因而聚合仍是組合,須要根據實際的業務需求來判斷。可能相同超類和子類,在不一樣的業務場景,關聯關係會發生變化。只看代碼是沒法區分聚合和組合的,具體是哪種關係,只能從語義級別來區分。聚合關係中,整件不會擁有部件的生命週期,因此整件刪除時,部件不會被刪除。再者,多個整件能夠共享同一個部件。組合關係中,整件擁有部件的生命週期,因此整件刪除時,部件必定會跟着刪除。並且,多個整件不能夠同時共享同一個部件。這個區別能夠用來區分某個關聯關係究竟是組合仍是聚合。兩個類生命週期不一樣步,則是聚合關係,生命週期同步就是組合關係。)
在面向對象編程中常常重點強調「繼承」。在新手程序員的印象裏,或許先入爲主地認爲「繼承應當隨處可見」。沿着這種思路產生的程序設計一般拙劣又複雜。相反,在建立新類時首先要考慮「組合」,由於它更簡單靈活,並且設計更加清晰。等咱們有一些編程經驗後,一旦須要用到繼承,就會明顯意識到這一點。
6. 繼承
經過使用 class
關鍵字,這些概念造成了編程語言中的基本單元。遺憾的是,這麼作仍是有不少麻煩:在建立了一個類以後,即便另外一個新類與其具備類似的功能,你仍是得從新建立一個新類。但咱們若能利用現成的數據類型,對其進行「克隆」,再根據狀況進行添加和修改,狀況就顯得理想多了。「繼承」正是針對這個目標而設計的。但繼承並不徹底等價於克隆。在繼承過程當中,若原始類(正式名稱叫做基礎類、超類或父類)發生了變化,修改過的「克隆」類(正式名稱叫做繼承類或者子類)也會反映出這種變化。
繼承經過基本類型和派生類型的概念來表達這種類似性。基本類型包含派生自它的類型之間共享的全部特徵和行爲。建立基本類型以表示思想的核心。從基類型中派生出其餘類型來表示實現該核心的不一樣方式。
儘管繼承有時意味着你要在接口中添加新方法(尤爲是在以extends關鍵字表示繼承的Java中),但並不是總需如此。第二種也是更重要地區分派生類和基類的方法是改變現有基類方法的行爲,這被稱爲覆蓋(overriding)。要想覆蓋一個方法,只須要在派生類中從新定義這個方法便可。
7. 多態
若是咱們把派生的對象類型統一當作是它自己的基礎類型(「圓」看成「形狀」,「自行車」看成「車」,「鸕鶿」看成「鳥」等等),編譯器(compiler)在編譯時期就沒法準確地知道什麼「形狀」被擦除,哪種「車」在行駛,或者是哪一種「鳥」在飛行。
這就是關鍵所在:當程序接收這種消息時,程序員並不想知道哪段代碼會被執行。「繪圖」的方法能夠平等地應用到每種可能的「形狀」上,形狀會依據自身的具體類型執行恰當的代碼。
若是不須要知道執行了哪部分代碼,那咱們就能添加一個新的不一樣執行方式的子類而不須要更改調用它的方法。那麼編譯器在不肯定該執行哪部分代碼時是怎麼作的呢?
舉個例子,下圖的 BirdController 對象和通用 Bird 對象中,BirdController 不知道 Bird 的確切類型卻還能一塊兒工做。從 BirdController 的角度來看,這是很方便的,由於它不須要編寫特別的代碼來肯定 Bird 對象的確切類型或行爲。那麼,在調用 move() 方法時是如何保證發生正確的行爲(鵝走路、飛或游泳、企鵝走路或游泳)的呢?
經過繼承,程序直到運行時才能肯定代碼的地址,所以發送消息給對象時,還須要其餘一些方案。爲了解決這個問題,面嚮對象語言使用後期綁定的概念。
當向對象發送信息時,被調用的代碼直到運行時才肯定。編譯器確保方法存在,並對參數和返回值執行類型檢查,可是它不知道要執行的確切代碼。
爲了執行後期綁定,Java 使用一個特殊的代碼位來代替絕對調用。這段代碼使用對象中存儲的信息來計算方法主體的地址。
所以,每一個對象的行爲根據特定代碼位的內容而不一樣。當你向對象發送消息時,對象知道該如何處理這條消息。
在某些語言中,必須顯式地授予方法後期綁定屬性的靈活性。例如,C++ 使用virtual關鍵字。在這些語言中,默認狀況下方法不是動態綁定的。
在 Java 中,動態綁定是默認行爲,不須要額外的關鍵字來實現多態性。
爲了演示多態性,咱們編寫了一段代碼,它忽略了類型的具體細節,只與基類對話。該代碼與具體類型信息分離,所以更易於編寫和理解。並且,若是經過繼承添加了一個新類型(例如,一個六邊形),那麼代碼對於新類型的 Shape 就像對現有類型同樣有效。所以,該程序是可擴展的。
代碼示例:
1 void doSomething(Shape shape) { 2 shape.erase(); 3 // ... 4 shape.draw(); 5 }
此方法與任何 Shape 對話,所以它與所繪製和擦除的對象的具體類型無關。若是程序的其餘部分使用 doSomething()
方法:
1 Circle circle = new Circle(); 2 Triangle triangle = new Triangle(); 3 Line line = new Line(); 4 doSomething(circle); 5 doSomething(triangle); 6 doSomething(line);
能夠看到不管傳入的「形狀」是什麼,程序都正確的執行了。
這是一個很是使人驚奇的編程技巧。分析下面這行代碼:
1 doSomething(circle);
當預期接收 Shape 的方法被傳入了 Circle,會發生什麼。因爲 Circle 也是一種 Shape,所 以 doSomething(circle)
能正確地執行。也就是說,doSomething()
能接收任意發送給 Shape 的消息。這是徹底安全和合乎邏輯的事情。
這種把子類當成其基類來處理的過程叫作「向上轉型」(upcasting)。在面向對象的編程裏,常常利用這種方法來給程序解耦。再看下面的 doSomething()
代碼示例
1 shape.erase(); 2 // ... 3 shape.draw();
咱們能夠看到程序並未這樣表達:「若是你是一個 Circle ,就這樣作;若是你是一個 Square,就那樣作…...」。若那樣編寫代碼,就需檢查 Shape 全部可能的類型,如圓、矩形等等。這顯然是很是麻煩的,並且每次添加了一種新的 Shape 類型後,都要相應地進行修改
。在這裏,咱們只需說:「你是一種幾何形狀,我知道你能刪掉 erase()
和繪製 draw()
你本身,去作吧,注意細節。」
儘管咱們確實能夠保證最終會爲 Shape 調用 erase()
、 draw()
,但並不能肯定特定的 Circle,Square 或者 Line 調用什麼。最後,程序執行的操做卻依然是正確的,這是怎麼作到的呢?
發送消息給對象時,若是程序不知道接收的具體類型是什麼,但最終執行是正確的,這就是對象的「多態性」(Polymorphism)。面向對象的程序設計語言是經過「動態綁定」的方式來實現對象的多態性的。
編譯器和運行時系統會負責對全部細節的控制;咱們只需知道要作什麼,以及如何利用多態性來更好地設計程序。
8. 單繼承結構
全部的類都應該默認從一個基類繼承:在 Java 中,這個最終的基類的名字就是 Object。
Java 的單繼承結構有不少好處。因爲全部對象都具備一個公共接口,所以它們最終都屬於同一個基本類型。
單繼承的結構使得垃圾收集器的實現更爲容易。
因爲運行期的類型信息會存在於全部對象中,因此咱們永遠不會遇到判斷不了對象類型的狀況。這對於系統級操做尤爲重要,例如異常處理。同時,這也讓咱們的編程具備更大的靈活性。
9. 集合
在一些庫中,一兩個集合泛型集合就能知足咱們全部的需求了,而在其餘(Java)中,不一樣類型的集合對應不一樣的需求:常見的有 List,經常使用於保存序列;Map,也稱爲關聯數組,經常使用於將對象與其餘對象關聯);Set,只能保存非重複的值;
其餘還包括如隊列(Queue)、樹(Tree)、堆(Stack)等等。
從設計的角度來看,咱們真正想要的是一個可以解決某個問題的集合。若是一種集合就知足全部需求,那麼咱們就不須要剩下的了。之因此選擇集合有如下兩個緣由:
(1)集合能夠提供不一樣類型的接口和外部行爲。堆棧、隊列的應用場景和集合、列表不一樣,它們中的一種提供的解決方案可能比其餘靈活得多。
(2)不一樣的集合對某些操做有不一樣的效率。例如,List 的兩種基本類型:ArrayList 和 LinkedList。雖然二者具備相同接口和外部行爲,可是在某些操做中它們的效率差異很大。在 ArrayList 中隨機查找元素是很高效的,而 LinkedList 隨機查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。因爲底層數據結構的不一樣,每種集合類型在執行相同的操做時會表現出效率上的差別。
咱們能夠一開始使用 LinkedList 構建程序,在優化系統性能時改用 ArrayList。經過對 List 接口的抽象,咱們能夠很容易地將 LinkedList 改成 ArrayList。
在 Java 5 泛型出來以前,集合中保存的是通用類型 Object
。Java 單繼承的結構意味着全部元素都基於 Object
類,因此在集合中能夠保存任何類型的數據,易於重用。要使用這樣的集合,咱們先要往集合添加元素。
因爲 Java 5 版本前的集合只保存 Object
,當咱們往集合中添加元素時,元素便向上轉型成了 Object
,從而丟失本身原有的類型特性。這時咱們再從集合中取出該元素時,元素的類型變成了 Object
。那麼咱們該怎麼將其轉回原先具體的類型呢?
這裏,咱們使用了強制類型轉換將其轉爲更具體的類型,這個過程稱爲對象的「向下轉型」。
經過「向上轉型」,咱們知道「圓形」也是一種「形狀」,這個過程是安全的。但是咱們不能從「Object」看出其就是「圓形」或「形狀」,因此除非咱們能肯定元素的具體類型信息,不然「向下轉型」就是不安全的。
也不能說這樣的錯誤就是徹底危險的,由於一旦咱們轉化了錯誤的類型,程序就會運行出錯,拋出「運行時異常」(RuntimeException)。(後面的章節會提到)
不管如何,咱們要尋找一種在取出集合元素時肯定其具體類型的方法。另外,每次取出元素都要作額外的「向下轉型」對程序和程序員都是一種開銷。
以某種方式建立集合,以確認保存元素的具體類型,減小集合元素「向下轉型」的開銷和可能出現的錯誤難道很差嗎?這種解決方案就是:參數化類型機制(Parameterized Type Mechanism)。
參數化類型機制可使得編譯器可以自動識別某個 class
的具體類型並正確地執行。
舉個例子,對集合的參數化類型機制可讓集合僅接受「形狀」這種類型的元素,並以「形狀」類型取出元素。Java 5 版本支持了參數化類型機制,稱之爲「泛型」(Generic)。泛型是 Java 5 的主要特性之一。你能夠按如下方式向 ArrayList 中添加 Shape(形狀):
1 ArrayList<Shape> shapes = new ArrayList<>();
10. 對象建立與生命週期
每一個對象的生存都須要資源,尤爲是內存。爲了資源的重複利用,當對象再也不被使用時,咱們應該及時釋放資源,清理內存。
Java 使用動態內存分配。每次建立對象時,使用 new
關鍵字構建該對象的動態實例。這又帶來另外一個問題:對象的生命週期。較之堆內存,在棧內存中建立對象,編譯器可以肯定該對象的生命週期並自動銷燬它;
然而若是你在堆內存建立對象的話,編譯器是不知道它的生命週期的。在 C++ 中你必須以編程方式肯定什麼時候銷燬對象,不然可能致使內存泄漏。Java 的內存管理是創建在垃圾收集器上的,它能自動發現對象再也不被使用並釋放內存。
垃圾收集器的存在帶來了極大的便利,它減小了咱們以前必需要跟蹤的問題和編寫相關代碼的數量。
Java 的垃圾收集器被設計用來解決內存釋放的問題(雖然這不包括對象清理的其餘方面)。垃圾收集器知道對象何時再也不被使用而且自動釋放內存。
11. 異常處理
異常處理機制將程序錯誤直接交給編程語言甚至是操做系統。
「異常」(Exception)是一個從出錯點「拋出」(thrown)後能被特定類型的異常處理程序捕獲(catch)的一個對象。它不會干擾程序的正常運行,僅當程序出錯的時候才被執行。
最後,「異常機制」提供了一種可靠地從錯誤情況中恢復的方法,使得咱們能夠編寫出更健壯的程序。有時你只要處理好拋出的異常狀況並恢復程序的運行便可,無需退出。
完!
轉載:https://github.com/LingCoder/OnJava8/blob/master/docs/book/01-What-is-an-Object.md