類別
物件導向分析( OOA )
Object-Oriented Analysis (OOA)
從問題中識別出各個物件、識別出物件的屬性與行為、並設計物件之間交互行為的一種分析方法。
[物件 Object]
- 物件分兩種
- 實質性
e.g. 自動提款機(Automated teller machine, ATM)
- 概念性 : 無法實際接觸
e.g. 顧客的帳目
- 實質性
- 物件擁有屬性(特徵)
例如大小、名稱、形狀等
- 物件狀態: 物件所有屬性的值
一個物件可能有一個值為紅色的顏色屬性和一個值為大的尺寸屬性。
- 物件擁有操作( Operation )
- 操作(Operation)通常會影響物件的屬性
- 行為( Behavior ) : 物件所執行的操作
某一物件可能會有一個運算讓其它物件將它的顏色屬性從一狀態改變到另一狀態,例如從紅色變成藍色
[類別 Class]
- 物件的藍圖
- 有某些共同屬性的團體
- 實體( Instance ):
利用類別 (一般的藍圖)所建立的每一個物件 (窗戶),被稱為類別的實體( Instance )。
每一個由類別而建立的物件,其每一個屬性可以擁有特定的狀態(值),但是將有相同的屬性和操作行為。
[modifier] class class_identifier {
// ..類別本體
}
- modifier
被使用來決定此類別相對於其它類別的存取權。
modifier 是可有可無的(以方括弧表示),可能是 public、abstract、或是 final。
- class : 關鍵字
關鍵字告訴編譯器此程式區塊為一類別宣告。
- class_identifier : 是您給予類別的名稱。
- 類別本體:
- 變數( Variable )
- 又稱值域(Field)或資料成員(Data member )
- 表示物件的屬性( 特徵 )
- 建構式( Constructor )
- 與類別名稱具有相同名稱的特殊方法,
- 不具有返回值( Return value )
- 建構式會在您使用關鍵字 new 產生一個實例時執行,
- 重載 : 執行哪一個建構式,視您實例化時給定哪一種型態的引數而決定。
- 沒有任何參數的建構式稱之為預設建構式 ( Default constructor )
- 方法( Method )
- 變數( Variable )
public class Hello{
private String name;
//Constructor 1
public Hello(){
name = "nobody";
}
//Constructor 2
public Hello(String one){
name = one;
}
//method
public void hello(){
System.out.print("Hello ");
System.out.println(name);
}
//method
public void setName(String one){
name = one;
}
}
[封裝 Encapsulation]
表示物件中資料的藏匿處(一個安全的「容器」),使得這些資料只能透過某些方法來取得。
封裝的重要性在於使您的類別較容易被其他的程式設計人員使用,並且避免類別中的某些資料遭受不適當的修改。
[Method]
[modifier] type identifier(type param, ...) {
[return data;]
}
- modifier : 權限修飾,
可以使用 public、protected、private 來修飾,分別將方法修飾為公開的、保護的或私用的。
- 第一個 type : 設定返回值型態,
可以使用基本資料型態,也可以是自定義的物件型態,
如果方法執行完畢後不返回任何資料,則設定為 void。
- identifier : 是方法名稱。
- ( 與 ) 之間 : 參數列
用來設定方法可接受的引數個數與型態,
每個參數之間以逗號區分,
如果方法不接受任何引數,則參數列保持空白即可。
- return : 返回值
從方法中返回值時使用 return 關鍵字並加上要返回的數值或變數,
返回的數值或變數之型態必須與第一個 type 所設定的型態相同。
public int gcdOf(int m, int n){
int r;
while (n != 0){
r = m % n;
m = n;
n = r;
}
return m;
}
不定長度引數( Variable-Length Argument )
布知道要設多少個參數時
public class SimpleAdder{
public int sumOf(int... params){
int sum = 0;
for (int i = 0; i < params.length; i++){
sum = sum + params[i];
}
return sum;
}
public static void main(String[] args){
SimpleAdder adder = new SimpleAdder();
System.out.println(adder.sumOf(1, 2));
System.out.println(adder.sumOf(1, 2, 3));
}
}
被宣告的不定長度引數 params,實際上被轉換為陣列,這是在編譯時期由編譯器為您作的轉換動作,
記得必須設定在引數列的最後一個
public void someMethod(
int arg1, int arg2, int... varargs) {
}
方法的重載( Overloading )
又名多載
在定義方法時,有時候只是參數個數或是參數型態不同,但是方法的執行目的是一致的,這時候您可以為方法取相同的名稱,編譯器會自動依參數個數或型態來判斷您所呼叫的是哪一個方法。
這樣依參數個數或引數型態不同,但允許方法具有同一個名稱的機制稱之為重載( Overloading ),
要注意的是,實際呼叫方法時,您所給定的引數個數及引數型態,將作為判斷哪一個方法該被呼叫執行的依據。
[關鍵字this , status]
this 參考名稱
public class Hello2{
private String name;
public Hello2(){
name = "nobody";
}
public Hello2(String name){
this.name = name;
}
public void hello(){
System.out.print("Hello ");
System.out.println(this.name);
}
public void setName(String name){
this.name = name;
}
}
this 可以有一種帶參數的呼叫方法,作用為呼叫類別中的建構方法
public class Hello3{
private String name;
public Hello3(){
this("nobody");
}
public Hello3(String name){
this.name = name;
}
}
status
- 靜態成員( Static member )
- 靜態成員屬於類別所有,是類別層次所擁有的成員
- 呼叫靜態方法時不會有 this 參考名稱,
所以靜態方法中不可以有非靜態變數,或呼叫非靜態方法。
- 可以使用關鍵字 static 來將成員修飾為靜態成員
可以直接使用類別名稱加上 . 運算子來存取靜態資料成員
public class SimpleMath{ public static final double PI = 3.141596; public static double circumference(double r){ return PI * r * r; } public static double area(double r){ return PI * r * r; } public static double volumn(double r){ return PI * r * r * r * 4 / 3; } } System.out.println("PI = " + SimpleMath.PI);
[父與子]
- 父類別(Parent class),基礎類別(Base class)
- 子類別(Child class),衍生類別(Derived class)。
Inheritance 繼承
可以基於某個父類別對物件的定義加以擴充,而制訂出一個新的子類別定義,子類別可以繼承父類別原來的某些定義,並也可能增加原來的父類別所沒有的定義,或者是重新定義父類別中的某些特性。
extends 擴充
Java 中使用 "extends" 作為其擴充父類別的關鍵字,其實就相當於一般所常稱的「繼承」(Inherit)
使用 extends 關鍵字來表示此類別繼承自其它類別
[class_modifier] class class_identifier extends superclass_identifier
- class_modifier : public、abstract 或 final。
- class : 關鍵字告訴編譯器此程式區塊是類別宣告。
- class_identifier : 是您為此子類別命名的名稱。
- extends : 關鍵字告訴編譯器此類別為其它類別的子類別。
- superclass_identifier : 是此子類別所繼承之父類別的名稱。
public class Pants extends Clothes {
private int waistline;
public void setWaistline(int waistline) {
this.waistline = waistline;
}
public int getWaistline() {
return waistline;
}
}
重新定義( Override )
又名覆寫
在繼承了父類別之後,如果您對於父類別中的某些方法並不滿意,則您可以在繼承之後重新定義( Override )該方法,或稱之為改寫。
- Overloading 重載:
依參數個數或引數型態不同,但允許方法具有同一個名稱的機制
super
在繼承某類別之後,如果您打算呼叫父類別中原先已定義的建構式或方法,則您可以使用 super 關鍵字
super.xxxMethod();
終止重新定義、終止繼承
有個方法並不想讓子類別繼承後重新定義,則您可以在方法上加上final
Parent Class:
public final void someMethod() {
// ...
}
希望某個類別完全不能被繼承:
public final class SomeClass {
//
}
java.lang.Object
在 Java 中,所有的物件都隱含的擴充了 java.lang.Object 類別, Object 類別是 Java 程式中所有類別的父類別
public class Foo {
// 實作
}
//等於
public class Foo extends Object {
// 實作
}
Object 中定義了許多個方法,都設定為final,所以無法異動
[toString()]
傳回物件本身的描述
public String toString() {
return getClass().getName() + '@' + Integer.toHexString(hashCode());
}
[finalize()]
如果沒有被任何的名稱參考,它將會被 JVM 回收,以釋放物件所佔據的資源
在 JVM 回收物件之前,會執行物件的 finalize()方法,您可以在這個方法中撰寫一些物件被回收前的善後工作
JVM 何時會回收物件並無法預知,所以如果您有立即性必須馬上處理的工作,不可以依賴在 finalize()方法中完成
[equals()]
equals()方法預設是比較物件的記憶體參考是否相同,開啟 Object 類別的原始碼,您也發現它的 equals()只是使用==運算子來比較
public boolean equals(Object obj) {
return (this == obj);
}
注意!class和變數的equals不太一樣... 變數要比對值是否相等用equals class的equals是==
多型 Polymorphism
多型操作指的是使用同一個操作介面,以操作不同的物件實例,
目的:
多型操作在物件導向上是為了降低對操作介面的依賴程度,進而增加程式架構的彈性與可維護性。
所謂的多型 (polymorphism) 就是運用類別 (class) 間繼承 (inherit) 的關係,使父類別 (superclass) 可以當成子類別 (subclass) 的通用型態。
基本方式:
如果類別與類別之間有繼承關係,則您可以用父類別宣告一個參考名稱,並讓其參考至子類別的實例,並以父類別上的公開介面來操作子類別上對應的公開方法
[多型導論]
多型操作指的是使用同一個操作介面,
以操作不同的物件實例
操作介面 :
- 就 Java 程式而言,操作介面通常指的就是您在類別上定義的公開方法,透過這些介面,您可以對物件實例加以操作。
- 透過對應介面來操作物件
- 使用不正確的類別型態來轉換物件的操作介面,則會發生 java.lang.ClassCastException 例外
假設 Class1 上定義了 doSomething() 方法,而 Class2 上也定義了 doSomething() 方法,而您定義了兩個 execute() 方法來分別操作 Class1 與 Class2 的實例:
public void execute(Class1 c1) {
c1.doSomething();
}
public void execute(Class2 c2) {
c2.doSomething();
}
//---修正為---//
public void execute(ParentClass c) {
c.doSomething();
}
程式中 execute() 分別依賴了 Class1 與 Class2 兩個類別,與其依賴兩個類別,不如定義一個父類別 ParentClass 類別,當中定義有 doSomething(),並讓 Class1 與 Class2 都繼承 ParentClass 類別並重新定義自己的 doSomething() 方法,如此您就可以將程式中的 execute() 改成
這就是多型操作所指的,使用同一個操作介面,以操作不同的物件實例。
兩個類別使用同樣的介面execute,對應各自的doSomething()方法 整合相同的介面,分別依賴不同類別,對應個自的doSomething()方法 從分別依賴 Class1 與 Class2 改為只依賴 ParentClass,程式對個別物件的依賴程式降低了,日後在修改、維護或調整程式時的彈性也增加了,這是繼承上多型操作的一個實例。
實際上在設計並不依賴於具體類別,而是依賴於抽象
Java 中在實現多型時,可以讓程式依賴於「抽象類別」(Abstract class)或是「介面」(Interface),
雖然兩者都可以實現多型操作,但實際上兩者的語義與應用場合是不同的。
[抽象類別 Abstract]
規範操作介面,並在執行時期可以操作各種子類別的實例
繼承某抽象類別的類別
必定是該抽象類別的一個子類
由於同屬一個類型,只要父類別中有定義同名方法,您就可以透過父類別型態來操作子類實例中被重新定義的方法,也就是透過父類別型態進行多型操作
抽象方法( Abstract method)
- 在父類別中事先規範子類別必須實作的方法,父類別中暫時無需實作
- 僅宣告方法名稱而不實作當中的邏輯的方法
抽象類別( Abstract class ) :
- 包括有抽象方法的類別
- 擁有未實作方法的類別
- 不能被用來生成物件,只能被繼承擴充
- 於繼承後實作未完成的抽象方法
- 將抽象類別的名稱加上 Abstract 作為開頭,可表明這是個抽象類別,用意在提醒開發人員不要使用這個類別來產生實例(事實上也無法產生實例),e.g. :
AbstractCircle.java
宣告方式
使用關鍵字 : abstract
AbstractCircle.java :
public abstract class AbstractCircle {
protected double radius;
public void setRedius(int radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
public abstract void render();
}
ConcreteCircle.java :
public class ConcreteCircle extends AbstractCircle {
public ConcreteCircle() {}
public ConcreteCircle(double radius) {
this.radius = radius;
}
public void render() {
System.out.printf("畫一個半徑 %f 的實心圓\n", getRadius());
}
}
HollowCircle.java :
public class HollowCircle extends AbstractCircle {
public HollowCircle() {}
public HollowCircle(double radius) {
this.radius = radius;
}
public void render() {
System.out.printf("畫一個半徑 %f 的空心圓\n", getRadius());
}
}
共同的定義被提取至 AbstractCircle 類別中,並於擴充時繼承了下來,所以在 ConcreteCircle 與 HollowCircle 中不用重複定義,只要定義個別對 render() 的處理方式就行了,
由於 ConcreteCircle 與 HollowCircle 都是 AbstractCircle 的子類別,因而可以使用 AbstractCircle 上有定義的操作介面,來操作子類別實例上的方法
public class CircleDemo {
public static void main(String[] args) {
renderCircle(new ConcreteCircle(3.33));
renderCircle(new HollowCircle(10.2));
}
public static void renderCircle(AbstractCircle circle) {
circle.render();
}
}
對 renderCircle() 方法來說,它只需依賴 AbstractCircle 類別,而不用個別為 ConcreteCircle 與 HollowCircle 類別撰寫個別的 renderCircle() 方法
[介面 Interface]
介面有點像是完全沒有任何方法被實作的抽象類別,但實際上兩者在語義與應用上是有差別的。
介面命名,必須以I開頭,e.g. ITest.java
Abstract :
繼承某抽象類別的類別 必定是該抽象類別的一個子類
由於同屬一個類型,只要父類別中有定義同名方法,您就可以透過父類別型態來操作子類實例中被重新定義的方法,也就是透過父類別型態進行多型操作
Interface:
實作某介面的類別 並不被歸屬於哪一類
一個物件上可以實作多個介面。
考慮您有一個方法 doRequest(),
您事先並無法知道什麼型態的物件會被傳進來,
或者是這個方法可以接受任何類型的物件,
您想要操作物件上的某個特定方法,例如doSomething()方法,
問題是傳進來的物件是任意的,
除非您定義一個抽象類別並宣告doSomething() 抽象方法,
然後讓所有的類別都繼承這個抽象類別,
否則的話您的 doRequest() 方法似乎無法實作出來,
實際上這麼作也沒有價值。
介面目的:
- 定義一組可操作的方法,
- 實作某介面的類別必須實作該介面所定義的所有方法,
- 只要物件有實作某個介面,就可以透過該介面來操作物件上對應的方法,無論該物件實際上屬於哪一個類別, 像上面所述及的問題,就要靠要介面來解決。
宣告方式
使用關鍵字 : interface
[public] interface 介面名稱 {
權限設定 傳回型態 方法(參數列);
權限設定 傳回型態 方法(參數列);
// ....
}
在宣告介面時方法上的權限設定可以省略,如果省略的話,預設是 "public"。
使用interface的類別用關鍵字implements
[public] class 類別名稱 implements 介面名稱 {
// ....
}
可以使用多個介面
public class 類別名稱 implements 介面1, 介面2, 介面3 {
// 介面實作
}
由於實作了多個介面,所以要操作物件時,必要時必須作「介面轉換」,如此程式才知道如何正確的操作物件,
假設 someObject 實作了 ISomeInterface1 與 ISomeInterface2 兩個介面,則您可以如下對物件進行介面轉換與操作:
ISomeInterface1 obj1 = (ISomeInterface1) someObject;
obj1.doSomeMethodOfISomeInterface1();
ISomeInterface2 obj2 = (ISomeInterface2) someObject;
obj2.doSomeMethodOfISomeInterface2();
範例:
IRequest.java:
public interface IRequest {
public void execute();
public void doGoodbye();
}
HelloRequest.java
public class HelloRequest implements IRequest {
private String name;
public HelloRequest(String name) {
this.name = name;
}
public void execute() {
System.out.printf("哈囉 %s!%n", name);
}
public void doGoodbye(){}
}
WelcomeRequest.java
public class WelcomeRequest implements IRequest {
private String place;
public WelcomeRequest(String place) {
this.place = place;
}
public void execute() {
System.out.printf("歡迎來到 %s!%n", place);
}
public void doGoodbye(){}
}
RequestDemo.java
public class RequestDemo {
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
int n = (int) (Math.random() * 10) % 2; // 隨機產生
switch (n) {
case 0:
doRequest(new HelloRequest("良葛格"));
break;
case 1:
doRequest(new WelcomeRequest("Wiki 網站"));
}
}
}
public static void doRequest(IRequest request) {
request.execute();
}
}
您設計了一個 doRequest()方法,雖然 HelloRequest 與 WelcomeRequest 是兩種不同的類型(類別),但它們都實現了 IRequest,所以 doRequest() 只要知道 IRequest 定義了什麼方法,就可以操作 HelloRequest 與 WelcomeRequest 的實例,而不用知道傳入的物件到底是什麼類別的實例
介面的繼承
public interface 名稱 extends 介面1, 介面2 {
// ...
}
不同於類別一次只能繼承一個父類別,一個介面可以同時繼承多個父介面,實作子介面的類別必須將所有在父介面和子介面中定義的方法實作出來。
泛型 Generics
泛型解決的不只是讓您少寫幾個類別的程式碼,還在於讓您定義「安全的」泛型類別(Generics class),泛型提供編譯時期檢查,您不會因為將物件置入某個容器(Container)而失去其型態
[沒有泛型前]
在 J2SE 5.0之前,Java 程式設計人員可以使用 Object 定義類別以解決以上的需求,為了讓定義出來的類別可以更加通用(Generic),傳入的值或傳回的實例都是以 Object 型態為主,當您要取出這些實例來使用時,必須記得將之轉換為原來的類型或適當的介面,如此才可以操作物件上的方法。
已下範例兩個類別,其中除了宣告成員的型態、參數列的型態與方法返回值的型態不同之外,剩下的程式碼完全相同,
BooleanFoo.java
public class BooleanFoo {
private Boolean foo;
public void setFoo(Boolean foo) {
this.foo = foo;
}
public Boolean getFoo() {
return foo;
}
}
IntegerFoo.java
public class IntegerFoo {
private Integer foo;
public void setFoo(Integer foo) {
this.foo = foo;
}
public Integer getFoo() {
return foo;
}
}
或許有點小聰明的程式設計人員會將第一個類的內容複製至另一個檔案中,然後用編輯器「取代」功能一次取代所有的型態名稱(即將 Boolean 取代為 Integer)。
雖然是有些小聰明,但如果類別中的邏輯要修改,您就需要修改兩個檔案,泛型(Generics)的需求就在此產生,
當您定義類別時,發現到好幾個類別的邏輯其實都相同,就只是當中所涉及的型態不一樣時,使用複製、貼上、取代的功能來撰寫程式,只是讓您增加不必要的檔案管理困擾。
由於 Java 中所有定義的類別,都以 Object 為最上層的父類別,所以用它來實現泛型(Generics)功能是一個不錯的考量,
J2SE 1.4 或之前版本上,大部份的開發人員會這麼作:
ObjectFoo.java
public class ObjectFoo {
private Object foo;
public void setFoo(Object foo) {
this.foo = foo;
}
public Object getFoo() {
return foo;
}
}
ObjectFoo foo1 = new ObjectFoo();
ObjectFoo foo2 = new ObjectFoo();
foo1.setFoo(new Boolean(true));
// 記得轉換操作型態
Boolean b = (Boolean) foo1.getFoo();
foo2.setFoo(new Integer(10));
// 記得轉換操作型態
Integer i = (Integer) foo2.getFoo();
[Generics做法]
使用<T>
用來宣告一個型態持有者(Holder)名稱 T,之後您可以用 T 這個名稱作為型態代表來宣告成員、參數或返回值型態
public class GenericFoo<T> {
private T foo;
public void setFoo(T foo) {
this.foo = foo;
}
public T getFoo() {
return foo;
}
}
使用泛型所定義的類別在宣告及配置物件時,您可以使用角括號一併指定泛型類別型態持有者 T 真正的型態,而型態或介面轉換就不再需要了,getFoo() 所設定的引數或傳回的型態,就是您在宣告及配置物件時在 <> 之間所指定的型態,您所定義出來的泛型類別在使用時多了一層安全性,可以省去惱人的 ClassCastException 發生,編譯器可以幫您作第一層防線,例如下面的程式會被檢查出錯誤:
範例1
public class GenericFooDemo {
public static void main(String[] args) {
GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
GenericFoo<Integer> foo2 = new GenericFoo<Integer>();
foo1.setFoo(new Boolean(true));
Boolean b = foo1.getFoo(); // 不需要再轉換型態
System.out.println(b);
foo2.setFoo(new Integer(10));
Integer i = foo2.getFoo(); // 不需要再轉換型態
System.out.println(i);
}
}
範例2
public class GenericFoo2<T1, T2> {
private T1 foo1;
private T2 foo2;
public void setFoo1(T1 foo1) {
this.foo1 = foo1;
}
public T1 getFoo1() {
return foo1;
}
public void setFoo2(T2 foo2) {
this.foo2 = foo2;
}
public T2 getFoo2() {
return foo2;
}
}
GenericFoo<Integer, Boolean> foo =
new GenericFoo<Integer, Boolean>();
範例3
宣告陣列
public class GenericFoo3<T> {
private T[] fooArray;
public void setFooArray(T[] fooArray) {
this.fooArray = fooArray;
}
public T[] getFooArray() {
return fooArray;
}
}
String[] strs = {"caterpillar", "momor", "bush"};
GenericFoo3<String> foo = new GenericFoo3<String>();
foo.setFooArray(strs);
strs = foo.getFooArray();
參考
GitHub - Justin Lin - 第 8 章 繼承(Inheritance)、多型(Polymorphism)
https://github.com/JustinSDK/JavaSE6Tutorial/blob/master/docs/CH08.md
GitHub - Justin Lin - 第 12 章 泛型
https://github.com/JustinSDK/JavaSE6Tutorial/blob/master/docs/CH12.md