Java物件導向
Upcoming SlideShare
Loading in...5
×
 

Java物件導向

on

  • 15,041 views

艾鍗學院-Java物件導向課程

艾鍗學院-Java物件導向課程

Statistics

Views

Total Views
15,041
Views on SlideShare
1,873
Embed Views
13,168

Actions

Likes
0
Downloads
23
Comments
0

4 Embeds 13,168

http://www.ittraining.com.tw 13164
http://ittraining.com.tw 2
http://131.253.14.98 1
http://webcache.googleusercontent.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Java物件導向 Java物件導向 Document Transcript

  • Java 物件導向作者: JareyEmail: jarey@jareymobilec.com.tw課程: 第二堂內容: 1.Java 物件導向觀念 - Object 與 Class、繼承、封裝、多型、介面 2.Java package 觀念 47
  • 3 物件導向觀念 物件導向程式設計(Object-Oriented Programming),是目前相當熱門的一種程式設計概念,而 Java 本身就是是一個物件導向式的語言,物件導向式的設計可以讓你的程式不在是只能用在一個程式之中,而是可以一再的被利用到其他的程式之中,這將讓程式設計更加的有彈性與擴充性。Java 本質上提供了一個良好的物件導向設計程式撰寫平台,但是要設計一個具有良好架構的物件導向程式,讀者還是必須先了解物件導向的設計觀念與基本精神,在這個章節中筆者將會用一些生活化的範例來協助讀者了解物件導向的觀念。3.1 物件導向的 2 個精神與 3 個特徵 2 個精神 Abstraction(抽像化):物件抽像化能物件被良好定義與描述,並保持中立性. Inheritance(繼承):繼承給序程式更高的延展性與拓展性. 3 個等徵 Inheritance(繼承): • 擴 充(extends)父類別 • 被保護的(protected)成員 • 重新定義(Override)方法 • Object 類別 • final 關鍵字 Encapsulation(封裝): • 使用類別(Class)定義物件 • 類別成員(Class member) • 建構方法(Constructor) • 關於 this • 關於 static 成員 48
  • • 重載(Overload)方法 • 不定長度引數 • 遞迴方法 • 垃圾回收 Polymorphism(多型): • 抽象類別(Abstract class) • 介面(interface)型態 • 介面與介面多重繼承3.2 封裝 所謂物件導向程式,主要是由許多不同功能的物件組合而成,而其中的組合關係可能相當的錯綜復雜,若每個物件之間的變數都能夠直接任意的存取使用,很容易就會造成物件內部參數的錯亂,為了避免這類的情況發生,最好的方式就是將物件內的變數都封裝起來,並提供一個單一對外存取的窗口,任何物件要存取這個變數都必須透過這個窗口,而這個窗口就是利用函式(方法)來包裝變數,如此一樣便能讓程式運作更加的穩定與安全。事實上物件裡所包含的結構及方法,本身也是透過物件包裝起來,這也算是封裝特性的一種。所以我們可以利用封裝的特性將物件實作的方法與介面分開,防止物件本身的一些屬性被使用者無意間的修改。 類別成員(Class 類別成員(Class member)在類別中可定義Field成員及方法(Method) 成員。package com.ittraining.example;public class Student { private int classID; // 學號 protected int classID2; private String name; // 名稱 private String schoolName;//學校 public void setClasssID(int classID) { this.classID = classID; 49
  • } public void setName(String name) { this.name = name; } public int getClassID() { return classID; } public String getName() { return name; } public void setSchoolName(String schoolName){ this.schoolName=schoolName; } public String getSchoolName(){ return schoolName; } public void printData(){ System.out.println("姓名:"+name+"n學號:"+classID+"n學校:"+schoolName); }} 在 Java 中,類別的存取權限修飾詞有"public"、"protected"、"private"、"default(無條飾詞)",當修飾詞或空時,預設將會以以套件 (package)為可存取範圍,也就是說只有在同一層 package 中的 class 才能進行存取。 上述的範例程式中程式中 我們定義了一個 Student 類別 其中包含 3 個被宣告為 private , ,的 field 成員,分別是 classID 與 name。由於宣告為 private,所以代表這 3 個成員只能在同一個 Object 中進行存取,而無法別其他物件直接進行存取。 接著我們定義了6個 Method 成員 全部都是宣告為"public" 代表這些 method 是公開的 , , ,可被其他物件所呼叫,method 的定義方式如下 : 存取修飾 傳回值型態 方法名稱(參數列) { 50
  • return 傳回值; } 也就是我們將 classID 與 name 成員變數透過 private 宣告進行封裝,並另外在建立四個公司的方法讓其他物件透過我們公開的方法去對 classID 與 name 進行存取,而非直接將classID 與 name 成員公開出去,一切的資料存取都需透過 method 成員呼叫,而非直接呼叫該field 資料成員來進行存取。我們稱之為“資料的封裝”,封裝的好處在於可以確保資料的安全性,與存取介面的一致性。 靜態成員(Static 靜態成員(Static member) 對於每個物件來說,每個物件都各自擁有自己的資料成員。就算兩個物件是由同一個類別透過 new 關鍵字產生,也不會發生更改了 A Object 內的成員變數,連帶著 B Object 也同時變動的情況。因為就在 Object 被 class loader 載入的那一刻,資料成員就以復本(Copy)的型態載另到記憶體之中。 然而在某些時後你會想要這些物件都可以共享相同的資料成員,此時就需要將刻資料成員宣告為 static。一但資料成員被宣告為 static 後,在同一個類別之中,該資料就只會存在一份,object 被 class loader 載入時並不會在 copy static 成員到記憶體中,而是共享的。舉個例來說,我們將前一個 Student 的範例中進行些小修改,填加幾個預設學校名稱的參數,並宣告為 static。 51
  • package com.ittraining.example;public class Student { private static String SCHOOL_ONE="建國中學"; private static String SCHOOL_TWO="北一女中"; private static String SCHOOL_THREE="景美女中"; private int classID; // 學號 private String name; // 名稱 private String schoolName="建國中學";//學校 public void setClasssID(int classID) { this.classID = classID; } public void setName(String name) { this.name = name; } public int getClassID() { return classID; } public String getName() { return name; } public void setSchoolName(String schoolName){ this.schoolName=schoolName; } public String getSchoolName(){ return schoolName; } public void printData(){ System.out.println("姓名:"+name+"n學號:"+classID+"n學校:"+schoolName); } public static void main(String args[]){ Student st1 =new Student(); st1.setClasssID(12345); st1.setName("Jarey"); st1.setSchoolName(Student.SCHOOL_ONE); st1.printData(); }} 52
  • 由於 static 成員是直接屬於類別,並不屬於任一個物件所獨有,因此可以直接以類別名稱去進行存取,而無需實體化成物件。例如:st1.setSchoolName(Student.SCHOOL_ONE);即有靜態資料,相對的我們也可以將方法宣告成靜態的方法(static method),而靜態方法通常是為了提供一些簡單的常數運算或一些操作上的小工具,例如我們可以於 Student.java 中新增一個 help 的靜態方法,該方法可說明此類別的工能與使用方式。 略 public static void help(){ System.out.println("請輸入姓名、學號與、所屬學校"); }靜態方法的呼叫方式與靜態資料相同,皆是直接透過類別名稱+.運算子 + 方法名。其實我們時常用到 System.out.println()本身就是一個靜態的資料+靜態方法的應手,這也就是為什麼我們不必去 new System 物件就可以宜接使用 out 這個靜物件,並可以直接呼叫 println()這個靜態方法將字串貼於 console 上.由於 static 成員是屬於類別而不是物件,因此若你要在 static method 中呼叫非 static 資料,在編譯時就會出現以下的錯誤訊息:non-non-static variable xxx cannot be referenced from a static context同樣的你也不能在 static method 中乎叫非 static 的 methodnon-non-static method xxxx() cannot be referenced from a static context xxxx()由於 static 成員是屬於類別而不是物件 所以當您呼叫 static 方法時 並不會傳入物件的位置參考 所以 static , , ,方法中不會有 this 參考,由於沒有 this 參考,所以在 Java 的 static 方法成員中不允許使用非 static 成員,因為程式沒有 this 來參考至物件位址,也 就無法辨別要存取哪一個物件的成員,事實上,如果您在 static 方法中使用非 static 資料成員,在編譯時就會出現以下的錯誤訊息:non-non-static variable test cannot be referenced from a static context 53
  • 或者是在 static 函式中呼叫非 static 函式,在編譯時就會出現以下的錯誤訊息:non-non-static method showMe() cannot be referenced from a static context最後如果你希望在載入類別時就先進行一些初始化的動作,此時你可以使用 static 區塊,將所有需要做初始化資源的成員全都放在 static 區塊中。Static 區塊會在第一次呼叫而被載入時,static 區塊中的程式碼會先被執行,且只會執行一次。 public class Student { .... static { // 初始化程式碼 } .... } 建構方法(Constructor) 建構方法(Constructor) 當一個物件被載入到 memory 時,會先呼叫該物件的建構方法,而建構方法主要可用來做物件的初始化動作。另外 Java 的語法中並不像 C 有解構方法(Destructor),而是交由垃圾回收器自動幫我們做處理。建構方法與一般方法不同處在於建構方法並無回傳質,而每一個物件一定必須要有建構方法,若你沒有特別撰寫建構方法,系統會使用預設的建構方法(無任何傳入質)進行物件的初始化。我們可將 Student 範例在進行改寫,填入建構方法來為物件做初始化的動作。 54
  • package com.ittraining.example;public class Student { private static String SCHOOL_ONE="建國中學"; private static String SCHOOL_TWO="北一女中"; private static String SCHOOL_THREE="景美女中"; private int classID; // 學號 private String name; // 名稱 private String schoolName="建國中學";//學校 public void setClasssID(int classID) { this.classID = classID; } public Student() { super(); } public Student(int classID, String name, String schoolName) { this.classID = classID; this.name = name; this.schoolName = schoolName; } public void setName(String name) { this.name = name; } public int getClassID() { return classID; } public String getName() { return name; } public void setSchoolName(String schoolName){ this.schoolName=schoolName; } public String getSchoolName(){ return schoolName; } public void printData(){ System.out.println("姓名:"+name+"n學號:"+ classID+"n學校:"+schoolName); 55
  • } public static void help(){ System.out.println("請輸入姓名、學號與、所屬學校"); } public static void main(String args[]){ Student st1 = new Student(12345, "Jarey", Student.SCHOOL_ONE); st1.printData(); } } 重載(Overload)方法 重載(Overload)方法 (Overload) 「重載」 Overload) (Overload ,又有人譯作「超載」「過載」Java 支援方法「重載」 Overload) ( 、 ,這種機制為類似功能的方法提供了 統一的名稱,但是根據參數列型態的不同,而自動呼叫對應的方法。一個例子可以從 java.lang.String 類別上提供的所有方法看到,像是它的 valueOf()方法就提供了多個版本:static String valueOf(boolean b)static String valueOf(char c)static String valueOf(char[] data)static String valueOf(char[] data, int offset, int count)static String valueOf(double d)static String valueOf(float f)static String valueOf(int i) valueOf(longstatic String valueOf(long l)static String valueOf(Object obj) 56
  • 雖然呼叫的方法名稱都是 valueOf(),但是根據所傳遞的引數資料型態不同,您可以呼叫不同版本的方法來進行對應的動作。方法重載的功能使得程式設計人員能較少苦惱於方法名稱的設計,以統一的名稱來呼叫相同功能的方法,方法重載不僅可根據傳遞引數的資料型態不同來呼叫對應的 方法,參數列的參數個數也可以用來設計方法重載。方法重載時可以根據方法參數列的資料型態,也可以根據參數的個數,不過必須注意的是,方法重載不可根據傳回值的不同來區別。方法重載當被使用於物件導向設計時的建構方法的使用時,提供物件生成時不同的建構方法,或者是使用於物件所提供的同名方法,但多樣化的參數設定方式。最後我們設計了一個 Account 範例來為這一章做個總節。 class Account{ //私有變數,記錄存款金額 private int money=100; //公開函式,做為存取money變數的單一窗品 public int getMoney(){ //傳回money質 return money; } } public class Encapsulation { public static void main(String[] args) { Account tony=new Account(); new //只能透過 tony.getMoney()money變數 //直接對money變數存取如:tony.money 將不被允許 System.out.println("tony存款有:"+tony.getMoney()+"元"); 57
  • } }【執行結果】 圖 1-6 封裝範例執行結果 我們利用關鍵字 private 宣告了一個私有的變數 money 用來記錄存款金額。由於變數宣被告成 private,因此只有在 Account 物件本身自己可以存取此變數,其他物件皆無權限存取該變數,這就意味者我們己經將變數封裝於 Account 物件之中。接下來我們要建立一個公開函式,做為其他物件存取 money 變數的窗口,請注意到 getMoney 函式必須宣告為 public,如此一來任何的物件都可以呼叫此函式,並取得 Money 質。 由於存款金額是一個相當重要的資訊,雖然我們己經利用資料封裝的特性,禁止其他物件直接存取 money 變數,但若被封裝的資訊只願意被分享給特定物件存取時又該如何設計呢?答案其實很簡單,竟然我們只限制所有物件都必須透過 getMoeny 函式存取 money 變數,那我們一樣可以利用這一個單一的存取窗口來進行身分檢查的動作,接著就讓我們為此範例程式加入密碼檢查的功能: 58
  • class Account{ //私有變數,記錄存款金額 private int money=100; //字串型態變數,記錄密碼為123 private String passWorld="123"; public int getMoney(String passWorld){ //比對密碼是否相同 if(this if this.passWorld==passWorld) this return money; else return -1;//密碼錯誤則傳回-1 } } public class Encapsulation { public static void main(String[] args) { Account tony=new Account(); new System.out.println("tony存款有:"+tony.getMoney("123")+"元"); } } 在 Account 裡我們新增了一個 passWorld 變數用來存放密碼,並在 getMoney()函式中增加了一個密碼檢查的功能,在這裡密碼預設為 123,只要密碼正確就會傳回 money 質。這個範例程式中還保留有許多可以擴充的功能,如撰寫一個設定密碼的函式,與存入存款的函式,這些都是相當不錯的練習題材,讀者可以動手練習撰寫看看。3.3 繼承 繼承觀念在物件導向程式設計中扮演著相當重要的解色,透過繼承可以讓類別所提供的功能與方法得以傳承至其他的類別中使用,同時可以讓程式的分工更加的細膩,每個類別只需負責提供一個特定的功能服務,若有其他類別需要這類的功能只需利用繼承的方式將該功能傳承下來,這點就相當類似自然界中物種的分類方式,例如:貓與老虎都是同屬於貓科動物,代表 59
  • 它們兩者之間都具有貓科動物的特徵,但卻又個自擁有屬於自己的習性,再以人類來說:我們身體上的一些特徵,有些可能是源自於你的上一代的特性,當然,你的父母親也是源自於他們上一代的遺傳。繼承在 Java 語言中的保留字為 extends ,我們可以在宣告類別名稱的尾端利用 extends 關鍵字宣告此類別是繼承自那個類別,值得注意的是 Java 並不像 C++支援多重繼承,所以每個類別最多只能繼承至一個類別,但同一個類別卻可以被許多不同類別繼承喔!讓我們利用一個繪圖範例來說明,下表為本範中使用到的各個類別之間的功能與關係表: 程式 功能說明 繼承關係 Dot.java 繪製一個點於螢幕上 無 Line.java 繪製一個線於螢幕上 extend Dot Shape.java 繪製一個形狀於螢幕上 extend Shap DrawMain.java 主程式 無【程式 Dot.java】 : Dot 類別位置此範例程式繼承結構中的最上層,因此所提供的是一個最基礎的功能,將一個字元(等同於一個點)繪製於螢幕上,為了增加此範例的變化性,setStyle 方法可以讓我們自行定義要印出來的字元為何,若無自行定義字元圖樣,則預設輸出為*字。 //Dot.java //繪製點於螢幕上。 public class Dot{//宣告一個名稱為Dot的類別 char style=*; //設定點的圖樣 public void setStyle(char type){ char style=type; } //畫出一個點於螢幕上 public void drawDot(){ System.out.print(style);//繪點(並不會自行斷行) } 60
  • }【程式 Line.java】 : Line 類別繼承至 Dot 類別,因此在 Line 類別中將可以呼叫父類別(Dot 類別)所提供的方法,在 drawLine()方法中,利用重覆的呼叫 Dot 類別的 drawDot()方法繪製出我們需要長度的線條於螢幕上,其中 offset 傳入參數代表的是線條的起始位置,length 傳入參數代表的是線條的長度。 //Line.java //繪製一條線於螢幕上 //繼承至Dot public class Line extends Dot{//繼承Dot類別 int offset=0; //設定每條線的空隔偏移 public void setInterval(int value){ int offset=value; } //繪製線於螢幕上 public void drawLine(int offset,int length){ int int for int i=1;i<=offset;i++){ for(int System.out.print(" ");//依偏移量繪製空格 } for int i=1;i<=length;i++){ for(int drawDot();//呼叫父類別的drawDot方法畫點 } System.out.println("");//斷行 } }【程式 Shape.java】 : Shape 類別繼承至 Line 類別,因此在 Shape 類別中可以直接呼叫父類別(Line 類別)所提供的方法,在這 drawTriangle()方法中我們依三角型的圖形需求向 Line 類別呼叫繪圖多條不同長度的與起始位置的線條,最後這些線條將會排例成一個正三角型。 61
  • //Shape.java //繪製圖型於螢幕上 public class Shape extends Line{ //繪製三角型的方法 public void drawTriangle(int height){ int int lenght=1;//線的長度 //依照要求高度繪製圖型 for int i=1;i<=height;i++){ for(int //呼叫父類別的drawLine方法 drawLine((height-i),lenght); lenght=lenght+2;//線的長度加二 } } }【程式 DrawMain.java】 : DrawMain 為本範例的主程式,在程式進入點 main 方法裡,首先將 Shape 類別實作成二個物件,並分別存放於 triangleA 與 triangleB 變入中,接著我們為這兩個物件分別設定二個不同的輸出字元與高度。到這裡不知讀者是否還記得前一節所介紹的類別與物件的關係,在此範例中我們利用同樣的 Shape 類別實作成兩個物件,隨後它們分別擁有個自的資料成員,因此到最後的輸出結果,將會有兩個截然不同的表現方式。讀者可以再重新的回想思考一下類別與物件之間的關係,相信應該很快就能有所領悟。 //DrawMain.java //主程式 public class DrawMain { public static void main(String args[]){ //取得Shape物件,並指定至triangleA變數中 Shape triangleA =new Shape(); new //取得Shape物件 Shape triangleC =new Shape(); new //設定圖型樣式為字元A triangleA.setStyle(A); //繪製高度為10的三角型 62
  • triangleA.drawTriangle(10); //設定圖型樣式為字元C triangleC.setStyle(C); //繪製高度為5的三角型 triangleC.drawTriangle(5); } }【執行結果】 : 圖 1-4 繼承範例執行結果 在這個範例中我們利用了繼承結構的設計方式,由一個 Dot 類別開始,Dot 類別只具有一個畫點的簡單功能,當我們想要為它加上畫線功能時,只需要利用利用繼承的方式讓 Line 類別承襲先前的 Dot 類別畫點方法,並在加上一個新的畫線的功能,最後 Shape 則可利用許多條不同長度的線條組合成一個圖樣,讀者若有興趣也可以自行在撰寫繪製其他圖樣的 Shape 類別(如星型、倒三角型),這時我們就不必在從新設計畫線與畫點的功能,只需將撰寫好的類別繼承至 Line 類別,就可以直接引用 Line 與 Dot 中所提供的方法。所以活用繼承關係可以讓程式 63
  • 更具有彈性與擴充性,同時也達到程式再利用的優點。 由下圖可以更容易了解繼承結構的物件運作方式,當主程式呼叫 setStyle()方法時,由於在子類別找不到該方法就會往父類別呼叫(向繼承的類別查尋是否有提供該方法),最後這個請求會傳到 setStyle()方法的提供者 Dot 物件,同樣的,繪製圖型時也是利用繼承的階層關係,經由一層層的往上呼叫,最終會由 Dot 畫出點於螢幕上。Dot 物件並不會知道自己要畫的是什麼樣的圖形,Line 物件也只知道自己要畫的是線,只有 triangleA 與 triangleC 物件知道自己將要畫的是三角型,這這正是繼承結構最大的特色,每個物件只專注自己負責的功能,越往上層的物件愈單純,如此一來每個物件都可以相當容易進行功能上的擴充,或是重覆再利用於其他的應用程式中。 圖 1-5 繼承範例物件運作意示圖3.4 多型 多型主要的目的是為了讓程式的撰寫上更具有彈性與擴充性,而所謂的多型即意謂著單一的物件可以被宣告成多種型別。在 Java 語言中多型常見於繼承結構與介面(interface)之中,例如現有一個 A 類別繼承至 B 類別,在建立 A 物件時可以有兩種方式: 64
  • A objectA = new A();B objectB = new A();由於 A 類別是繼承至 B 類別,所以我們可以將 A 物件轉型成為 B 物件型態,若讀著還記得先前章節所介紹的轉型方式,該還記得由小資料型別轉大資料型別並不會造成資料流失因此可以自動轉型,但由大資料型別轉小資料型別可就得非得利用強制轉不可,同樣的,若我們將上式改寫成:A objectaA=new B();將發生編譯錯誤的情況,原因是 B 類別只是被 A 類別繼承,所以 B 類別並無法得知 A 類別內部所提供的資料與方法,聰明的讀者這時一定想到若利用強制轉型方式便可以順利的轉型過去,但這樣做將會發生執行時期錯誤(Run time error),這將會讓你的程式隱藏著不可預期的錯誤(因為是由 B 物件並不認得 A 物件的內容) 。【多型範例程式】 //多型範例 //Polymorphism.java //SuperClass為父類別 class SuperClass{ public void superMethod1(){ System.out.println("SuperClass"); } public void superMethod2(){ System.out.println("SuperClass"); } } //Polymorphism為子類別,同時也是主程式 public class BaseClass extends SuperClass{ //覆寫掉SuperClass類別的superMethod1()方法 public void superMethod1(){ System.out.println("BaseClass"); } //子類別式所提供的方法 public void baseMethod(){ System.out.println("BaseClass"); } 65
  • //程式進入點 public static void main(String args[]){ //建立一個BaseClass物件 BaseClass baseObject=new BaseClass(); new //建立一個BaseClass物件並轉型為SuperClass SuperClass baseToSuperObject = new BaseClass(); //建立一個SuperClass物件 SuperClass superObject = new SuperClass(); //此註解一個錯誤的物件強製轉型做法,將發生執行時期錯誤。 //BaseClass runtimeErrObject=(BaseClass)new SuperClass(); System.out.println("n==baseObject==由BaseClass實體化=="); //呼叫父類別的superMethod1()方法 baseObject.superMethod1(); //呼叫父類別的superMethod2()方法 baseObject.superMethod2(); //呼叫本身提供的mainMethod方法 baseObject.baseMethod(); System.out.println("n=baseToSuperObject=由BaseClass實體化"); //呼叫父類別的superMethod1()方法 baseToSuperObject.superMethod1(); //呼叫父類別的superMethod2()方法 baseToSuperObject.superMethod2(); //以下註解掉這行將會發生編譯時期錯誤 //baseToSuperObject.baseMethod(); ((BaseClass)baseToSuperObject).baseMethod(); System.out.println("n==superObject==由SuperClass實體化=="); superObject.superMethod1(); superObject.superMethod2(); //以下註解將會發生編譯時期錯誤 //superObject.baseMethod(); //以下註解將會發生執行時期錯誤 //((BaseClass)superObject).baseMethod(); }} 66
  • 【執行結果】 圖 1-7 多型範例執行結果 或許讀者直接閱讀程式碼會覺得很抽像,下例四張圖分別代表範例中的 baseObject、baseToSuperObject、superObject 與 runtimeErrObject 物件,透過將抽像的物件轉換成具體的圖形可以讓我們更加的容易了解物件之間內部的運作情形。 baeObject 物件 在範例中我們利用關鍵字 new 將 BaseClass 類別實體化為物件後,再指派給同樣為BaseClass 物件型別的 baseObject 變數,因此由圖中可以發現 baseObject 與最初實體化BaseClass 物件,所指到的是同一個物件型別,接著由於 BaseClass 繼承自 SuperClass 類別,因此 JavaVM 會先將父類別 SuperCalss 實體化為物件,接著再將子類別 BaseClass 實體化,會這麼做的原因是為了確保父類別物件存在且可使用,如此子類別才能夠順利的呼叫叫父類別的函式。Java 對具有繼承結構的物件方法呼叫有一套處理規則,當我們對物件呼叫某一個函式時,若該物件擁有繼承的架構,則 JavaVM 將會從最底層的子類別開始尋找函式,若找不到再 67
  • 向父類別呼叫尋找,一但在傳遞途中找到了該函式,就不在往上層呼叫,並執行該函式,若一直往上傳遞到最上層的父類都還找不到該函式,則將傳回找不到函式的錯誤訊息。 利用這套規則 baseObject 物件首先呼叫了 superMethod1()函式,並且在 BaseClass 物件中找到了該函式名稱,雖然在父類別 SuperClass 物件中也有提供相同名稱的函式,但因為所有的函式呼叫都是從最底層子類別開始尋找,所以父類別的同名函式將永遠不會被執行到,這種情形在物件導向中稱為覆寫(overriding),至於為何要有覆寫的功能,簡單的說利用覆寫可以不必設定一堆方法的名稱,只需利用一個方法名稱就能依當時物件所屬的類別層級做適當的回應,如此可以讓程式更加的精簡。同樣的 superMethod1 與 baseMethod 的函式呼叫都是依據這套規則,分別會在 SuperClass 與 BaseClass 物件中被呼叫執行。 繼承 圖 1-8 baseObject 物件運作圖 baseToSuperObject 物件 在這裡 baseToSuperObject 物件可以充份的表現出 Java 多型的特性,由下圖可以看到,首先我們將 BaseClass 物件實體化後指派至 SuperClass 型別的 baseToSuperObject 變數,雖然 baseToSuperObject 變數本身是指向 SuperCalss 型別,但本身骨子裡卻是由 BaseClass 子類別所實體化出來的,因此當 baseToSuperObject.Method1()被呼叫時儘管目前的變數型別是指向 SuperClass 物件,但由於 baseToSuperObject 骨子裡還是由 BaseClass 物件實作而成, 68
  • 所以依然必須導守,繼承結構函式呼叫規則,函式呼叫會從最初實體人的型態 BaseClass 物件開 始 尋 找 , 若 找 不 到 再 往 上 層 父 類 別 SuperClass 呼 叫 。 直 得 注 意 的 是baseToSuperObject.baseMethod(); 這行程式碼將會發生編譯時期的錯誤,因為baseToSuperObject 變數目前是指向 SuperClass 型別,而在 SuperClass 物件中並找不到baseMethod()這個函式的存在。解決這類問題的方法有二種,第一種為在父類別 SuperClass撰寫一個空的 baseMethod 函式,如此一樣編譯時期便可以順利的在 SuperClass 物件中找到baseMethod()方法,並由於該方法被子類別 BaseClass 所覆寫,所以在執行時期會先於BaseCalss 中找到該方法並執行。第二種解決方法為直接將 baseTosuperObjet 變數強制轉型回 BaseClass 型別,如此一來在就不會有找不到 baseMethod 函式的問題存在。所以在多型的世界裡,物件可以有多種不同的型別,但骨子裡卻還是保有最初被實體化時的型別,藉由多型的觀念,物件可以依不同的型別層級做出不同的回應,讓程式的設計更加的精簡靈活。 SuperClass baseToSuperObject = new BaseClass(); SuperClass 轉 型 為 Su p e r Cl a s s 物 件 型 態 public void superMethod1()編譯時期錯誤,SuperClass物 public void superMethod2()件中找不到baseMehtod()。 繼承 呼叫父類別實體化 BaseClass 物件實體化 public void superMethod1() x public void baseMethod() baseToSuperObject.superMethod1(); baseToSuperObject.superMethod2(); baseToSuperObject.baseMethod(); ((BaseClass)baseToSuperObject).baseMethod(); 圖 1-9 baseToSuperObject 物件運作圖 superClass 物件 上述二個範例物件都是以 BaseClass 為基礎,接著讓我們改以父類別 SuperCalss 物件為基礎,並呼叫同樣的三個函式,看看會有什麼不同的執行結果。在下圖中 superObject 參數所 69
  • 指向的 物件 型別 為 SuperClass 正好 與 new 關建 字後 所實 體化 的體物 型態 相同 ,因 此superObject 並不會認得 BasseClass 物件,父類別也不會對子類別進行件實體化的動作,(簡單的說在繼承結構裡,只有下層(子類別)會認得上層(父類別)的物件,而上層(父類別)物件並不會知道有誰繼承了它)所以所有的函式呼叫都將會直接跳到 SuperClass 物件裡去尋找,並不在具有繼承結構時的函式呼叫規則,因此當 baseMethod 函式被呼叫時,將會發生編譯時期的錯誤,原因在於 SuperClass 物件中並沒有定義這個函式,此時就算是利用強制轉型的方式,將 superObject 轉型為 BaseClass 型別,雖然在編譯時基是合法的,但在執行時期時依然會錯誤,原因在於 superObject 最初是由 SuperClass 類別實體化而來,所以並不會認得BaseClass 的內容,所以光是強制轉型過去依然是無法執行的。 圖 1-10 superObject 物件運作圖 rutimeErr 物件 在下圖為將 SuperClass 類別實體化為物件後在轉型為 BaseClass 型態,並指派至runtimeErrObjetct 變數中,由於父類別無法自動轉型為子類別,因此在範例中是利用強制轉型的方式,雖然這樣做在編譯時期看來一切都為合法,並不會有什麼錯誤發生,但在執行時期時,由於 SuperClass 物件並不會認得,以下程式碼是一個錯誤的示範,因此在範例程式碼中是被註解起來的,讀者可以試著把註解拿掉編譯看看,編譯器並不會允許以下的語法結構。由於 runtimeErrObject 是由 SuperClass 類別實體化而成的物件,因此子類別 BaseClass 物件將 70
  • 不會被自動的實體化,且 SuperClass 也不會知有那些類別繼承了自己,因此這類的轉型在執行時期是不被允許的。 繼承 圖 1-11 runtimeErObjectr 物件運作圖3.5 介面 介面與類別的不同處在於介面本身並不提供功能的實作,而只具有類別的架構,利用介面可以分離類別的功能面與架構面,利用介面描述類別所提供的功能架構,而實際的功能實作則由類別去完成,所以說介面就等於抽像的類別。介面在程式設計中並非必要的做法,其實就算程式完全沒沒有使用介面也都能夠設計完成,甚至使用了介面反而必須費神分析該從類別中抽離出那些介面,並需額外撰寫關於介面處理的程式碼,即然如此那為何又會有介面的存在呢?的確一切都用具體類別就能快速的解決與完成程式的問題是相當的方便,但是,過度的依賴類別會使得類別與類別之間緊密的結合在一起,之後很難在將類別分開重新在利用於其他程式之中,導至之後撰寫程式一切都需重頭在來過一次。這也就是使用介面的好處之一,透過介面可以將類別的方法架構抽離出來,類別彼此之間透過介面互相結合,就像把類別當成零組件,只要有合適的地方就可以來出來再次利用。介面還有另一項主要的用途,可以使 Java 具有多重繼承的特性。Java 並不像 C/C++提供多重繼承的功能,但使用介面則沒這有這項限制,一個類別可以同時實作多個不同的介面,所以可以使用介面的觀念來達到多重繼承的需求。 71
  • 或許讀者初次使用介面的觀念一時之間會難以適應,但只要有耐心養成使用介面撰寫程式的良好習慣,相信很快的就可以體驗到程式再利用所帶來的好處。首先讓我們先看到介面的語法結構: public interface 介面名稱{ final 常數型態宣告 常數名稱; public void 抽像方法名稱(); } 介面的宣告與類別相當類似,類別使用的是 class 關鍵字,而介面使用的則是 interface,關鍵字之後接的是介面的名稱,這裡與類別命名規則一樣,介面名稱必須於程式檔案名稱相同。介面裡所宣告的所有變數都將被自動冠上 final 關鍵字,因此介面中的變數皆為常數不允許被更動。介面中的方法皆為抽像的方法,並不會進行方法的實作,所以介面中所有的抽像方法宣告都是以雙引號 ; 做為結尾,例如: public void test(); public void run();在介面中並不需要利用 abstract 修飾字,所有在介面中所宣告的方法會自動被視為抽像的,以下為一個簡單的介面宣告範例: //Book.java 介面範例: public interface Book { //設定書本資訊的介面 public void setInfo(String name,String author, int price); //取得書本資訊的介面 public String getInfo(); }Book 為一個關於書藉資料的介面,透過介面將一般書藉常見的資訊抽離出來,像是作者書本名稱、作者名稱、價格,任何只要有實作 Book 介面的程式就必須實介面中所宣告的方法。在 72
  • 撰寫完介面之後,必須於類別中使用 implements 關鍵字來宣告此類別所實作的介面,另外implements 關鍵字後可以同時宣告多個介面,每個不同介面名稱之間需使用逗號方開,語法使用方式如下: 修飾詞 class 類別名 implements 介面名1, 介面名2, 介面名3{ }底下為一個完整的介面範例程式【介面範例程式】 //BookOne.java 實作Book介面 public class BookOne implements Book{//實作Book介面 String bookName; String authorName; int bookPrice; //實作取得書本資訊介面 public String getInfo() { String info="Book Name="+bookName+ " Author Name="+authorName+" Price="+bookPrice; return info; } //實作設定書本資訊介面 public void setInfo(String name, String author, int price) { bookName=name; authorName=author; bookPrice=price; } } //BookTwo.java 實作Book介面 public class BookTwo implements Book{//實作Book介面 String bookName; String authorName; int bookPrice; //實作取得書本資訊介面 73
  • public String getInfo() { String bookInfo="Book NAME t Author Name t pricen"+ bookName+" tt "+authorName+" tt "+bookPrice; return bookInfo; } //實作設定書本資訊介面 public void setInfo(String name, String author, int price) { bookName=name; authorName=author; //打八折 int)(price*0.8); bookPrice=(int int } } //BookMain.java 介面應用範例主程式 public class BookMain { public static void main(String args[]){ //取得BookOne物件,並轉型為Book介面 Book book1=new BookOne(); new //利用setInfo介面設定書本資訊 book1.setInfo("LatteBox", "Jarey", 100); //取得BookTwo物件,並轉型為Book介面 Book book2=new BookTwo(); new //利用setInfo介面設定書本資訊 book2.setInfo("java", "tony", 200); //利用getInfo介面取出書本資訊 System.out.println("nbook1的資訊為:"); System.out.println(book1.getInfo()); System.out.println("nbook2的資訊為:"); System.out.println(book2.getInfo()); } }【執行結果】 74
  • 圖 1-12 多型範例執行結果 在範例中,BookOne 與 BookTwo 為兩個不同的類別,BookTwo 在設定價格時,會自動將書本的價格打八折,另外兩者在顯示書藉資訊格式也略有不同,但兩個類別都同時實作 Book 介面,因此可以確定的是兩個類別一定會提供 setInfo 與 getIfno 這兩個函式。在主程式中BookOne 與 BookTwo 物件雖然與 Book 無繼承的關係,但卻擁有界面實作的關係,所以可以自動的轉型為 Book 介面型別(因為 Book 所提供的函式一定能於實作的類別中呼叫成功) ,隨後的操作就都只認介面不認類別。利用介面可以不用費心去了解介面後面的實作方式為何,直接可以透過介面所提供函式進行操作,就如同範例中 BookOne 與 BookTwo 兩個物件對於 Book 介面實作的方式略有不同(如印出資訊的格式、與價格計算方式),但主程式中對於兩個物件的操作方式卻都是相同的(同樣呼叫 setInfo 與 getInfo 函式)。讀者可以自行設計其它實作 Book介面的類別,替換掉原本的 BookOne 或 BookTwo 類別,且無需更動到主程式中函式對於 Book的操作方式,就能另程式有不同的風格展現,這也就是利用介面將程式函式架構從實作面抽離的好處。3.6 (抽像類別 Abstract class)在介面中我們可以定義方法的名稱與傳入、傳出的型態,但不去實作當中的邏輯。抽像類別與介面不同之處在於,抽像類別可以同時包含抽像的方法,與己實作的法方。在抽像類別中我們可以同時定議許多的抽像方法 Abstract method(如同介面並實作其中的邏輯) ,但也可以包含其他己經具有邏輯實作的方法。 簡單的說我們可以把抽像類別看成一個完成一半的程式,另一半未完成的部份我們只明確定義了操的介面,實作部份則交由其他的類別去完成。我們將 75
  • 上節中的 Student 範例改寫成 Abstract 類別:package com.ittraining.example;abstract class AbstractStudent { private static String SCHOOL_ONE = "建國中學"; private static String SCHOOL_TWO = "北一女中"; private static String SCHOOL_THREE = "景美女中"; private int classID; // 學號 private String name; // 名稱 private String schoolName = "建國中學";// 學校 public void setClasssID(int classID) { this.classID = classID; } public AbstractStudent() { super(); } public AbstractStudent(int classID, String name, String schoolName) { this.classID = classID; this.name = name; this.schoolName = schoolName; } public void setName(String name) { this.name = name; } public int getClassID() { return classID; } public String getName() { return name; } public void setSchoolName(String schoolName) { this.schoolName = schoolName; } 76
  • public String getSchoolName() { return schoolName; } abstract void printData();}在上述的範例中,我們將 printData 方法宣告成抽像方法(Abstract Method),並將實作的內容去除掉 接著我們在撰寫另一個類別繼承 Student 類別 並對 printData 抽像方法進行實作 。 , 。package com.ittraining.example;public class StudentImpl extends AbstractStudent { void printData() { System.out.println("姓名:" + this.getName() + "n學號:" + this.getClassID() + "n學校:"+ this.getSchoolName()); } public static void main(String args[]){ AbstractStudent st1= new StudentImpl(); st1.printData(); }} 使用 Abstract 設計可以讓你的程式架構更好,更具彈性,我們可以將程式的主架構撰寫 好,而部份的實作細節可以切開由其他不同的類別去進行實作,如此當程式的功能有 需要變動時,我們只需重新撰寫一套新的實作類別,便可直接擴充程式的功能,而無 需回過頭去將整套程式改寫.事實上,上述的例子即是一個 Template Method 的 Pattern,使用抽象類別與方法來實作 Template Method 模式,在很多應用場合都可 以見到。 77