天天看點

碼出高效:Java開發手冊-第2章(1)第2章 面向對象

第2章 面向對象

“一樹一菩提,一‘門’一世界。”一切皆對象,萬物有三問:我是誰?我從哪裡來?我到哪裡去?

碼出高效:Java開發手冊-第2章(1)第2章 面向對象

本章開始講解面向對象思想,并以Java 為載體講述面向對象思想在具體程式設計語言中的運用與實踐。目前主流的程式設計語言有50 種左右,主要分為兩大陣營:面向對象程式設計與面向過程程式設計。

面向對象程式設計(Object-Oriented Programming,OOP)是劃時代的程式設計思想變革,推動了進階語言的快速發展和工業化程序。OOP 的抽象、封裝、繼承、多态的理念使軟體大規模化成為可能,有效地降低了軟體開發成本、維護成本和複用成本。面向對象程式設計思想完全不同于傳統的面向過程程式設計思想,使大型軟體的開發就像搭積木一樣隔離可控、高效簡單,是當今程式設計領域的一股勢不可當的潮流。OOP 實踐了軟體工程的三個主要目标:可維護性、可重用性和可擴充性。

2.1 OOP 理念

面向過程讓計算機有步驟地順序地做一件事情,是一種過程化的叙事思維。但是在大型軟體開發過程中,發現用面向過程語言開發,軟體維護、軟體複用存在着巨大的困難,代碼開發變成了記流水賬,久而久之就成為“面條”代碼,子產品之間互相耦合,流程互相穿插,往往牽一發而動全身。面向對象提出一種計算機世界裡解決複雜軟體工程的方法論,拆解問題複雜度,從人類思維角度提出解決問題的步驟和方案。

比如“開門”這個動作,面向過程是“open(Door door)”,動賓結構,“door”是被作為操作對象的參數傳入方法的,方法内定義開門的具體步驟實作。而在面向對象的世界裡,首先定義一個對象“Door”,然後抽象出門的屬性和相關操作,屬性包括門的尺寸、顔色、開啟方式(往外開還是往内開)、防盜功能等;門這個對象的操作必然包括open() 和close() 兩個必備的行為,主謂結構。面向過程的代碼結構相對松散,強調如何流程化地解決問題;面向對象的思維更加内聚,強調高内聚、低耦合,先抽象模型,定義共性行為,再解決實際問題。

但是,程式設計語言僅是一個工具,就像練武之人的劍,武功高者草木皆劍,武功差者即使倚天劍在身也依然平庸。是以,能否将工具的價值發揮得淋漓盡緻,最終還是取決于開發工程師本身。優秀的開發工程師用面向過程的語言也能把程式寫得非常内聚,可擴充性好,具備一定的複用性;而平庸程式員用面向對象語言一樣能把程式寫得松散随意、毫無抽象與模組化、子產品間耦合嚴重、維護性差。

傳統意義上,面向對象有三大特性:封裝、繼承、多态。本書明确将“抽象”作為面向對象的特性之一,支援面向對象“四大特性”的說法。抽象是程式員的核心素質之一,展現出程式員對業務的模組化能力,以及對架構的宏觀掌控力。雖然面向過程也需要進行一定的抽象能力,但是相對來說,面向對象思維,以對象模型為核心,豐富模型的内涵,擴充模型的外延,通過模型的行為組合去共同解決某一類問題,抽象能力顯得尤為重要;封裝是一種對象功能内聚的表現形式,使子產品之間耦合度變低,更具有維護性;繼承使子類能夠繼承父類,獲得父類的部分屬性和行為,使子產品更有複用性;多态使子產品在複用性基礎上更加有擴充性,使系統運作更有想象空間。

抽象是面向對象思想最基礎的能力之一,正确而嚴謹的業務抽象和模組化分析能力是後續的封裝、繼承、多态的基礎,是軟體大廈的無形基石。在面向對象的思維中,抽象分為歸納和演繹。前者是從具體到本質,從個性到共性,将一類對象的共同特征進行歸一化的邏輯思維過程;後者則是從本質到具體,從共性到個性,逐漸形象化的過程。在歸納的過程中,需要抽象出對象的屬性和行為的共性,難度大于演繹。演繹也是一種抽象思維,并非是具像思維。如果人對理論的認知與了解存在誤區,那麼推理的過程一定會産生偏差,演繹的結果可能是一個抽象結果,并非一定是一個具體的對象或者物體,比如從化合物到食物,從食物到水果,都還是在抽象層面上。演繹是在已有問題多個解決方案的基礎上,正确地找到合适的使用場景。在使用集合時,演繹錯誤比較常見,比如針對查多改少的業務場景,使用連結清單是非常不合理的;如果在底層架構技術選型時有錯誤,則有可能導緻技術架構完全不适應業務的快速發展。

Java 之父 Gosling 設計的Object 類,是任何類的預設父類,是對萬事萬物的抽象,是在哲學方向上進行的延伸思考,高度概括了事物的自然行為和社會行為。我們都知道哲學的三大經典問題:我是誰,我從哪裡來,我到哪裡去。在Object 類中,這些問題都可以得到隐約的解答:

(1)我是誰? getClass() 說明本質上是誰,而toString() 是目前我的名片。

(2)我從哪裡來? Object() 構造方法是生産對象的基本方式,clone() 是繁殖對象的另一種方式。

(3)我到哪裡去? finalize() 是在對象銷毀時觸發的方法。

這裡重點介紹clone() 方法,它分為淺拷貝、一般深拷貝和徹底深拷貝。淺拷貝隻複制目前對象的所有基本資料類型,以及相應的引用變量,但沒有複制引用變量指向的實際對象;而徹底深拷貝是在成功clone 一個對象之後,此對象與母對象在任何引用路徑上都不存在共享的執行個體對象,但是引用路徑遞歸越深,則越接近JVM 底層對象,會發現徹底深拷貝實作難度越大,因為JVM 底層對象可能是完全共享的。介于淺拷貝和徹底深拷貝之間的都是一般深拷貝。歸根結底,慎用Object 的clone() 方法來拷貝對象,因為對象的clone() 方法預設是淺拷貝,若想實作深拷貝,則需要覆寫clone() 方法實作引用對象的深度周遊式拷貝。

另外,Object 還映射了社會科學領域的一些問題:

(1)世界是否因你而不同? hashCode() 和equals() 就是判斷與其他元素是否相同的一組方法。

(2)與他人如何協調? wait() 和notify() 是對象間通信與協作的一組方法。随着時代的發展,當初抽象的模型已經有部分不适用當下的技術潮流,比如finalize() 方法在JDK9 之後直接被标記為過時方法。而wait() 和notify() 同步方式事實上已經被同步信号、鎖、阻塞集合等取代。

封裝是在抽象基礎上決定資訊是否公開,以及公開等級,核心問題是以什麼樣的方式暴露哪些資訊。抽象是要找到屬性和行為的共性,屬性是行為的基本生産資料,具有一定的敏感性,不能直接對外暴露;封裝的主要任務是對屬性、資料、部分内部敏感行為實作隐藏。對屬性的通路與修改必須通過定義的公共接口來進行,某些敏感方法或者外部不需要感覺的複雜邏輯處理,一般也會進行封裝。封裝使面向對象的世界變得單純,對象之間的關系變得簡單,各人自掃門前雪,耦合度變弱,有利于維護。智能化的時代,對封裝的要求越來越高,産品使用更加簡單友善、輕松自然。就像天貓精靈,與使用者互動的唯一接口就是語音輸入,隐藏了指令内部的細節實作和相關資料,這些資訊外部使用者無法通路,即大大降低了使用成本,又有效地保護内部資料安全。

設計模式七大原則之一的迪米特法則就是對于封裝的具體要求,即A 子產品使用B子產品的某個接口行為,對B 子產品中除此行為之外的其他資訊知道得盡可能少。比如:耳塞的插孔就是提供聲音輸出的行為接口,隻需關心這個插孔是否有相應的耳塞标記,是否是圓形的,有沒有聲音即可,至于内部CPU 如何運算音頻資訊,以及各個電容如何協同工作,根本不需要去關注,這使子產品之間的協作隻需忠于接口、忠于功能實作即可。

封裝這件事情是由儉入奢易,由奢入儉難。屬性值的通路與修改需要使用相應的getter/setter 方法,而不是直接對public 的屬性進行讀取和修改,可能有些程式員存在疑問,既然通過這兩個方法來讀取和修改,那與直接對屬性進行操作有何差別?如果某一天,類的提供方想在修改屬性的setter 方法上進行鑒權控制、日志記錄,這是在直接通路屬性的情形中無法做到的。若是将已經公開的屬性和行為直接暴力修改為private,則依賴子產品都會編譯出錯。是以,在不知道什麼樣的通路控制權限合适的時候,優先推薦使用private 控制級别。

繼承是面向對象程式設計技術的基石,允許建立具有邏輯等級結構的類體系,形成一個繼承樹,讓軟體在業務多變的客觀條件下,某些基礎子產品可以被直接複用、間接複用或增強複用,父類的能力通過這種方式賦予子類。繼承把枯燥的代碼世界變得更有層次感,更有擴充性,為多态打下文法基礎。

人人都說繼承是is-a關系,那麼如何衡量目前的繼承關系是否滿足is-a 關系呢?判斷标準即是否符合裡氏代換原則(Liskov Substitution Principle,LSP)。LSP 是指任何父類能夠出現的地方,子類都能夠出現。從字面上很難深入了解,先打個比方,警察在槍戰片中經常說:放下武器,把手舉起來!而對面的匪徒們有的使用手槍,有的使用匕首,這些都是武器的子類。父類出現的地方,即“放下武器”,那麼,放下手槍,是對的,放下匕首,也是對的!在實際代碼環境中,如果父類引用直接使用子類引用來代替,可以編譯正确并執行,輸出結果符合子類場景的預期,那麼說明兩個類之間符合LSP 原則,可以使用繼承關系。

繼承的使用成本很低,一個關鍵字就可以使用别人的方法,似乎更加輕量簡單。想複用别人的代碼,跳至腦海的第一反應是繼承它,是以繼承像抗生素一樣容易被濫用,我們傳遞的理念是謹慎使用繼承,認清繼承濫用的危害性,即方法污染和方法爆炸。方法污染是指父類具備的行為,通過繼承傳遞給子類,子類并不具備執行此行為的能力,比如鳥會飛,鴕鳥繼承鳥,發現飛不了,這就是方法污染。子類繼承父類,則說明子類對象可以調用父類對象的一切行為。在這樣的情況下,總不能在繼承時,添加注釋說明哪幾個父類方法不能在子類中執行,更不能覆寫這些無法執行的父類方法,抛出異常,以阻止别人的調用。方法爆炸是指繼承樹不斷擴大,底層類擁有的方法雖然都能夠執行,但是由于方法衆多,其中部分方法并非與目前類的功能定位相關,很容易在實際程式設計中産生選擇困難症。比如某些綜合功能的類,經過多次繼承後達到上百個方法,造成了方法爆炸,因而帶來使用不便和安全隐患。在實際故障中,因為方法爆炸,父類的某些方法簽名和子類非常相似,在IDE 中,輸入類名+ 點之後,在自動提示的極為相似的方法簽名中選擇錯誤,導緻線上異常。綜上所述,提倡組合優先原則來擴充類的能力,即優先采用組合或聚合的類關系來複用其他類的能力,而不是繼承。

多态是以上述的三個面向對象特性為基礎,根據運作時的實際對象類型,同一個方法産生不同的運作結果,使同一個行為具有不同的表現形式。多态是面向對象天空中絢麗多彩的禮花,提升了對象的擴充能力和運作時的豐富想象力。我們來明确兩個非常容易混淆的概念:“override”和“overload”,“override”譯成“覆寫”,是子類實作接口,或者繼承父類時,保持方法簽名完全相同,實作不同的方法體,是垂直方向上行為的不同實作。“overload”譯成“重載”,方法名稱是相同的,但是參數類型或參數個數是不相同的,是水準方向上行為的不同實作。多态是指在編譯層面無法确定最終調用的方法體,以覆寫為基礎來實作面向對象特性,在運作期由JVM 進行動态綁定,調用合适的覆寫方法體來執行。重載是編譯期确定方法調用,屬于靜态綁定,本質上重載的結果是完全不同的方法,是以本書認為多态專指覆寫。自然界的多态最典型例子就是碳家族,據說某化學家告訴他女朋友将在她的生日晚會上送她一塊碳,女朋友當然不高興,可收到的卻是5 克拉的鑽石。鑽石就是碳元素在不斷進化過程中的一種多态表現。嚴格意義上來說,多态并不是面向對象的一種特質,而是一種由繼承行為衍生而來的進化能力而已。