2018年11月7日 星期三

Java SE 入門教學 - 物件導向

更新時間:11/07/2018

前言

在現代意義上的物件導向編程中引用「物件」和「導向」的術語在20世紀50年代末和60年代初首次出現在麻省理工學院。早在1960年人工智能組的環境中,「物件」就可以指具有屬性(properties, attributes)的識別項(LISP原子);Alan Kay 後來引用了對LISP內部的詳細理解,對他1966年的思想產生了強烈的影響。

在1960年的 Simula 語言中即可發現,當時的程式設計領域正面臨著一種危機:在軟硬體環境逐漸複雜的情況下,軟體如何得到良好的維護?物件導向程式設計在某種程度上通過強調可重複性解決了這一問題。1970年代的 Smalltalk 語言在物件導向方面堪稱經典——以至於多年後的今天依然將這一語言視為物件導向語言的基礎。

Smalltalk 的建立者深受 Simula 67 的主要思想影響,但 Smalltalk 中的物件是完全動態的——它們可以被建立、修改並銷毀,這與 Simula 中的靜態物件有所區別。此外,Smalltalk 還引入了繼承性的思想,它因此一舉超越了不可建立實體的程式設計模型和不具備繼承性的 Simula。

物件導向程式設計在1980年代成為了一種主導思想,這主要應歸功於 C++ —— C語言的擴充版。在圖形化使用者介面(GUI)日漸崛起的情況下,物件導向程式設計很好地適應了潮流。

James Gosling,Mike Sheridan 和 Patrick Naughton 於1991年6月發起了 Java 語言項目。Java 最初是為交互式電視而設計的,但它對於當時的數字有線電視行業來說太先進了。這種語言最初被稱為 Oak(橡樹),源於 Gosling 辦公室外的一棵 Oak。後來這個項目名為 Green,最後改名為 Java,來自 Java coffee。Gosling 使用 C / C ++ 風格的語法設計 Java,系統和應用程序程序員會覺得這種語法很熟悉。Sun Microsystems 於1996年發行 Java 1.0。

一、函數導向和物件導向的思想

物件導向和函數導向的思想有著本質上的區別,做為物件導向的思維來說,當您拿到一個問題時,您分析這個問題不在是第一步先做什麼,第二步再做什麼,這是物件導向的思維,您應該分析這個問題裡面有哪些類別和物件,這是第一點,然後再分析這些類別和物件應該具有那些屬性和方法,這是第二點。最後分析類別和類別之間具體有什麼關係,這是第三點。

物件導向有一個非常重要的設計思維:合適的屬性與方法應該出現在合適的類別裡面。

二、物件導向的設計思想

物件導向的基本思想是,從現實世界中客觀存在的事物出發來構造軟件系統,並在系統的構造中盡可能運用人類的自然思維方式。

物件導向更加強運用人類在日常生活的邏輯思維中經常採用的思想方法與原則,如抽象、分類、繼承、聚合、多型等。

人在思考的時候,首先眼睛裡看到的是一個一個的物件。

三、物件和類別概念

3.1 物件實例

物件是用於計算機語言對問題域中事務的描述物件通過「屬性(attribute)」和「方法(method)」來分別對應事物所具有的靜態屬性和動態屬性。

3.2 類別

類別是用於描述同一個類的物件概念,類別中定義了這一類物件所具有的靜態屬性和動態屬性。

3.3 微妙關係

類別可以看成一類物件的模板,物件可以看成該類的一個具體實例。

3.4 例如:什麼叫瓶子?

瓶子的定義具有某些類特徵的東西就是瓶子,比如說什麼樣的形狀,比方說有個口,能倒水,能裝水,一般有個蓋等等。給瓶子下定義的過程,其實就是把瓶子裡的某些東西抽象出來了,所以瓶子在這裡是叫做一類事物的一個抽象,在你腦子裡有瓶子的概念,可瓶子的概念在你腦子裡到底是什麼呢?

瓶子的概念在你腦子裡叫做一類事物的一個抽象。怎麼抽象的呢?你往往抽象的是這兩個方面:第一個方面我們叫它靜態的屬性,瓶子應該具有哪些特徵,比分說瓶子應有個口,這是它具有的一個靜態屬性,瓶子一般有一個蓋,這也是它具有一個靜態屬性,除此之外,你還可能給它總結動態的屬性,什麼動態的屬性呢?比放說瓶子能倒水,這是它的動態屬性。瓶子這個概念在你腦子裡如果你細細的思維的話,其實你給它做了兩方面的總結,一方面是靜態的,一方面是動態的。

反映到 JAVA 的類別上,一個就是成員變數(靜態屬性),一個就是方法(動態屬性)。方法是可以執行的,可以動的。成員變量是某一個類的靜態屬性。所以你腦子裡瓶子的概念實際上是一類事物的一個抽象,這種東西我們叫它類,椅子是類,桌子是類,學生是類。

什麼是物件呢?這一類事物具體的某個實例就叫做物件。所以一類事物具體的某一個東西,符合這類事物具體的特徵的某個東西就叫做物件。瓶子是一個類,某個瓶子就是瓶子這個類裡面的一個物件。

四、如何抽象出一個類別

有兩方面,一方面是它的靜態屬性,另一方面是它的動態屬性。反映到 JAVA 裡面的類別怎麼包裝它呢?一方面成員變數,另一方面是方法。

例如:職員這個類怎麼抽象出來?

職員有哪些屬性呢?有姓名、年齡、薪水等屬性。職員有哪些方法呢?讓這個職員來顯示姓名、顯示年齡、修改姓名、領取薪水。當然顯示姓名、顯示年齡、修改姓名、領取薪水這些也可以讓別人來做,但物件導向的設計思維是最適合的方法應該出現在最適合的類別裡面。顯示姓名、顯示年齡、修改姓名、領取薪水由誰來做更合適呢?那就是職員自己最合適。所以這些方法應該出現在職員這個類別裡面。

對於類別來說,它有一些屬性或者稱為成員變數,以後說屬性或者成員變數指的是同一回事。

具體的物件有沒有相關的一些屬性或者叫成員變數呢?有,每一個人都有一份,只不過是取值不同而已。如從職員這個類別實例化出來的兩個職員:職員 A 和職員 B ,他們都有姓名、年齡、薪水這些屬性,但他們的名字、年齡、薪水都不一樣。這樣就能把職員 A 和職員 B 區分開來了,正是因為他們的屬性值不一樣,所以這個物件實例才能和另外的物件實例區分開來。

貓是一個類,這隻貓是一個物件,這隻貓和另外一隻貓該怎麼區分開來呢?那就得看你的貓這個類是怎麼定義的了。貓有貓毛,毛有顏色,OK,這隻貓是黑貓,另一隻貓是白貓,這樣通過貓毛的顏色區分開來了。如果只定義一個,如捉老鼠,白貓也能捉,貓也能捉,這樣就沒辦法區分出黑貓和白貓了,所以根據方法是沒辦法區分兩個物件實例的

所以每個物件實例都有自己的屬性,屬性值和另外一個對象一般是不一樣的。

五、類別有什麼?

5.1 類別內 non-static 屬性與方法

根據上述職員(Employee)的例子,寫成類別,再寫一個類別(TestClass1)來展示結果,這兩個類別都寫在同一個檔案,檔名存 TestClass1.java。

在 Employee 有「成員變數(Field)」、「建構子(Constructor)」和「方法(Method)」。這三個是組成類別的要素。
在 TestClass1 建立兩個物件實例

Employee A = new Employee("A", 30, 8000);
Employee B = new Employee("B", 35, 9000);

您會發現跟建立陣列時的寫法很像,只是型態現在變成 類別資料型態 Employee

建立實例的方式使用「new 建構子();」,因為 Employee 建構子帶有三個參數,所以分別給姓名、年齡和薪水。

物件實例建立好後,使用「物件實例.成員變數」或「物件實例.方法」取得相關數值或方法。


5.2 類別內 static 屬性與方法

上例的修飾子全都是 default,non-static 的成員變數與方法。我們第一個 Java 程式就只有一個 public static 的 main() 方法,這也可以形成一個類別。如果存取修飾值是 static,JVM 會自動載入記憶體,這與 non-static 的記憶體配置空間不同。non-static 的記憶體配置可以參考一維陣列記憶體分配。下方展示 static 方法如何調用與其記憶體配置,存檔為 TestGcd1.java。

類別中如果擁有 static 屬性或方法,JVM 載入至 Global 記憶體中產生一個相同名稱的變數,並在 Heap 區中 new 一塊記憶體放置 static 的屬性和方法。

如 TestGcd1 類別放在 Global 區塊的物件參考變數其位址為 0x3e2460 ,其內容值是一個位址 0x4f1610,映射到 Heap 區一塊足夠大的記憶體空間,用來存放 static 的 main() 這個方法。

相同的,Gcd1 類別在 Global 區塊建立一個物件參考變數 0x3e2480,其內容值為 0x4f1630,映射到 Heap 區可以找到 static int gcdOf(int x, int y) 的方法。

配置好 static 的部分後,JVM 從進入點 main()開始執行,首先碰到
int result = Gcd1.gcdOf(10, 5);
這行指令,發現 result 是 non-static,所以 result 變數會在 Statck 區塊建立起來,其位址為 0x22ff10,其值 5 為計算後回傳的結果。

為什麼 Gcd1 類別不需要使用 new 建構子(); 這個指令建立物件就能執行呢?因為已經在 Global 0x3e2480 建立物件實例,其內容值 0x4f1630 就有辦法找到 gcdOf(int x, int y)這個方法執行。


5.3 類別內混和 static、non-static 屬性和方法

當屬性修飾為 static 時,稱為「全域變數(global variable)」,資料共享。不推薦隨意使用全域變數,「合適的屬性與方法應該出現在合適的類別裡面」,所以如果真的要使用全域變數,那一定要重新審視是否真的需要?!

測試 static 與 non-static 屬性的記憶體分配情況,存檔為 TestVar.java。


當一個類別中有 static 的屬性 a 與不是 non-static 的屬性 b 時,Java 載入記憶體的方式不同。

屬性 a 在初始化時,JVM 就自動載入記憶體中,如下圖藍色的部分,可直接使用;但屬性 b,因為 non-static,所以要使用它必須 new 一塊記憶體,如下圖黑色的部分,才能使用。

但因為 new 一塊記憶體,所以這個類別所有的屬性與方法都會放在這一塊新記憶體中,當然 a 的屬性也會存在;但這一塊新記憶體中的 a 依舊指向 JVM 已經載入的位址(映射到藍色的部分)。

Var.a : 10
建立 obj1 : 新的記憶體 a 屬性指向 JVM 已經載入的位址 0x40BD40,其值為 10
obj1.a = obj1.a + 100; : 改變 0x40BD40 位址的值為 110
建立 obj2 : 新的記憶體 a 屬性指向 JVM 已經載入的位址 0x40BD40,其值為 110

當建立 obj1 和 obj2 時,新的記憶體 b 屬性的內容值都會填入 20。所以 obj1.b 與 obj2.b 這兩塊記憶體的值是獨立的,不關聯。


5.4 方法(Method) - 多載(Overloading)

方法多載是一種功能,Java 的類別允許同名的方法,如果符合下列的規則:

◉ 不同的參數個數

add(int, int)
add(int, int, int)

◉ 不同參數的資料型態

add(int, int)
add(int, float)

◉ 如果參數個數且資料型態一樣,但順序不同

add(float, int)
add(int, float)

呼叫時的規則:

◉ 根據呼叫的方法名稱與傳遞的參數值之型別、個數、順序,選擇適當的方法。
◉ 若參數值為基本資料型別,就根據 promotion(晉升)原則做基礎對應

byte → short → int → long → float → double
   char →

◉ 若參數值為參考型別(類別資料),則以「多型」原則做對應;若都沒有對應到,則往下一個原則繼續比對。
◉ 試著用 AutoBoxing/AutoUnboxing 來對應,若沒對應上,則往下個原則繼續比對。
◉ 最後會試著用變動參數做對應

若以上都對應不上,則出現編譯時期錯誤


測試多載1



測試多載2:輸入 short 、 long 或 float ,會根據 promotion(晉升) 原則做基礎對應(byte → short(char) → int → long → float → double)


測試多載3:當輸入 method2(10, 20) 時會拋出例外

ambiguous 混淆...程式會分不清楚使用哪個,因為基本資料型別會 promotion(晉升),導致兩個 method2 都可以使用。


測試多載4:參數值為包覆類別時,試著用 AutoBoxing/AutoUnboxing 來對應。


測試多載5:當輸入 obj.method3(10L, 20L); 或 obj.method3(10F, 20F); 時會拋出例外,會提示沒有適合的方法。


測試多載6:參數值為參考型別(類別資料)時,使用「多型」原則做對應。

5.5 建構子方法(Constructor Method)

用來設定初值,且載入記憶體使用完這些建構子後就會釋放。

◉ 建構子方法是一種特殊的方法
◉ 建構子方法的功能用於宣告屬性變數(成員變數、欄位)的初值
◉ 建構子方法宣告時,名稱必須與類別名稱相同
◉ 建構子方法前不可宣告任何傳回值型別
◉ 建構子在使用 new 關鍵字時才會被呼叫
◉ 預設建構子:每個類別至少有一個建構子。若沒有宣告建構子方法,JAVA 會提供一個預設建構子(沒有參數,也沒有任何實作)

當建立一個類別時,JVM 會建立一個隱藏變數 this,記錄著這個類別的位址。在使用 constructor method 時,可以使用這個隱藏變數 this,增加程式的可塑性。

◉ this 參考用於呼叫類別物件中的成員變數和成員方法。

this.成員變數
this.成員方法

◉ this()用於呼叫類別物件中同名的另一個建構子方法。
◉ this()必須寫在建構子方法的第一行


使用三個不同的建構子,建立三個物件實例。您會發現 obj1 使用預設建構子,而後呼叫帶有一個參數的建構子,所以 obj1.a = 200;而 obj2 呼叫帶有二個參數的建構子後,把 a 的值再加100,故 obj2.a = 300;


使用建構子方法設定初始值



六、物件導向的特性

JAVA 是屬於物件導向的語言,所有的類別都有一個根(Root),那就是 Object 。

6.1 封裝(Encapsulation)

◉ 目的:保護重要的屬性變數或副程式。(讓外部程式不可隨意存取)
◉ 方法:將變數或副程式宣告為 private(私有)
◉ 開發:有限度的開放被保護的屬性變數或副程式

getter 取值
setter 設值

getter 和 setter 必須檢查值的合法性(有效性)與安全性。


將矩形類別內,屬性變數宣告為 private,並在 setter 方法內檢查值的合法性。


封裝的練習:


6.2 繼承(Inheritance)

◉ 目的:不開發重複功能的程式碼
◉ 方法:使用 extends 關鍵字。例如:子類別(subclass) extends 父類別(superclass)
◉ 開發:子類別在使用時,會去呼叫父類別的建構子

其呼叫原則為

1.如果未特別告知,則會去呼叫父類別的預設建構子
2.如果有撰寫 super 關鍵字,則看 super 如何撰寫。

◉ 兩個關鍵字的用法(this、super):

1.呼叫自己類別的建構子:this()

(i) 只能寫在建構子的第一行
(ii) 呼叫自己的屬性變數 this.屬性屬性
(iii) 呼叫自己的方法 this.方法()

2.呼叫父類別的建構子:super()

(i) 只能寫在建構子的第一行。如果子類別想要同時呼叫自己的與父親的建構子,其寫法必須先呼叫自己的建構子,再由自己的其他建構子去呼叫父親的建構子。
(ii) 呼叫父類別的屬性變數 super.屬性屬性
(iii) 呼叫父類別的方法 super.方法()


測試繼承的呼叫方式


類別中只要有寫任何一個建構子,javac 就不會幫您寫預設建構子 public Father(){}


Complier error:Son 類別會去呼叫 Father 類別中的預設建構子,但 Father 類別沒有預設建構子可以呼叫,所以拋出例外


上個例子,在 Son 類別中添加 super(10); 即可成功執行。



繼承關係中,有一個觀念的考題

is a (是一種) 是繼承關係

例:狗是一種動物,汽車是一種交通工具

has a (有一個、擁有什麼) 是擁有關係

例:狗有四條腿,汽車有四個輪子



6.3 覆寫、覆蓋、遮蔽(Override)

◉ 目的:外部程式無法存取父類別中的方法
◉ 方法:在子類別中,宣告一個與父類別同名的方法
◉ 時機:

1.父、子類別中的計算公式不同
2.父類別中的方法,不符合子類別的需求

◉ 覆寫的規則:

覆寫是發生在有繼承關係的類別中,方法的名稱宣告必須與父類別相同。若方法有回傳值,其傳回型態必須與父類別的回傳值相同或是原方法(父類別的方法)傳回值型別的子類別。方法中的參數列(不論數量、資料型態及擺放順序)都必須相同。存取修飾子不可以比原來的父類別中的方法還要嚴謹(即不可降低存取範圍)。若父類別中方法有宣告拋出例外類別,則子類別中的方法也拋出相同的例外類別,或該例外類別的子類別。

若方法的宣告只有名稱一樣但參數不一樣時,此不稱為覆寫而是多載。可以加上選擇性的關鍵字 Override 標註。


實作範例:


◉ Override 與 Overload 兩者觀念容易混淆



Override失敗的例子:存取修飾子權限問題

存取修飾子不可比原來的父類別中的方法還嚴謹。JAVA存取修飾子等級:public > protected > default > private

如果把子類別中錯誤的地方 remark,則可以成功執行且 s.method1(10.0) 是使用繼承過來的父類別方法



方法有傳回值時,如果要 Override,必須遵守規則:

(1)傳回值型態必須與父類別傳回值型態一致。
  或是
(2)原方法(父類別的方法)傳回值型別的子類別。



父類別中的方法有宣告拋出例外類別,如要 Override,必須遵守

(1)子類別中的方法也拋出相同例外類別
(2)該例外類別的子類別
(3)不拋出例外

自定例外的步驟:

(1)產生自訂例外類別
(2)在宣告方法時加 throws 修飾
(3)在方法中使用 (throw new 自訂例外類別) 以丟出例外
(4)在 try-catch 程式區塊內呼叫該方法或再 throws 出去

例外類別的繼承關係

例外類別的範例



七、總結

一定要區分類別和物件,什麼叫做類別?什麼叫做物件?類別是一類事物的一個抽象,具有共同特徵的一類事物的一個抽象。物件是這個類具體的某一個實例,所以以後說實例(instance)或者說物件(object)指的是同一回事「物件實例」。

一個類別包含「成員變數(Filed)」、「建構子(Constructor)」和「方法(Method)」,成員變數與方法可以使用存取修飾子,但建構子不行。方法可以擁有同名但參數有所不同的「多載(Overload)」。建構子內可以使用 this() 和 super() 方法,this 代表自己的類別,而 super 代表父類別。

物件導向其特性有「封裝(Encapsulation)」、「繼承(Inheritance)」與「覆寫(Override)」。保護重要的屬性變數或副程式、不開發重複功能的程式碼、外部程式無法存取父類別中的方法。各有其目的,能讓程式設計得更完善。





沒有留言:

張貼留言