萬物皆對象,萬物對象
一、抽象過程:
1,萬物皆為對象。
狗、房子這種具體的事物是對象,“服務”這種抽象的概念也是對象。
你可以用對象來儲存東西。狗對象可以儲存狗頭,狗腿等。“服務”對象可以儲存服務類型、服務員、顧客等。
你可以要求對象執行某種操作。比如,讓狗叫一聲,讓“服務”對象做一個“送貨上門”的動作。
2,程式是對象的集合
程式中的各個對象通過發送訊息來告訴彼此要做什麼,來合作完成一項任務。比如你調用某個對象的某個方法,“調用”的過程就是“發訊息”。
3,一個對象可以由許多其他對象組成。
4,每一個對象都有自己的類型。
比如,A a=new A();那麼,a的類型就是A,“類”就是“類型”。你建立了一個狗對象,那麼,狗對象的類型就是狗。
類與類區分開來的一個重要因素是:“可以發送什麼樣的訊息給它”,即:每個類都有自己的方法。
5,某一特定類型的對象可以接收同樣的訊息。
接收訊息即調用方法。泰迪類、博美類都屬於狗類。上面這句話的意思就是:因為泰迪和博美都是狗,所以,它們都能夠調用狗的“叫”、“跑”等方法。比如:
BoMei和TaiDi繼承了Dog,所以,它們建立的對象可以調用Dog的方法。
這就意味著,我們要編寫BoMei或者TaiDi相關的代碼時,只需要編寫Dog相關代碼就可以,這就是OOP(物件導向設計)中最強有力的概念之一。如下:
我給Dog定義了legs屬性。在test(Dog dog)方法中,我傳入的參數是“Dog”,操作的也是Dog類的屬性。但是當我在16行調用這個方法時,我傳的參數是“BoMei”對象,此時,操作的就是BoMei。
同理,當我們要操作的是TaiDi對象時,給test傳入TaiDi對象就行。我們就不必分別為TaiDi和BoMei再定義一個test(TaiDi ta)、test(BoMei bo)方法。
6,每個對象都是唯一的
對象都有狀態、行為、標識、類型。比如上面的BoMei類,“狀態”就是“變數”,就是“屬性”,上面的BoMei類,legs就是它的“狀態”。行為就是“方法”、“函數”,對應上面的bark()、eat()等。行為可以改變狀態,比如上面的test(Dog dog)改變了legs屬性的值。類型就是對象所屬的類,而“標識”就相當於身份證一樣,這在java中表現為:每個對象在記憶體中都有唯一的地址。
二、每個對象都有一個介面
蘋果有皮、有核,是甜的,長在樹上。梨也有皮,也有核,也是甜的,也長在樹上。那麼蘋果和梨有很多相似性,被劃分到同一類型:水果類。這個“水果類”就是一個抽象資料類型(所謂的抽象,就是抽取了一些類似的表象東西,比如皮、核、味道、生長環境等)。
抽象資料類型的運作方式與基礎資料型別 (Elementary Data Type)是一致的。比如對於基礎資料型別 (Elementary Data Type),可以這樣使用:
對於抽象資料類型,可以這樣使用:
建立某一類型的變數(建立對象或者執行個體),並調用其方法(發送訊息或請求,讓它知道該做什麼)。
每個具體的對象又有自己特有的狀態,比如,蘋果的皮是紅的,梨子的皮是綠的。蘋果水分少,梨子水分多等。所以,蘋果、梨都是水果類,但是它們又是水果類中獨立的個體,這些個體就是一個個唯一的對象,那麼對象與類的關係就明了了:每一個對象都屬於定義了特性和行為的某個特定的類。編程系統對待抽象資料類型與對待一般資料類型是一視同仁的,也會作類型檢查等:
蘋果和梨子都能吃,能榨果汁,能製作蘋果罐頭等。吃、榨果汁、製作罐頭這屬於行為(也就是方法,也就是請求),蘋果和梨子都滿足這些特定的請求,那麼這些特定的請求就定義在介面中,而介面也是一種類型:
介面只是定義了可以向某一特定對象發出的請求,那麼具體到蘋果、梨子該發出怎樣的請求,剝皮吃還是不剝皮,榨果汁時要注意什麼,這都是具體請求該有的細節,在蘋果類、梨子類中必須有這些具體的細節方法,這些代碼就構成了“實現”。
三、每個對象都提供服務
不要試圖把所有功能都擁擠在一個對象裡,完成一項服務項目需要各個對象的配合,比如實現一個列印模組,我們可以定義一個對象專門檢查配置是否正常,另一個對象定義怎樣列印一張4A圖紙,再定義一個對象集合,調用前兩個對象,再加之自己的方法最終把圖紙列印出來。每個對象都可以很好的完成一個任務,但並不試圖做更多的事情。然後這些對象齊心協力去完成一項服務。
上述的“齊心協力”其實就是軟體設計的基本品質要求之一:高內聚。
四、存取控制
Java用三個關鍵字控制了變數及方法的訪問:public、private、protected。public其他類都可以訪問,private只有本類及類的內部方法能夠訪問,其他類不可見。protected表示只有本類及繼承本類的子類可見,其他不可見。除此之外,Java還有一種預設的存取權限:包存取權限,類可以訪問同一個包中的其他類成員。
存取控制的原因1:讓用戶端程式員(類的調用者)無法觸及他們不該觸及的部分。調用者只需要調用有用的方法,有些變數等是設計者為了實現內部邏輯而使用的,不需要讓用戶端程式員知道。如果把那些私人變數公開化,既會干擾到用戶端程式員的調用思路,也可能被用戶端程式員誤操作而修改了狀態值。
存取控制的原因2:類庫的設計者可以改變內部的工作方式而不會影響到外部客戶端程式員的調用。
五、組合,彙總,代碼複用
代碼複用是物件導向程式設計語言所提供的最了不起的優點之一。
直接用某個類建立一個對象也屬於複用,下面第6行就是在複用Apple類。
將某個類的對象置於某個新類中(建立一個成員對象),也屬於複用:
上例中是用Fruit、Dog合成了Test類,所以,這個概念稱為:組合(composition)。如果組合是動態發生的,則稱為“彙總(aggregation)”。
什麼是動態發生呢?看:
第7行並沒有建立Apple執行個體,等到11行調用時,才執行個體化了Apple,這就是動態發生。
組合的關係是:has-a,即:汽車擁有引擎、公司擁有員工、Test擁有Fruit、Dog。
六、繼承
複製現有的類,然後添加和修改這個複製品來建立新類,這就是繼承。當源類(又叫:父類、超類、基類)發生變動時,被修改的子類也會發生這些變動。
上面Circle繼承了Shape,自然也就繼承了Shape的color屬性,以及getColor()、setColor()方法、draw()方法。在繼承過來的同時,Circle修改了draw()方法,表現出自己與父類不同的地方(這一行為叫做override,即:覆蓋)。當然,Circle也可以添加自己的新方法。
如果我把父類的getColor方法修改一下:
那麼子類的這個方法就跟著發生了改變。
父類含有所有子類所共用的特性和行為。比如上例,color變數是共用的(color屬於特性),draw()方法也是共用的(draw()屬於行為)。
父類與子類有著相同的類型。
從上面代碼可以看出,Circle類型同時也是Shape類型,這個很容易理解,博美是狗,泰迪是狗,圓是圖形,沒毛病。
理解物件導向設計的重要門檻是理解:通過繼承而產生的類型等價性。
使父類與子類產生差異的兩種方法:
1, 添加新方法。(此時,子類與父類的關係是:is like a)
2, 覆蓋。(只覆蓋而不添加新方法的話,子類與父類的關係是:is a)。
七、向上轉型
上面代碼中,test()裡面的參數為父類Shape,調用的也是父類Shape的draw()方法。當在main方法中調用test時,卻傳入了子類Circle對象,test方法也可以正常調用,且這時,調用的是Circle的draw()方法。
經常需要這樣,把一個子類對象當做它的父類型來對待,這樣做的好處是,不依賴特定類型的代碼,也不受添加新類型的影響。比如,下一次你傳入一個正方形子類,那麼test內部就會自動調用正方形的draw()方法。你如果添加一個新類:六角形,然後把六角形對象傳入test,它也能夠正常調用。
物件導向設計語言採用的是“後期綁定”。當test()方法具體調用時,才能確定參數所對應的具體類型。
上述把子類當做父類型的過程叫做“向上轉型,upcasting”。
八、容器
我們有時需要管理很多個物件,我們不知道需要多少個對象,不知道這些對象能夠活多久,不知道儲存這些對象需要多大空間,一問三不知。
容器(集合)協助我們解決了上述問題。我們把對象放入容器中,在任何時候都可以去擴充它。
Java中具有滿足各種需要的容器。比如:List(有序的對象集合),Map(建立對象之間的關聯,也叫映射),Set(每種對象只有一個,不會重複,類似於數學中的集合),當然,還有隊列(先放進去的對象先出來,FIFO)、樹(以任意順序把許多個物件放進該容器,當你遍曆時每個值已經排好序)、堆棧(最後放進去的對象先出來,LIFO隊列)等。
對象的關聯比如:
把”張三”與”NAME”關聯,把”17”與”AGE”關聯,這樣,在擷取”張三”時,只需要如下:
1,不同的容器作用不同(介面不同、方法不同)。
比如:下面是Stack(堆棧)的繼承層次及介面方法:
下面是Queue的繼承層次及方法:
二者不在一個繼承樹下,且各自的方法功能不相同。實際上,Stack(堆棧)是一種後進先出的模式,只能在棧頭進行插入與刪除操作。Queue(隊列)是一種先進先出的模式,只能在隊尾進行插入,在隊頭進行刪除。
2,不同的容器效能不同
比如:ArrayList和LinkedList。如果是隨機訪問一個元素,對於ArrayList來說時間是固定的,而LinkedList需要從第一個元素開始尋找,直到找到目標元素,如果目標元素在容器的末端,那麼就要花費更多時間。
如果是插入一個元素,對於ArrayList,插入位置後面的元素統統要往後移動一位(想想實際生活中的插隊),而對於LinkedList來說,只需要把插入位置的兩個對象拆開,然後把目標元素加入進來即可(想想實際生活中小朋友們手拉手的情況)。是LinkedList的插入與刪除:
九、泛型,向下轉型
Java中,所有對象都繼承自Object,那麼由向上轉型規律可知,能存放Object的容器,就能存放任何Java對象。
其實容器裡放置的並不是對象本身,而只是對象的”引用”,指向對象的地址。
當你把一個非Object對象(比如String)的引用放進一個容器時,由於該容器只能存放Object引用,所以,它將會強制轉成Object引用。那麼當你再次取出該引用時,就變成了Object引用,而不是你想要的String。如下:
雖然存入List中的是dog的引用,而把它賦值給dog2時,卻報錯,因為dog.get(0)取出來的是Object類型的引用。這時,就要用到向下轉型。
向上轉型是把子類當作父類來用,而向下轉型是把父類型轉換為一個更具體的類型:
比如,把Object引用強制轉換為Dog。
向上轉型是安全的,比如你可以大膽的說:蘋果是水果,梨子是水果,所以,你可以大膽的把任何對象強制轉成Object:
向下轉型卻是不安全的,你只知道它是一條狗,但是你不知它具體是什麼種類:
上例把泰迪放入List,取出來時是Object,卻錯誤的把它轉換成博美,結果發生了錯誤。
為了避免上面種種風險的發生,Java中引入了泛型:
如,明確說明List狗窩中只能放泰迪,那麼你放入博美就會直接報錯。
如,我已經知道List狗窩裡面睡的是泰迪,你取出來把它喊成博美,那麼也會報錯。
十、對象的生命週期
出生:
當你每次new一個對象時,Java就動態地在一個稱為“堆”的記憶體池中建立一個對象。由於是動態,所以,直到運行時才能知道要建立多少個對象,對象的生命週期是什麼,以及對象的具體類型是什麼。再搬出前面出現過的一段代碼來加深對“動態”的瞭解:
上例中,直到運行test()時才會建立Apple執行個體。
死亡:
Java的記憶體回收機制會自動探索對象什麼時候不用了,然後去銷毀它,以達到釋放記憶體的目的。
Java判斷某個對象是否可以回收是一件複雜的事情。比如,一般情況下,建立一個對象,會在堆中產生一個對象,而會在棧中放一個該對象的引用(一個數字,指向這個對象在堆中的地址),記憶體回收行程有一種早期策略,每產生一個引用,計數器就會+1,而每減少一個引用,計數器就會-1,當發現計數器為0時,說明該對象可以回收了。如:
圖中第四步,per2指向了per1引用的地址,那麼就沒有引用指向age=20的那個對象了,於是,那個對象會被記憶體回收行程回收。
十一、異常處理
異常是一種對象,當程式發生錯誤時,它從那個錯誤點“拋出”,然後由該錯誤對應的專門的處理器“抓住”。所以,如果代碼是正常的,異常代碼將不會發生。
有些異常是在啟動並執行時候才能發現的,主要是由於程式員的失誤造成的,這類異常叫做“運行時異常”,比如:
你明知道test不能轉成整數,還非要轉,運行時就會報錯。
還有一種異常,叫“非運行時異常”,就是運行前就該有所防範的,比如想從某個路徑下載入一個檔案,這種情況下,就有找不到這個檔案的可能性,那麼,你必須做出防患於未然,可以拋出異常:
也可以捕獲異常:
十二、並發編程
為了提高響應能力,我們想把一個任務分成多個子任務獨立的運行,讓他們一起幹活。這些獨立啟動並執行子任務就是”線程”,而“一起幹活”就是“並發”。
實際上,在單一處理器環境中,線程之間是輪番交叉啟動並執行,由處理器來分給每個線程時間。而在多處理器上才存在真正的”一起幹活”,即“並行”。
多線程並發完成一項工作的過程中,資源共用就帶來隱患,比如桌子上有一個粉筆(資源),兩個小朋友(線程)同時伸手去拿粉筆(搶佔資源),那麼就會打起來。所以,就要有一種機制,當一個小朋友要去拿粉筆時,先把粉筆保護起來(加鎖),等這個小朋友不用了,再釋放鎖,另一個小朋友才可以用粉筆。
更多內容請關註: