Java/Object-Oriented Programming in Java

From YuntechWiki

Jump to: navigation, search

Java物件導向程式設計 董少桓編著,版權所有

Contents

實例、實例變數與實例方法

Java 是一個物件導向程式語言。物件導向的基本觀念是讓程式可以描述、建構及處理真實世界中所看到的物件並設計它們之間的層次關係。例如:Exam 可以有 EnglishExam、ChineseExam 兩個副屬的種類。而一次 EnglishExam 又可以有任意數量的考生應考,其中每一份考卷中都可以有考生姓名與成績的資料。這時 Exam、EnglishExam 及 ChineseExam 便很適合設計成類別,而一份英文考試的考卷便可以用 EnglishExam 這個類別所生成的實例(instance or object)來代表。

一個類別可以使用 new 指令來產生實例。一個 Java 的程式可以由許多個有層次關係的類別構成,這個層次關係描述這些類別間的繼承關係。這一類的例子在真實世界中不勝枚舉,舉例來說:如果「哺乳類」是一個類別,那麼「人類」則可以是「哺乳類」的一個「子類別」(subclass or subtype),相對的「哺乳類」也可以是「人類」的「父類別」(superclass or supertype)。而某一個人「張三」便是「人類」的一個「實例」

以「張三」這個實例來說,他的身高、體重、性別等資料都可以存放在這個實例的「實例變數」中,而「張三」能「跑」能「跳」,則可以是它的「實例方法」。簡單的說:「類別(class)」可以用 new 來產生「實例(instance)」,而「實例」可以包含「實例變數」與「實例方法」。其中「實例變數」負責資料的儲存,而「實例方法」則負責資料的處理

Image001.jpg

註:Java 原文將「實例變數」稱為 non-static field;而將「實例方法」稱為 non-static method。為了與中文的譯名一致,這份講義將以 instance variable 與 instance method 稱之。同樣的,這份講義亦將以 class method 代替原文的 static method,以 class variable 代替原文的 static variable。


何謂實例(Instance)

實例(instance)是由 new 所產生的某個類別的實作,也稱為物件(object)。一個 class 可以生成多個實例,而且每個實例都擁有一份自己的實例變數。以下圖為例:class A 宣告了三個實例變數 x,y 與 z,而每個實例都為這三個實例變數配置了記憶體以儲存他們的值。

NewImage005.jpg


「實例變數」及「實例方法」與用於設計結構化程式的「類別變數」及「類別方法」的主要差異是:

  • 類別變數或類別方法以 static 起始,他們在程式開始執行時即實質的存在,所以不需要產生實例,即可使用。
  • 實例變數或實例方法沒有 static 起始,他們一定要等到實例產生之後才有實質的存在,因此需要實例,才可以使用。

在後續的圖例中,我們將使用「淡的顏色」來表示「沒有記憶體、沒有實質存在的變數或方法」,並以「鮮明的顏色」來表示「有記憶體、有實質存在的變數或方法」以區別它們的差異: Picture.jpg

以下是一個藉由呼叫實例方法將實例變數的值傳回並輸出的範例:

class Book {
    String name = "Sun";
    String getName() {
        return name;
    }
}

public class Ex2 {
    public static void main(String argv[]) {
        Book b1 = new Book();
        Book b2 = new Book();
        System.out.println("Name of b1:" + b1.getName());
        System.out.println("Name of b2:" + b2.getName());
    }
}

執行結果:

Name of b1:Sun
Name of b2:Sun

觀看執行過程及詳細解說

Class Method 與 Instance Method 的差異

接下來的這個範例,說明了類別方法與實例方法的差異:

class A {
    static String c1() {
        return "class method不需new就可使用";
    }
    String c2() {
        return "instance method需new才可使用";
    }
} 

public class Ex3 {
    public static void main(String argv[]) {
        System.out.println("=呼叫class method=");
        System.out.println(A.c1());
        System.out.println("=呼叫instance method=");

        // System.out.println(A.c2());=>error

        A a = new A();
        System.out.println(a.c2());
    } 
}

執行結果:

=呼叫類別方法=
class method不需new就可使用
=呼叫實例方法=
instance method需new才可使用

觀看執行過程及詳細解說

Class Variable 與 Instance Variable 的差異

這個範例繼續說明了類別變數與實例變數之間的差異:

class A {
    static int a = 0;
    int b = 5;
    int getA() {
        return a;
    }
    void setA(int value) {
        a = value;
    }
    int getB() {
        return b;
    }
    void setB(int value) {
        b = value;
    }
}

public class Ex4 {
    public static void main(String argv[]) {
        A obj1 = new A();
        A obj2 = new A();
        obj1.setA(1);
        obj1.setB(20);
        obj2.setA(2);
        obj2.setB(30);
        System.out.println("a of obj1:" + obj1.getA());
        System.out.println("a of obj2:" + obj2.getA());
        System.out.println("b of obj1:" + obj1.getB());
        System.out.println("b of obj2:" + obj2.getB());
    }
}

執行結果:

a of obj1:2
a of obj2:2
b of obj1:20
b of obj2:30

觀看執行過程

由於 a 是類別變數,只有一個儲存值,而這個值是共享的;b 則是實例變數,所以每個實例都有一個 b 的儲存值,而它們的值也可以不一樣。

類別變數、區域變數與實例變數的不同之處,透過它們在 Java 程式內的可見範圍(scope)與佔用記憶體的起始時間與釋放時間(extent)的不同,可以更明確的區分出來:

  1. 類別變數:每一個 class 都有一個命名空間(name space),因此兩個 class 若有同名的變數、方法也不用擔心彼此衝突。這便好像將一個程式的記憶體劃分成好幾個區域,而每一個 class 都配屬了一個區域。隸屬於某一 class 的類別變數的有效時間是從程式開始執行,一直到程式結束:
    • extent:從程式開始執行,一直到程式結束;
    • scope:以 public、private、protect 宣告類別變數的可見範圍。
  2. 區域變數:於方法執行時才存在,方法執行完畢後便消失。這種變數的值是存放在記憶體中稱為堆疊(stack)的一塊區域上。
    • extent:從方法執行時開始,到方法結束時為止。
    • scope:該變數所屬,由 { } 區隔的區塊(block)中。
  3. 實例變數:實例變數是用來的儲存實例中資料的變數,實例(instance)的另一個名稱是物件(object)。例如在一個處理學生資料的程式中,某一學生的資料,可以存放在 name、id、score 等幾個隸屬於這個物件的實例變數中。實例是存放在記憶體中稱為堆積堆(heap)的一塊區域上。
    • extent:從一個實例被造(new),直到它的記憶體被回收(garbage collect)時為止。
    • scope:實例變數只能在實例產生後才能使用,所以是以該實例所存放的變數的scope與extent以及public, private, protect來共同判定。

實例方法中的this是什麼?

這個單元將會以數個範例,將一個類別方法,轉換成作用相同的實例方法,以更深入的闡述在實例方法中常常用到的this這個保留字的意義。

在下面的範例中 ee 是一個用 new 生成的 EnglishExam 物件,這個物件的三個實例變數分別被設值成 3, 4, 5,然後傳入 englishScore 這個類別方法中:

class EnglishExam {
  public int vocab, grammar, listen;
  public static int englishScore(int v, int g, int l) {
    return v + g + l;
  }
}

public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    System.out.print("The score of the exam is ");
    System.out.println(EnglishExam.englishScore(ee.vocab, ee.grammar, ee.listen));
  }
}

執行結果:

The score of the exam is 12

englishScore 雖然是一個類別方方法,然而傳入這個方法的三個參數,實際上是 ee 的三個實例變數的值。那何必這麼麻煩?直接將 ee 傳入 englishScore 不是更簡潔嗎?以下即是將上例改寫後的版本:

class EnglishExam {
  public int vocab, grammar, listen;
  public static int score(EnglishExam THIS) {
    return THIS.vocab + THIS.grammar + THIS.listen;
  }
} 

public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    System.out.print("The score of the exam is ");
    System.out.println(EnglishExam.score(ee));
  }
}

這個範例直接將 ee 傳入 score(原名是 englishScoer)中,而 score 仍然是一個類別方法。在這個例子中我們使用:

EnglishExam.score(ee);

來呼叫score這個方法。如果我們將以上的呼叫改寫成:

ee.score();

並將 score 這個類別方法,改寫成實例方法:

class EnglishExam {
  public int vocab, grammar, listen;
  public int score() {
    return vocab + grammar + listen;
  }
} 

public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    System.out.print("The score of the exam is ");
    System.out.println(ee.score());
  }
}

那麼執行的結果仍然是:

The score of the exam is 12

由此可見,一個 instance method 其實有一個隱藏的參數。以上例而言是 ee,而任何在 score 方法中所用到的變數,如果不是區域變數或是類別變數,便會被當成是該隱藏性參數(ee)的實例變數。

如果我們需要支援 ChineseExam,那麼上面的程式可以進一步改寫如下:

class EnglishExam {
  public int vocab, grammar, listen;
  public int score() {
    return vocab + grammar + listen;
  }
}
class ChineseExam {
  public int word, sentence, composition;
  public int score() {
    return word + sentence + composition;
  }
}
public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    ChineseExam cc = new ChineseExam();
    cc.word = 2; cc.sentence = 3; cc.composition = 4;
    System.out.print("The score of the English exam is ");
    System.out.println(ee.score());
    System.out.print("The score of the Chinese exam is ");
    System.out.println(cc.score());
  }
}

前面所提到的隱藏參數,可以使用 this 這個保留字來抓到。因此,上例也可以改寫成:

class EnglishExam {
  public int vocab, grammar, listen;
  public int score() {
    return this.vocab + this.grammar + this.listen;
  }
}
class ChineseExam {
  public int word, sentence, composition;
  public int score() {
    return this.word + this.sentence + this.composition;
  }
}
public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    ChineseExam cc = new ChineseExam();
    cc.word = 2; cc.sentence = 3; cc.composition = 4;
    System.out.print("The score of the English exam is ");
    System.out.println(ee.score());
    System.out.print("The score of the Chinese exam is ");
    System.out.println(cc.score());
  }
}

this 的值是當使用一個實例呼叫一個實例方法(例如:ee.score())時所傳入的隱藏性參數,而這個隱藏性參數便是那個實例(即:ee)的地址。因此,如果你在一個實例方法內更改一個實例變數的值時,那個實例變數的值便永遠被更改。

Java 可以使用 this 也可以不使用 this 來存取實例的變數或方法。在後續的單元,我們為了說明的需要,有時會使用 this,有時不使用 this,來存取實例變數或呼叫實例方法。

實例方法也可以有其他參數,例如,以下便是將權重傳入並計算分數的例子:

class EnglishExam {
  public int vocab, grammar, listen;
  public double score(double wb, double wg, double wl) {
    return wb * vocab + wg * grammar + wl * listen;
  }
}
public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    System.out.print("The score of the English exam is ");
    System.out.println(ee.score(0.2, 0.3, 0.5));
  }
}

在前面的單元中我們曾經使用 int, double 等型態的變數,這種變數稱為 primitive type。而 ee 的型態則是 EnglishExam,這種型態稱為 reference type。一個 reference type 變數的初值是 null,代表沒有任何實例的地址被設成它的值。Java 的字串屬於 String 類別,而字串也是 reference type 的一種。一個變數如果是 primitive type,則它的地址中所存放的是數值本身。如果一個變數是 reference type,則這個變數的地址內所存放的是一個指向一個實例的地址。這個被指向的實例是放在 Java 自動化記憶體管理區內,如果這個實例沒有被任何其他變數直接或間接的透過其他實例指到,那麼 Java 的 garbage collector 便會在執行記憶體回收動作時將該實例的記憶體自動回收。

由於 Java 是一個「傳值呼叫」(call-by-value)的程式語言,所以當一個方法被呼叫時,是變數的值被傳入方法中。由於存放在 reference type 變數中的值,實際上是地址,所以是地址被傳入被呼叫的方法內。因此便會發生在程式執行中,同時有數個變數指向同一個被 reference 的實例。而其中任何一個方法更改了那個實例的實例變數的值時,其他使用這個實例的方法也會看到被改變的新值。

實例的產生與封裝

在前面的章節中,我們曾經使用 new 來產生一個 EnglishExam 的實例,然後個別的將這個實例的實例變數初始化。例如:

EnglishExam ee = new EnglishExam();
ee.vocab = 3; ee.grammar = 4; ee.listen = 5;

另一個達到同樣目的的方式是在類別內直接的初始化實例變數的值。例如:

class EnglishExam {
  public int vocab = 6, grammar = 7, listen = 8;
  public int score() {
    return vocab + grammar + listen;
  }
}

如果一個實例變數沒有初始化,那麼它的值是便會根據它的型態,而設定成該型態的 default value。

Java 也支援使用一個類別的 constructor(建構子)將實例變數的值初始化。建構子與 class 同名,並且不需要定義 return type。呼叫建構子之後會製造一個物件並將那個物件回傳。例如:

class EnglishExam {
  public int vocab, grammar, listen;
  EnglishExam() {
    vocab = 6; grammar = 7; listen = 8;
  }
  public int score() {
    return vocab + grammar + listen;
  }
}

public class Exem {
  public static void main(String args[]) {
    EnglishExam ee = new EnglishExam();
    System.out.println("The score of the exam is: " + ee.score());
  }
}

執行結果:

The score of the exam is: 21

建構子也可以使用傳入的參數,將實例變數的值初始化。例如:

class EnglishExam {
  public int vocab, grammar, listen;
  EnglishExam() {
    vocab = 6; grammar = 7; listen = 8;
  }
  EnglishExam(int v, int g, int l) {
    vocab = v; grammar = g; listen = l;
  }
  public int score() {
    return vocab + grammar + listen;
  }
}

public class Exam {
  public static void main(String args[]) {
    EnglishExam ee = new EnglishExam();
    System.out.println("The score of the first exam is: " + ee.score());
    ee = new EnglishExam(7, 8, 9);
    System.out.println("The score of the second exam is: " + ee.score());
  }
}

這時的執行結果是:

The score of the first exam is: 21
The score of the second exam is: 24

以上的程式有一個沒有參數的建構子,及一個三個參數的建構子。如果一個類別內沒有定義建構子,那麼 Java 會自動提供一個沒有參數的建構子給這個類別。

觀看一個建構子範例的執行與解說

Getter, Setter 及 Data Abstraction

假設有一個 Exam 類別,而這個類別有一個 minute 的實例變數,而 e 則是一個 Exam 的實例。那麼使用 e.minute 便可以直接的取用它的值,然而 minute 卻必須宣告成 public。另一個存取實例變數的方式是使用 getter 及 setter 方法,這時程式設計師可以將 minute 宣告成 private,並選擇性的將 getter 及 setter 設定成所需要的存取權限。例如:

public class Exam {
  private int minutes;
  public Exam() {
    minutes = 80;
  }
  public int getMinutes() {
    return minutes;
  }
}

這時在其他的類別中若有一個 Exam 的實例 e,便可以使用 getMinutes 取出 e.minutes 的值:

e.getMinutes()

使用 getter 方法的好處之一是,能夠很容易的在取用實例變數的值時,增加協助偵錯的程式碼:

public class Exam {
  private int minutes;
  public Exam() {
    minutes = 80;
  }
  public int getMinutes() {
    System.out.println("Accessing minutes...");
    return minutes;
  }
}

setter 方法的作用也類似:

public class Exam {
  private int minutes;
  public Exam() {
    minutes = 80;
  }
  public int getMinutes() {
    return minutes;
  }
  public void setMinutes(int m) {
    System.out.println("Setting minutes...");
    minutes = m;
  }
}

然而 setter 方法不需要傳回值。因此 setMinutes 的傳回值的型態是void。

getter 與 setter 方法的另一個功用是,模擬不必真實的存在,但是卻可以透過運算而得到的實例變數。例如:

public class Exam {
  private int minutes;
  public Exam() {
    minutes = 80;
  }
  public int getMinutes() {
    return minutes;
  }
  public void setMinutes(int m) {
    minutes = m;
  }
  public int getHours() {
    return minutes/ 60.0;
  }
  public void setHours(double h) {
    minutes = (int)(h * 60);
  }
}

使用以上的 getHours 及 setHours 使得 Exam 好像多了 hours 這個其實並不存在的實例變數一般。

使用 getter 及 setter 方法,可以在改變實例內資料的儲存方式後,不用修改其他使用此資料的程式碼,讓程式易於維護。例如:hours 比 minutes 更常用時,便可以將上面的範例,更改成以 hours 來儲存,以增加程式的效率:

public class Exam {
  private double hours;
  public Exam() {
    hours = 1.5;
  }
  public int getMinutes() {
    return (int)(hours * 60);
  }
  public void setMinutes(int m) {
    hours = m / 60.0;
  }
  public double getHours() {
    return hours;
  }
  public void setHours(double h) {
    hours = h;
  }
}

這時程式碼雖然做了更改,但是並不影響程式的其他部份。如果沒有使用 getter 與 setter,類似的更動,便需要將程式碼中所有存取 minutes 的地方全部做修改。例如:

x.minutes           需要更改成     (int)(x.hours * 60)
(x.minutes / 60.0) 則需要更改成 x.hours

使用 getter 及 setter 方法,間接的存取實例變數的程式設計方式稱為「資料抽象化」(data abstraction)。 應用資料抽象化的方式寫程式,有以下的好處:

  1. 程式碼容易再利用
  2. 程式碼容易瞭解
  3. 易於增加類別的功能
  4. 易於改進資料的儲存方式

繼承、抽象類別

如果一個程式有以下幾個類別:

Class A{}
Class B extends A{}
Class C extends A{}
Class D extends B{}
Class E extends B{}

那麼它們之間的繼承與所產生的實例有以下的關係。其中繼承是以樹狀階層圖表示,而實例則以對應的多層次的同心圓來表示,同心圓的最內層對應最 super 的類別,而同心圓的最外層則對應這個實例所屬的類別:

Inheritance.jpg

假設我們要將前面的 EnglishExam 及 ChineseExam 增加一個 minutes 的實例變數。那麼一個不好的方式是將這個實例變數分別的放入這兩個類別中:

class EnglishExam {
  public int minutes;
  public int vocab, grammar, listen;
  public int getMinutes() {
    return minutes;
  }
  public int score() {
    return vocab + grammar + listen;
  }
}
class ChineseExam {
  public int minutes;
  public int word, sentence, composition;
  public int getMinutes() {
    return minutes;
  }
  public int score() {
    return word + sentence + composition;
  }
}
public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.minutes = 75;
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    ChineseExam cc = new ChineseExam();
    ee.minutes = 75;
    cc.word = 2; cc.sentence = 3; cc.composition = 4;
    System.out.print("The score of the English exam is ");
    System.out.println(ee.score());
    System.out.println("English exam takes " + ee.getMinutes() + "minutes.");
    System.out.print("The score of the Chinese exam is ");
    System.out.println(cc.score());
    System.out.println("Chinese exam takes " + cc.getMinutes() + "minutes.");
  }
}

不好的原因是 EnglishExam 及 ChineseExam 中有許多重複的程式碼。而這些重複的程式碼會增加開發的成本及維護的負擔。所幸 Java 容許我們將類別設計成階層式的結構,而這個階層的結構可以自然的將不同類別間的繼承關係表達出來。因此,我們可以設計一個 Exam 類別,並將 minutes 宣告在 Exam 內;再由 EnglishExam 及 ChineseExam 繼承 Exam 即可。Java 使用 extends 這個保留字讓一個類別繼承另一個類別。

class Exam {
  public int minutes;
  public Exam() {
    System.out.println("Calling Exam()...");
    minutes = 75;
  }
  public int getMinutes() {
    return minutes;
  }
}

class EnglishExam extends Exam {
  public int vocab, grammar, listen; 
  public int score() {
    return vocab + grammar + listen;
  }
}

class ChineseExam extends Exam {
  public int word, sentence, composition;
  public int score() {
    return word + sentence + composition;
  }
}

public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ee.vocab = 3; ee.grammar = 4; ee.listen = 5;
    ChineseExam cc = new ChineseExam();
    cc.word = 2; cc.sentence = 3; cc.composition = 4;
    System.out.print("The score of the English exam is ");
    System.out.println(ee.score());
    System.out.println("English exam takes " + ee.getMinutes() + "minutes.");
    System.out.print("The score of the Chinese exam is ");
    System.out.println(cc.score());
    System.out.println("Chinese exam takes " + cc.getMinutes() + "minutes.");
  }
}

這時 Exam 是 EnglishExam 及 ChineseExam 的父類別,而 EnglishExam 及 ChineseExam 是 Exam 的子類別。Exam 也有一個父類別,這個類別是 Java 內建的 Object 類別。一個 Java 程式內所有的類別都直接或間接的繼承了 Object 類別。Exam 也可以寫成:

class Exam extends Object {
...
}

除了減少重複不必要的程式碼以外,類別的繼承還有以下兩個好處:

  1. 讓父類別的程式碼可以在完全除錯後,才被子類別繼承。這樣可以讓程式的偵錯更為容易。
  2. 可以購買軟體廠商已經開發好的類別,再透過繼承擴充其功能。

一個子類別的實例,含有自己類別的實例變數與方法,以及所有父類別的實例變數與方法。例如:ee 這個實例便有自己定義的 vocab, grammar, listen 及繼承而來的 minutes 四個實例變數及 score 及 getMinutes 兩個方法。

通常將實例變數與實例方法放置於父類別時,需要滿足以下兩個條件:

  1. 可以減少重複的程式碼。
  2. 父類別的實例變數或實例方法對子類別有用處。例如:Exam 內的 minutes 便對 ChineseExam 及 EnglishExam 有用。

當類別間有繼承關係時,建構子的呼叫順序是先執行父類別的建構子。例如:

class Exam {
  public int minutes;
  public Exam() {
    System.out.println("Calling Exam()...");
    minutes = 75;
  }
  public int getMinutes() {
    return minutes;
  }
}

class EnglishExam extends Exam {
  public int vocab, grammar, listen; 
  public EnglishExam() {
    System.out.println("Calling EnglishExam()...");
    vocab = 7; grammar = 7; listen = 7;
  }
  public int score() {
    return vocab + grammar + listen;
  }
}

class ChineseExam extends Exam{
  public int word, sentence, composition;
  public ChineseExam() {
    System.out.println("Calling ChineseExam()...");
    word = 7; sentence = 7; composition = 7;
  }
  public int score() {
    return word + sentence + composition;
  }
}

public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    ChineseExam cc = new ChineseExam();
  }
}

會得到以下的執行結果:

Calling Exam()...
Calling EnglishExam()...
Calling Exam()...
Calling ChineseExam()...

如果我們需要將 EnglishExam 更加的細分。例如:增加一個 GREEnglishExam 類別,而這個類別的特性是 listen 實例變數的值是 0:

class Exam {
  public int minutes;
  public Exam() {
    minutes = 75;
  }
  public int getMinutes() {
    return minutes;
  }
}

class EnglishExam extends Exam {
  public int vocab, grammar, listen; 
  public EnglishExam() {
    vocab = 7; grammar = 7; listen = 7;
  }
  public int score() {
    return vocab + grammar + listen;
  }
}

class GREEnglishExam extends EnglishExam {
  public int score() {
    return vocab + grammar + 0;
  }
}

class ChineseExam extends Exam{
  public int word, sentence, composition;
  public ChineseExam() {
    word = 7; sentence = 7; composition = 7;
  }
  public int score() {
    return word + sentence + composition;
  }
}

public class Demo {
  public static void main(String argv[]) {
    EnglishExam ee = new EnglishExam();
    GREEnglishExam gre = new GREEnglishExam();
    System.out.println("English exam score is " + ee.score());
    System.out.println("GRE English exam score is " + gre.score());
  }
}

這時執行的結果會得到:

English exam score is 21
GRE English exam score is 14

呼叫 gre.score() 得到 14 的原因是,GREEnglishExam 的 score 方法遮蔽了 EnglishExam 的 score 方法,而名稱相同的方法有以下兩種關係:

  1. Overloading:是指參數輸入的個數或類別不同,但是卻同名的方法。
  2. Shadowing or overriding:這是指數個方法同名,而參數的個數與型態也相同,但是卻分別的定義在不同的類別的方法。

以上例而言 gre.score() 會呼叫 GREEnglishExam 的 score 方法,而 ee.score() 則會呼叫 EnglishExam 的 score 方法。在執行時呼叫幾個同名的方法的哪一個,是根據實例所屬的類別,例如:gre 的類別是 GREEnglishExam 所以 gre.score() 會呼叫 GREEnglishExam 的 score 方法。如果 GREEnglishExam 沒有定義被呼叫的方法,例如:gre.getMinutes(),這時則會呼叫其父類別的方法,如果父類別中也沒有定義,則會呼叫祖父類別的方法,依此類推。所以 gre.getMinutes()會呼叫到 Exam 的 getMinutes 方法。

private 與 protected 變數與方法

在討論 getter, setter 方法時,我們談到資料抽象化的好處。但是如果那個實例變數本身(例如:minutes)仍然是定義成 public 那麼將失去強制性,也就是其他的程式設計師仍然可以繞過 getter 與 setter 方法而直接的存取 minutes。

private 與 protected 兩個保留字可以設定某個變數或方法的存取範圍。private 變數或方法的存取範圍為自己的類別內,而 protected 變數或方法則包括自己的類別、子類別及所屬的 package 內。至於一個 package 則是由數個為了完成某種功能的類別所組合而成。例如:一個類別 C 若屬於一個 package p,則 C 的程式碼必須以

package p; 

起始,而且也必須存放在一個命名為 p 的目錄中。一個 Java 的檔案,如果要使用 C 或 p 所提供的功能,則可以用下兩種方式,輸入 C 或 p 內所有非 private 的名字:

import p.C;
import p.*;

一個 package 也可以使用

jar vcf p.jar p 

指令將其中的 .class 檔包裹起來,放在 jdk...\jre\lib\ext 的目錄中供其他程式使用。

private 與 protected 的變數與方法可以有許多交替使用的可能。例如:

class Exam {
  public Exam() {
    minutes = 75;
  }
  public int getMinutes() {
    return minutes;
  }
  public void setMinutes(int m) {
    minutes = m;
  }
  private int minutes;
}

以上的範例將 minutes 宣告為 private 而將 getMinutes, setMinutes 宣告為 public。這時只有 Exam 能直接存取 minutes,而程式中所有其他的類別都可以透過 getMinutes, setMinutes 間接的存取 minutes。Java 的習慣是將 private 的變數或方法宣告在 public 的變數或方法的下方。一個類別的 public 變數或方法,形成了這個類別對其他類別的 public interface 或公開的介面。

class Exam {
  public Exam() {
    minutes = 75;
  }
  public int getMinutes() {
    return minutes;
  }
  private int minutes;
}

而這個範例,則讓實例在初始化時便將 minutes 設值為75,由於沒有 setMinutes 而 minutes 又是 private,所以其他類別的程式碼將無法更動 minutes 的值。

class Exam {
  public Exam() {
    minutes = 75;
  }
  public int getMinutes() {
    return minutes;
  }
  protected int minutes;
}

這個範例則容許 Exam 的子類別及與 Exam 位於同樣 package 內的類別直接存取 minutes。

class Exam {
  public Exam() {
    minutes = 75;
  }
  protected int getMinutes() {
    return minutes;
  }
  protected void setMinutes(int m) {
    minutes = m;
  }
  private int minutes;
}

而這個範例則只有 Exam 能直接存取 minutes,同時也只有 Exam 的子類別及位於相同 package 中的類別能夠透過 getMinutes 及 setMinutes 間接的存取 minutes。

建構子之間的呼叫

如果在建構一個 EnglishExam 實例時要同時傳入四個參數值給 vocab, grammar, listen, minutes 四個實例變數,並將其初始化。而且也要能夠只傳入三個參數值給 vocab, grammar, listen,那麼一種寫這些建構子的方式是:

class EnglishExam extends Exam {
  public int vocab, grammar, listen;
  public EnglishExam() {
   vocab = 6; grammar = 6; listen = 6;
  }
  public EnglishExam(int v, int g, int l) {
    vocab = v; grammar = g; listen = l;
  }
  public EnglishExam(int v, int g, int l, int m) {
    vocab = v; grammar = g;  listen = l;
    minutes = m;
  }
  public int score() {
    return vocab + grammar + listen;
  }
}    

這個寫法有一個缺點就是

vocab = v; grammar = g; listen = l; 

出現兩次。避免這些重複程式碼的方法是使用 this(v, g, l) 去呼叫那個三個參數的建構子:

class EnglishExam extends Exam {
  public int vocab, grammar, listen;
  public EnglishExam() {
    vocab = 6; grammar = 6; listen = 6;
  }
  public EnglishExam(int v, int g, int l) {
    vocab = v; grammar = g; listen = l;
  }
  public EnglishExam(int v, int g, int l, int m) {
    this(v, g, l);
    minutes = m;
  } 
  public int score() {
    return vocab + grammar + listen;
  }
}    

在建構子中使用 this(...) 指的是呼叫另一個,在相同的類別中,參數的個數與型態皆相同的建構子。要注意的是在建構子中使用 this(...),一定要寫在建構子的第一行。

另一種可能是當 EnglishExam 與 ChineseExam 都需要呼叫一個參數的建構子將 minutes 初始化。一種不好的寫法是:

class EnglishExam extends Exam {
  ...
  public EnglishExam(int m) {
    minutes = m;
  }
  ...
}
class ChineseExam extends Exam {
  ...
  public ChineseExam(int m) {
    minutes = m;
  }
  ...
}

這時的

minutes = m;

也是同樣的重複在兩個類別中。改良的寫法是使用 super(m) 去呼叫定義在 Exam 類別內的一個參數的建構子:

class Exam {
  public int getMinutes() {
    return minutes;
  }
  public Exam() {
    minutes = 75;
  }
  public Exam(int m) {
    minutes = m;
  }
  private int minutes;
}
class EnglishExam extends Exam {
  ...
  public EnglishExam(int m) {
    super(m);
  }
  ...
}
class ChineseExam extends Exam {
  ...
  public ChineseExam(int m) {
    super(m);
  }
  ...
}

實例方法間的呼叫

如果你需要擴充 Exam、EnglishExam 與 ChineseExam,使它們能夠產出一份,包括考試時間、分數的考試資料。例如:

Chinese exam score: 88 Exam time: 75 minutes
English exam score: 76 Exam time: 60 minutes

第一種完成這個程式的寫法是為 EnglishExam 與 ChineseExam 都提供一個 report 方法:

class Exam {
  public int minutes;
  Exam(int m) {
    minutes = m;
  }
  public int getMinutes() {
    return minutes;
  }
}

class EnglishExam extends Exam {
  public int vocab, grammar, listen;
  EnglishExam(int v, int g, int l) {
    super(60);
    vocab = v;
    grammar = g;
    listen = l;
  } 
  public int score() {
    return vocab + grammar + listen;
  }
  public void report() {
    System.out.println("English exam score: " + score() + 
               " Exam time: " + getMinutes() + " minutes"); 
  }
}

class ChineseExam extends Exam{
  public int word, sentence, composition;
  ChineseExam(int w, int s, int c) {
    super(75);
    word = w;
    sentence = s;
    composition = c;
  }
  public int score() {
    return word + sentence + composition;
  }
  public void report() {
    System.out.println("Chinese exam score: " + score() + 
               " Exam time: " + getMinutes() + " minutes"); 
  }
}

public class Demo {
  public static void main(String argv[]) {
    ChineseExam cc = new ChineseExam(35, 35, 18);    
    cc.report();
    EnglishExam ee = new EnglishExam(30, 20, 26);
    ee.report();
  }
}

你也可以使用 this 來呼叫方法,所上例中的 report 可以改寫如下:

public void report() {
  System.out.println("English exam score: " + this.score() + 
         " Exam time: " + this.getMinutes() + " minutes"); 
}

由於 getMinutes 方法是 Exam 提供給它的子類別的方法。所以也可以用 super 來呼叫這個方法:

public void report() {
  System.out.println("English exam score: " + this.score() + 
         " Exam time: " + super.getMinutes() + " minutes"); 
}

super 與 this 這兩個保留字,同時用在建構子中與方法的呼叫,但是意義完全不同。用於建構子中的呼叫時,super 與 this 是用來呼叫其他的建構子;用於方法的呼叫時,super 是指呼叫自己或祖先中同名且參數一致的方法;而 this 甚至有可能呼叫到子孫類別中同名且參數一致的方法。由於 this 是在方法呼叫時才傳入的隱藏性參數,指的是實例本身,並不一定是指 this 這個字所在的類別,所以要看 this 呼叫時的實例為何,才能知道是那個方法被呼叫。

另一個寫這個程式的方式是將 report 拆開並分別放在父類別與子類別中:

class Exam {
  public int minutes;
  Exam(int m) {
    minutes = m;
  }
  public int getMinutes() {
    return minutes;
  }
  public void report() {
    System.out.println(" Exam time: " + this.getMinutes() + " minutes"); 
  }
}

class EnglishExam extends Exam {
  public int vocab, grammar, listen;
  EnglishExam(int v, int g, int l) {
    super(60);
    vocab = v;
    grammar = g;
    listen = l;
  } 
  public int score() {
    return vocab + grammar + listen;
  }
  public void report() {
    System.out.println("English exam score: " + this.score());
    super.report(); 
  }
}

class ChineseExam extends Exam{
  public int word, sentence, composition;
  ChineseExam(int w, int s, int c) {
    super(75);
    word = w;
    sentence = s;
    composition = c;
  }
  public int score() {
    return word + sentence + composition;
  }
  public void report() {
    System.out.println("Chinese exam score: " + this.score());
    super.report();
  }
} 

public class Demo {
  public static void main(String argv[]) {
    ChineseExam cc = new ChineseExam(35, 35, 18);    
    cc.report();
    EnglishExam ee = new EnglishExam(30, 20, 26);
    ee.report();
  }
}

當使用 super 呼叫一個方法(report)時,Java 會忽略定義在目前類別(EnglishExam 或 ChineseExam)的同名的 report 方法,而會從目前類別的父類別(Exam)開始往上搜尋,找到後,便呼叫那個方法。以上例而言,就是定義在 Exam 中的 report 方法。

繼承的動畫範例

到目前為止,我們已經闡述了四種呼叫 Java 方法的方式,這四種方式是:

  1. 呼叫類別方法
  2. 以實例來呼叫實例方法
  3. 以 this 來呼叫實例方法
  4. 以 super 來呼叫實例方法

在這四種方式中最單純的是呼叫類別方法,因為呼叫一個類別方法可以藉由看靜態(static)的程式碼即可確定是哪一個類別方法被呼叫到。

以 super 來呼叫實例方法也很單純,其判斷的方式是以程式碼的繼承關係來決定。例如在 C 類別中呼叫 super.m() 則可以從 C 的父類別開始依序向上(祖先們)搜尋,而搜尋到的第一個 m 即為被呼叫的方法。

「以實例來呼叫實例方法」及「以 this 來呼叫實例方法」則不能單單的看靜態的程式碼,而必須要依照程式在動態執行時所使用的實例是哪一個類別的實例才能決定。為了能明確說明,Java 決定哪一個實例方法被執行的的機制(這個機制稱為動態搜尋或 dynamic lookup),在這一節我們使用了幾個能夠動態顯現的與動畫,並將實例以一層包含一層的方式及將類別以樹狀階層圖的方式對照,來明確的解說程式執行時的實例或 this 到底是哪一個類別的實例,以決定是那個方法被執行。

以下是一個動畫的範例。這個範例說明了父類別之物件不可以使用子類別之方法,但子類別之物件可以使用父類別之方法。程式碼如下:

class Make_counter {
    int count;
    void add1() {
        count = count + 1;
    }
    void add3() {
        add1();
        add1();
        add1();
    }
    int getCount() {
        return count;
    }
}

class Make_counter2 extends Make_counter {
    void add2() {
        add1();
        add1();
    }
}

public class Ex6 {
    public static void main(String argv[]) {
        Make_counter2 c2 = new Make_counter2();
        c2.add2();
        c2.add3();
        System.out.println("count:" + c2.getCount());
        Make_counter c1 = new Make_counter();
        // c1.add2(); => error
    }
}

執行結果:

count:5

觀看執行動畫

以下的這個範例使用有層次的實例與動畫,說明this, super的特性:

class Father {
    String a = "father";
}
class Son extends Father {
    String a = "son";
    String get_null() {
        return a;
    }
    String get_this() {
        return this.a;
    }
    String get_super() {
        return super.a;
    }
}
public class Ex7 {
    public static void main(String argv[]) {
        Son s = new Son();
        System.out.println("a:" + s.get_null());
        System.out.println("this.a:" + s.get_this());
        System.out.println("super.a:" + s.get_super());
    } 
}

執行結果:

a:son
this.a:son
super.a:father

這個範例中的 s 是一個 Son 的實例,而 Son 的父類別是 Father,因此 s 實例有兩層,外層是宣告於 Son 中的實例變數與方法,而內層是宣告於 Father 的實例變數與方法。get_this 方法中的 this 即是 s;而 get_super 中的 super 指的是 s 的內層相對於 Father 的部分。

觀看執行動畫


以下這個範例說明類別間有同名而且型態也相同的方法(overwrite)的特性:

class GrandParent {
    String eyes() {
        return "blue";
    }
}
class Parent extends GrandParent {
    String eyes() {
        return "green";
    }
}
public class Ex8 {
    public static void main(String args[]) {
        GrandParent gail = new GrandParent();
        Parent sue = new Parent();
        System.out.println("當子類別擁有與父類別同方法名稱時稱為overwrite");
        System.out.println("gail.eyes():" + gail.eyes());
        System.out.println("sue.eyes():" + sue.eyes());
    }
}

執行結果:

當子類別擁有與父類別同方法名稱時稱為overwrite
gail.eyes():blue
sue.eyes():green

GrandParent 由於沒有父類別(除了 Object 以外),所以 gail 的實例只有一層;然而,sue 的實例卻有兩層,外層對應 Parent,內層對應 GrandParent。

觀看執行動畫


以下這個範例將透過宣告在數個不同類別的實例方法的呼叫,更深入的闡釋 super 與 this 的特性:

class A {
    String color() {
        return "blue";
    }
    String getColor() {
        return this.color();
    }
}
class B extends A {
    String color() {
        return "green";
    }
    String getColor1() {
        return this.color();
    }
    String getColor2() {
        return super.color();
    }
}
class C extends B {
    String color() {
        return "red";
    }
    String getColor3() {
        return this.color();
    }
    String getColor4() {
        return super.color();
    }
}
public class Ex10 {
    public static void main(String args[]) {
        A a = new A();
        B b = new B();
        C c = new C();
        System.out.println("a.color():" + a.color());
        System.out.println("a.getColor():" + a.getColor());
        System.out.println("b.color():" + b.color());
        System.out.println("b.getColor1():" + b.getColor1());
        System.out.println("b.getColor2():" + b.getColor2());
        System.out.println("c.color():" + c.color());
        System.out.println("c.getColor():" + c.getColor());
        System.out.println("c.getColor1():" + c.getColor1());
        System.out.println("c.getColor2():" + c.getColor2());
        System.out.println("c.getColor3():" + c.getColor3());
        System.out.println("c.getColor4():" + c.getColor4());
    }
}

執行結果:

a.color():blue
a.getColor():blue
b.color():green
b.getColor1():green
b.getColor2():blue
c.color():red
c.getColor():red
c.getColor1():red
c.getColor2():blue
c.getColor3():red
c.getColor4():green

這個程式最讓人訝異的是呼叫 c.getColor() 的結果是 red。因為 c.getColor 會呼叫宣告在 A 之內的 getColor;而 getColor 內的 this 指的是 c,不是 A;而 c 的 getColor 會傳回 red。

觀看執行動畫

抽象類別

還有一個寫Exam這個程式的方式是使用抽象類別(abstract class)。抽象類別的功用是為它的子類別們提供共用的變數與方法。在抽象類別中可以宣告抽象方法(abstract method),抽象方法沒有程式碼的具體定義,它只有方法的名稱及型態的宣告。在一個抽象類別中宣告一個抽象方法,便必需要在這個抽象類別的直接子類別(direct subclass)定義與這個抽象方法同名、同型態的方法,要不然會發生編譯錯誤。

如果要在 Exam 的子類別強迫定義 score 方法,並且只用 Exam 的 report 印出 score、time 及 examName 的資料。那麼這個程式可以改寫成以下這個沒有重複程式碼的版本:

abstract class Exam {
  private int minutes;
  private String examName;
  Exam(String n, int m) {
    examName = n;
    minutes = m;
  }
  public int getMinutes() {
    return minutes;
  }
  public String getExamName() {
    return examName;
  }
  public void report() {
    System.out.println(this.getExamName() + "score: " +
    this.score() + " Exam time: " + this.getMinutes() + " minutes"); 
  }
  abstract int score();
}

class EnglishExam extends Exam {
  public int vocab, grammar, listen;
  EnglishExam(int v, int g, int l, String n) {
    super(n, 60);
    vocab = v;
    grammar = g;
    listen = l;
  } 
  public int score() {
    return vocab + grammar + listen;
  }
} 

class ChineseExam extends Exam {
  public int word, sentence, composition;
  ChineseExam(int w, int s, int c, String n) {
    super(n, 75);
    word = w;
    sentence = s;
    composition = c;
  }
  public int score() {
    return word + sentence + composition;
  }
} 

public class Demo {
  public static void main(String argv[]) {
    Exam ex;
    ex = new ChineseExam(35, 35, 18, "Chinese exam");    
    ex.report();
    ex = new EnglishExam(30, 20, 26, "English exam");
    ex.report();
  }
}

在 Exam 類別內的 report 方法所用到的 this 可以有兩種可能的實例與之對應:ChineseExam 的實例與 EnglishExam 的實例。而呼叫在 main 中的 ex.report() 會繼續呼叫到 this.score(),這時實際上被呼叫的 score 方法有兩種可能:

  1. 呼叫到定義在 ChineseExam 中的 score,如果 ex 的值是一個 ChineseExam 的實例。
  2. 呼叫到定義在 EnglishExam 中的 score,如果 ex 的值是一個 EnglishExam 的實例。

因此 this 的意義不是指出現 this 這個字的類別(Exam),而是指用於呼叫方法(score)的實例,這個實例就是 this。以上例而言是 ex,而 ex 的值可以是一個 ChineseExam 的實例,也可以是一個 EnglishExam 的實例,而 ChineseExam 與 EnglishExam 都是 Exam 的子類別,所以 this.score() 實際上可能呼叫到自己的子孫所定義的方法

一個抽象類別的主要功用是為它的子類別提供共用的變數與方法。抽象類別內可以宣告抽象方法。但是抽象類別不能產生實例,所以以下的程式碼會產生編譯錯誤:

Exam ex = new Exam();

抽象類別的抽象方法強制其直接子類別必須定義同名、同型態的實體方法,而 Java 的編譯器可以檢查這個規定是否被達成,因此可以減輕程式設計師自行檢查某個類別是否符合設計需求的負擔。

雖然抽象類別不能產生實例,但是卻可以用於宣告實例變數的型態。例如:

public class Demo {
  public static void main(String argv[]) {
    Exam ex;
    ex = new ChineseExam(35, 35, 18, "Chinese exam");    
    ex.report();
    ex = new EnglishExam(30, 20, 26, "English exam");
    ex.report();
  }
}

ex 這個變數的型態是 Exam,因為 ChineseExam 與 EnglishExam 都是一種 Exam,所以 ex 可以儲存 ChineseExam 的實例,也可以儲存 EnglishExam 的實例,因為 ChineseExam 與 EnglishExam 不但是 Exam 的子類別,也是 Exam 的子型態(subtype)。這便是 ChineseExam 與 Exam 有 is-a 的關係,也就是一個 ChineseExam 是一個(is-a)Exam;而一個 EnglishExam 也是一個 Exam。

但是型態被宣告為 Exam 的變數,只能用於呼叫宣告在 Exam 內的方法。假設 ChineseExam 內有一個 getWord 的方法,但是在 Exam 中沒有這個方法,那麼 ex.getWord() 將會產生編譯錯誤,因為 ex 實例的型態沒有這個方法。

如果確實有需要呼叫 getWord 方法,則可使用轉型的方式進行呼叫。例如:

public class Demo {
  public static void main(String argv[]) {
    Exam ex;
    ex = new ChineseExam(35, 35, 18, "Chinese exam");    
    System.out.println("Word score: " + (ChineseExam)ex.getWord();
  }
}

抽象類別有一個特性,它不可能是在一個類別繼承結構的最下層。如果需要,程式設計師可以宣告一個最下層的類別為 final,一個 final 的類別不能被其他類別繼承。

物件導向程式的設計原則

如同前面的範例所展示的,一個問題可能有好幾種不同的解法。到底哪一種方法比較好?在什麼狀況用哪種解法呢?以下是設計物件導向程式的幾個參考原則:

  1. 類別的結構與實際一致。
  2. 高層次的類別歸納一般化的屬性(general properties),例如:Exam或生物;而低層次類別的屬性則比較特殊化(specialize),例如:EnglishExam或人類。
  3. 沒有重複的程式碼。
  4. 直接存取常用的資訊,避免反覆的透過計算得到。
  5. 將其他類別不需要用的或不需要知道的變數與方法以 private 或 protected 隱藏起來。
  6. is-a 與 has-a 的適當設計:以前例而言,ChineseExam 與 EnglishExam 都是一種(is-a)Exam。所以這時將它們設計成父類別與子類別的關係便很正確。但是,一個考生(a Student)的資料卻不能因為所有的 Exam 都有考生,而將 Student 設計成 EnglishExam 與 ChineseExam 的父類別。然而,將 Student 設計成一個單一的類別,並在 Exam 中宣告一個能夠存放考生實例的實例變數,卻很恰當。因為一份考卷 has-a 考生。這種在類別中宣告實例變數,以儲存其他類別所產生的實例,稱為(has-a)的關係。

抽象類別的動畫範例

以下的範例將使用「寵物(Pet)」類別及「狗(Dog)」及「貓(Cat)」兩個子類別。狗及貓都擁有「聲音(sound)」這個方法,但狗跟貓的叫聲卻是不同的,此時我們可以利用抽象類別來實作這個例子。

abstract class Pet {
    abstract String sound();
}
class Dog extends Pet {
    String sound() {
        return "汪汪";
    }
}
class Cat extends Pet {
    String sound() {
        return "喵喵";
    }
}
public class Ex11 {
    public static void main(String args[]) {
        Dog d = new Dog();
        Cat c = new Cat();
        System.out.println("d.sound():" + d.sound());
        System.out.println("c.sound():" + c.sound());
    }
}

執行結果:

d.sound():汪汪
c.sound():喵喵

上例中 d 與 c 兩個變數若宣告成 Pet 型態,則答案仍然會一樣。

觀看執行動畫


以下這個範例是一個整合了 abstract class, super, this, array 的應用。這個範例的特色是宣告了 Area 這個 abstract class 及 getArea 這個抽象方法,讓 Square 及 Triangle 來繼承,而 whichBig 這個方法則可以比較任何兩個 Area 實例(Square 及 Triangle 的實例都是 Area 的實例),何者的面積較大。此外,在 main 中的 a 陣列,也可以存放任何的 Area 的實例,因此 a 可以存放 Square 的實例,也可以存放 Triangle 的實例:

abstract class Area {
    int h, w;
    Area(int a, int b) {
        h = a;
        w = b;
    }
    abstract int getArea();
    boolean whichBig(Area p) {
        return (this.getArea() > p.getArea());
    }
}
class Rectangle extends Area {
    Rectangle(int h, int w) {
        super(h, w);
    }
    int getArea() {
        return h * w;
    }
}
class Triangle extends Area {
    Triangle(int h, int w) {
        super(h, w);
    }
    int getArea() {
        return (h * w) / 2;
    }
}
public class Ex12 {
    public static void main(String args[]) {
        int total = 0;
        Area[] a = { new Rectangle(6, 6), new Triangle(10, 10) };
        System.out.println("Is a[0] bigger than a[1]?" + a[0].whichBig(a[1]));
        for (int i = 0; i < a.length; i++) {
            total = total + a[i].getArea();
        }
        System.out.println("total:" + total);
    }
}

執行結果:

Is a[0] bigger than a[1]?false
total:86

觀看執行動畫及詳細解說

介面

前一個單元,介紹了以抽象類別內的抽象方法,來將需求強制加在抽象類別的直接子類別上。這種功能提供了程式設計師必需先想、先設計,然後才開始寫程式的好習慣。這個功能還提供了程式設計師以下的可能:

  1. 將額外的註解放入抽象方法中,讓程式碼有更容易瞭解;
  2. 可以將一個類別交給其他人來寫;
  3. 如果某個程式設計師忘了按照需求定義抽象方法所強制的名稱與類別,編譯器可以自動產生錯誤信息。

除了抽象類別能宣告抽象方法之外,Java 還提供了介面(interface)來宣告抽象方法。不同於抽象類別:

  1. 介面只提供抽象方法或常數宣告,而不能定義變數或一般的方法。
  2. 一個子類別只能繼承一個父類別,但卻可以實作(implements)許多個介面。因此,介面模擬了多重繼承(multiple inheritance)的功能,但是卻可以避免一個類別繼承了多個父類別,但卻可能同時繼承了多個同名的變數或方法而造成混淆。

假設在設計 Exam 這個程式時,你已經想過要提供一個 score 方法給 ChineseExam 與 EnglishExam。這時你可以先將 ScoreInterface 設計如下:

public interface ScoreInterface {
  public abstract int score ();
}

而 ChineseExam 與 EnglishExam 則 implements 這個 interface:

class ChineseExam extends Exam implements ScoreInterface {
  ...
}
class EnglishExam extends Exam implements ScoreInterface {
  ...
}

這時 ChineseExam 與 EnglishExam 便必須提供 score 方法,而且要有同樣的輸出入參數的型態,要不然 Java 的編譯器便會產生錯誤信息。因此使用 interface 的一大好處是讓 Java 的編譯器,也可以幫助維持程式的一致性;而不用透過人工,這個容易出錯的方式來維持。

Interface 也可以成為一個變數的型態,而所宣告的變數可以用來呼叫這個 interface 內宣告的方法。例如:

...

public class Demo {
  public static void main(String argv[]) {
    ScoreInterface ex;
    ex = new ChineseExam(35, 35, 18, "Chinese exam");    
    ex.score();
    ex = new EnglishExam(30, 20, 26, "English exam");
    ex.score();
  }
}

然而,如果上例的 ChineseExam 與 EnglishExam 類別中定義了 report 方法,而 ScoreInterface 中卻沒有宣告。這時下列的程式碼便會在編譯時產生型態錯誤:

...

public class Demo {
  public static void main(String argv[]) {
    ScoreInterface ex;
    ex = new ChineseExam(35, 35, 18, "Chinese exam");    
    ex.report();
    ex = new EnglishExam(30, 20, 26, "English exam");
    ex.report();
  }
}

改正的方式是透過轉型:

...

public class Demo {
  public static void main(String argv[]) {
    ScoreInterface ex;
    ex = new ChineseExam(35, 35, 18, "Chinese exam");    
    ((ChineseExam)ex).report();
    ex = new EnglishExam(30, 20, 26, "English exam");
    ((EnglishExam)ex).report();
  }
}

泛型與 Collection

泛型(generics)是一種可以讓變數或方法的型態,經由「型態參數」的使用,讓這些變數或方法的型態,可以更廣泛而又有彈性的型態宣告機制。泛型可以強化編譯時間的型態檢查(compile-time type checking),也可以讓程式碼更有彈性與再利用性。

舉例而言,如果我們需要用陣列來寫一個儲存實例的類別 ── Store,而 Store 有 put、elementAt 及 printAll 三個實例方法。那麼這個類別可以這麼寫:

public class Store {
  Object[] myStore;
  int here = 0;   // 追蹤目前的儲存位置
  Store (int n) {
    myStore = new Object[n];   // 能儲存 n 個實例
  }
  void put(Obejct in) {
    if (here < myStore.length - 1) {
       myStore[here] = in;
       here++; 
    } else {
       // 錯誤:超過容量
    }
  }
  Object elementAt(int i) {
    if ((0 <= i) && (i < myStore.length)) {
       return myStore[i];
    } else
       // 錯誤:超出範圍
    }
  }
  void printAll() {
    // 印出所有的儲存在 myStore 中的物件
  }
}

為了讓 Store 能夠存放不同類型的實例,myStore 必須宣告成 Object,因為所有實例的型態也都是 Object。這樣的寫法可以讓一個 Store,能夠存放不同類別的實例。例如:

Store s = new Store(10);
s.put(new Integer(2));
s.put("a String");

然而,從 s 取出實例時便需要轉型,以回復原來的型態:

Integer integer = (Integer) s.elementAt(0);
String string = (String) s.elementAt(1);

當我們需要呼叫儲存在 s 內的實例方法時,也需要轉型。這樣便相當的不方便。例如:

Store s = new Store(10);
s.put(new Integer(2));
s.put("a String");
for (int counter = 0; counter < 2; counter++) {
    // 如果要呼叫 s.elementAt(counter) 的某個方法,便一定要轉型
    // 因為 s.elementAt(counter) 的型態是 Object
    if (s.elementAt(counter) instanceOf String) { 
    // instanceof 這個保留字可以用來測試某個實例是否是某個型態
       ... (String)s.elementAt(counter).length() ...
    } else if (s.elementAt(counter) instanceOf Integer) {
       ... (Integer)s.elementAt(conter).toString() ...
    }
}

然而,在Java 5以後,程式設計師可以使用泛型以解決上述的問題。例如:

// 使用 ArrayList<T> 這個 generic class 來產生一個專門存放 
// Exam 的 ArrayList:new ArrayList<Exam>()。T 是一個型態參數,
// 實際使用時可以被其他的 reference type 代替,例如:Exam。
// 如果不能夠宣告存放在一個ArrayList中的物件是Exam型態的物件,
// 那麼解決的方式就是存放Object型態的物件,但是這樣在取用時,
// 便相當的不方便,而泛型的使用解決了這個問題。

List<Exam> e = new ArrayList<Exam>();

// 使用 e.add(...) 存入實例
 
e.add(new EnglishExam(9, 8, 7));
e.add(new ChineseExam(8, 9, 6));

// 使用 e.get(i) 傳回存放在位置 i 的實例

Exam anExamInstance = e.get(0);

// 存入更多的實例...

// 這種 for-loop 每 loop 一次便傳回一個 e 中的實例
// 被傳回的實例存放在 anExam 這個變數內
for (Exam anExam : e) { 
// anExam的型態是Exam,因此不需要轉型
  System.out.println(anExam.score());
}

至於為何要使用 new Integer(...),來製造一個 integer 的實例呢?其原因是:Java 的程式庫中有許多類別,為了簡化這些類別的撰寫,讓它們不用考慮 primitive type 與 reference type 的差異:例如,上例的 Store 便可以讓所有的 reference type(所有的 Object),都能存放,但卻不能存放 primitive type 的值。因此,Java 使用 boxing 的方式將 primitive 轉型為 reference 型態,例如:Integer(3) 就是將 primitive type 的 3 boxing 成 reference type 的 Integer(3) 。Unboxing 則可以將之還原成 primitive 型態。自從 Java 5.0 版之後,boxing 及 unboxing 都是自動的。然而,程式設計師也可以撰寫執行 boxing 及 unboxing 的程式碼,例如:Integer(i) 可以將 int i 轉型成 Integer,而 (int)o 則可以將型態為 Integer 的 o 轉型成 int。

Collection 與 Map

為了讓程式設計師能夠不必自行設計 Store 這種用來儲存實例的資料結構。 Java 在 java.util 這個套件中,提供了 Collection 與 Map 的 API (Application Programming Interface) 供程式設計師使用。Collection

一個經由 generic interface Collection<T> 所產生的 collection,可以用來儲存與處理許多型態是 T 的實例。Collection<T> 的架構有三個主要的層次:第一層包括:Collection<T>, List<T>, Set<T>, SortedSet<T> 幾個 interfaces;第二層包括:AbstractCollection<T>, AbstractList<T>, AbstractSequentialList<T>, AbstractSet<T> 幾個 abstract classes,第三層包括:ArrayList<T>, LinkedList<T>, HashSet<T>, LinkedHashSet<T>, TreeSet<T> 幾個類別。這份講義將以 ArrayList<T> 當作例子,說明 Collection API 的使用方式。

一個經由 generic interface Map<K, V> 所產生的 map,能夠對應由型態是 K 的 keys 與型態是 V 的 values,而一個 key 最多只能對應一個 value。Map<K, V>的架構也有三個層次:第一層包括:Map<K, V> 及 SortedMap<K, V> 兩個 interface;第二層包括:AbstractedMap<K, V> 這個 abstracted class;第三層則包括:IdentityHashMap<K, V>, HashMap<K, V>, LinkedHashMap<K, V> 及 TreeMap<K, V>幾個類別。這份講義將以 HashMap<K, V> 當作例子,說明 Map API 的使用方式。

Generic List

List<T> 是 Java 的類別館 java.util package 所提供的一個 interface。實作 List<T> 的類別能夠將所存放的元素以當初存入的次序依序取出。ArrayList<T> 是一個實作 List<T> 的類別;LinkedList<T> 是另一個實作 List<T> 的類別。以下是使用 ArrayList<T> 的範例(T 是型態參數,使用時能夠以真實的型態代替之):

import java.util.List;
...
 
// 宣告 list 是一個可以儲存 String 的 ArrayList
 
List<String> list = new ArrayList<String>();
 
// 用 list.add(...) 存入實例
 
String string1 = "a string";
list.add(string1);
...
 
// 用 list.get(...) 取出實例
String string2 = list.get(0);
 
// 用 list.iterator() 及 while-loop 依存入的次序,取出及處理每一個實例
 
Iterator<String> iterator = list.iterator();
 
while (iterator.hasNext()){
String aString = iterator.next();
...
}
 
// 用 for-loop 依存入的次序,取出及處理每一個實例
 
for (String aString : list) {
// list 內可以存放許多的 string
// 針對 list 內的每一個 string (aString) 均執行以下的程式碼:
System.out.println(aString);
...
}

Generic Map

Map<K, V> 是 Java 的類別館 java.util package 所提供的一個 interface。實作 Map<K, V> 的類別,能夠將 key 與 value 一對一對的存入,而一個 key 只能與一個 value 對應。HashMap<K, V> 是一個實作 Map<K, V> 的類別。以下是使用 HaspMap<K, V> 的範例(K, V 是型態參數,使用時能夠以真實的型態代替之):

import java.util.Map;
...
// map 是一個對應 Integer 與 String 的 HashMap
 
Map<Integer, String> map = new HashMap<Integer, String>();
 
// 用 map.put(..., ...) 存入一對 key 與 value
 
Integer key1 = new Integer(123);
String value1 = "value abc";
map.put(key1, value1);
...
 
// 用 map.get(key1) 取出與 key1 對應的 value
 
String value1_1 = map.get(key1);
 
// 或者直接存入 123 而不是 Integer(123)
// 而 auto-boxing 會將 123 轉換成 Integer(123)
 
Integer key1 = 123;
String value1 = "value abc";
map.put(key1, value1);
 
// 使用 map.get(key, value) 存入,接著以 map.get(key) 取出 value
 
map.put(123, value1);
String value1_1 = map.get(123);
 
// 用 map.keySet().iterator() 及 while-loop 取出每一個 key
// 接著用 map.get(...) 取出每一個 value
Iterator<Integer> keyIterator = map.keySet().iterator();
while(keyIterator.hasNext()){
Integer aKey = iterator.next();
String aValue = map.get(aKey);
}
 
// 用 map.values().iterator 及 while-loop 取出每一個 value
Iterator<String> valueIterator = map.values().iterator();
while(valueIterator.hasNext()){
String aString = valueIterator.next();
}
 
// 用 for-loop 取出每一個 key 與 value
for(Integer aKey : map.keySet()) {
String aValue = map.get(aKey);
System.out.println("" + aKey + ":" + aValue);
}
 
// 用 for-loop 列印每一個 value
for(String aValue : map.values()) {
System.out.println(aValue);
}

Generic的應用實例

堆疊(stack)是一種「後進先出」(last in first out)的資料結構。可以將一個 堆疊想像成是一個能裝盤子的桶狀物,盤子一個一個的堆上去;取出時則先取出最後放入,也是最上面的盤子。堆疊有兩個常用的方法:push 將一筆資料放入堆疊,pop 將最後放入的資料取出。

以下這個範例應用 LinkedList<T> 實作一個 UnboundedStack 類別,UnboundedStack 沒有設定能儲存的資料量有多少。UnboundedStack 繼承 Stack 這個抽象類別。除了 push 與 pop 兩個抽象方法之外,Stack 還有一個 top 方法,這個方法只是將 stack 上方的值傳回,而不實際取出;因此可以將最上方的值先 pop 出來,得到其值後,再 push 回去,以保持 stack 原來的狀態。

UnboundedStack 使用 LinkedList<T> 的 addFirst 及 removeFirst 兩個方法。addFirst 將一個實例加入一個 list 的最前方;removeFirst 將一個 list 最前方的實例取出。

UnboundedStack 的限制是:只能將 String 類別的實例,放入堆疊中。此外,當一個堆疊中沒有資料時,pop 傳回 null。null 是一個保留字,其意義是「沒有實例」。null 也是 reference type 變數的初始值(default value):

import java.util.*;
 
abstract class Stack {
abstract String pop();
abstract void push(String value);
 
String top(){
String value;
 
value=this.pop();
this.push(value);
return value;
}
}
class UnboundedStack extends Stack{
LinkedList<String> stack = new LinkedList<String>();
 
boolean empty(){
return (stack.size() == 0) ? true : false;
}
void push(String arg){
stack.addFirst(arg);
}
String pop(){
if (!(this.empty())) {
String topValue = stack.getFirst();
stack.removeFirst();
return topValue;
} else {
return null;
}
}
}
 
public class UStackMain {
public static void main(String args[]) {
UnboundedStack s = new UnboundedStack();
s.push("abc");
s.push("def");
s.push("ghi");
System.out.println(s.top()); // 印出 ghi
System.out.println(s.pop()); // 印出 ghi
System.out.println(s.pop()); // 印出 def
System.out.println(s.pop()); // 印出 abc
System.out.println(s.pop()); // 印出 null
}
}

以下這個範例,更進一步的應用泛型,使得一個堆疊能夠存放不同型態的實例:

import java.util.*;
abstract class Stack<T> {
abstract T pop();
abstract void push(T value);
T top() {
T value;
value=this.pop();
this.push(value);
return value;
}
}
 
class UnboundedStack<T> extends Stack<T> {
LinkedList stack = new LinkedList<T>();
 
boolean empty() {
return (stack.size() == 0) ? true : false;
}
void push(T value) {
stack.addFirst(value);
}
T pop() {
if (!(this.empty())) {
T topValue = (T)stack.getFirst();
stack.removeFirst();
return topValue;
} else {
return null;
}
}
}
 
public class GUStack {
public static void main(String args[]) {
UnboundedStack s = new UnboundedStack();
s.push("abc"); s.push(2); s.push("ghi");
System.out.println(s.top()); // 印出 ghi
System.out.println(s.pop()); // 印出 ghi
System.out.println(s.pop()); // 印出 2
System.out.println(s.pop()); // 印出 abc
System.out.println(s.pop()); // 印出 null
}
}

例外狀態的處理

一個程式在執行時,可能會有許多不正常或例外的狀態需要處理。例如:網路突然斷線、插入了錯誤的光碟、使用 0 為除數或陣列的 index 值超出了範圍等等。為了讓處理這些例外狀況的程式碼與主程式不混在一起,Java 使用 了try、catch 將主程式與處理例外狀況的程式碼分開。

try { 
... // 程式的主要邏輯在此,
... // 執行時可能會產生IO Exception
}catch (IOException e) {
// 處理IO Exception的程式碼,例如:
e.printStackTrace();
System.out.println(e);
...
}

為了能區分不同種類的 Exception,Java 也將 Exception 以繼承的方式分類。例如以下的三個 Exception 類別:

Exception
   |
   V
IOException
   |
   V
FileNotFoundException

其中 Exception 涵蓋所有的 Exception,而 FileNotFoundException 則指定 file not found 這種狀況。

try 之後,可以 catch 許多的例外。例如:

try {   
...
...
} catch (FileNotFoundException e) {
System.out.println("檔案不存在");
} catch (IOException e) {
System.out.println("發生輸出入錯誤");
}

然而,catch 的出現的次序必須與各個 exception 類別的繼承次序配合。繼承層次低的類別要先出現,繼承層次高的後出現。例如:以下這個錯誤的寫法:

try {   
...
...
} catch (IOException e) {
System.out.println("發生輸出入錯誤");
} catch (FileNotFoundException e) {
System.out.println("檔案不存在");
}

System.out.println("檔案不存在") 便不可能被執行到,因為 FileNotFoundException 也是 IOException 的一種。所以會先被IOException 抓到。

以下是一個讀取輸入資料檔的範例:

import java.io.*; 
import java.util.*;
public class ReadFile {
public static void main(String args[]) {
try {
FileInputStream stream = new FileInputStream("xx.part");
InputStreamReader reader = new InputStreamReader(stream);
BufferedReader breader = new BufferedReader(reader);
String record;
String nameString;
int grade;
while ((record = breader.readLine()) != null) {
StringTokenizer tokens = new StringTokenizer(record);
nameString = tokens.nextToken();
grade = Integer.parseInt(tokens.nextToken());
System.out.println(nameString + " " + grade);
}
stream.close();
}
catch (FileNotFoundException e) {
System.out.println(e);
} catch (IOException e) {
System.out.println(e);
}
}
}

由以上的範例可以看出,try 與 catch 之間是程式的正常邏輯。當在讀取檔案時發生例外時,程式的執行則自動的跳到 catch 中執行。至於是跳到那個 catch,則要看是 FileNotFoundException 還是其他的 IOException 而定。

以下則是一個將 while 放在 try、catch 之外並且利用一個 tryAgain 的變數讓一個程式可以不斷的請使用者放入磁片,直到放正確才繼續處理的程式。

import java.io.*;
import java.util.*;
public class ReadData {
public static void readFile(String fileName) {
boolean tryAgain = true;
while (tryAgain) {
try {
tryAgain = false;
//
// 讀檔成功之後的程式碼
//
}
catch (FileNotFoundException e) {
tryAgain = true;
}
catch (IOException e) {
System.out.println(e);
}
}
}
}

其他常見的 Exception 類別還有:ArrayIndexOutOfBoundsException, ArithmeticException等。當 Exception 發生時,也可以使用 System.exit(0) 結束程式的執行。例如:

catch (IOException e) { 
System.exit(0);
}

除了 try, catch 外,finaly 子句是用來寫在例外或沒有例外發生時都需要執行的程式碼。其語法如下:

try { 
...
}
catch (exception-class name e) {
...
}
finally {
...
}

除了使用 Java 內建的數個 Exception 類別。程式設計師也可以依據程式的需要,自行定義 Exception 的子類別:

public class StrangeDataException extends Exception { 
}

以上的 StrangeDataException 是一個 Exception 的子類別。定義好之後,便可以在 try 子句內使用 new 與 throw 來產生一個 exception:

try {
...
if (...) { // someting-wrong
throw (new StrangeDataException()) ;
} else {
... // 正常處理
}
}
catch (StrangeDataException e) {
// e 是一個 StrangeDataException 的物件
//
// 處理 StrangeDataException 發生時的程式碼
//
}

以下這個範例則是在產生 StrangeDataException 物件時透過有參數的 constructor 將例外處理時所需要的資料傳入的範例:

...
public class StrangeDataException extends Exception {
某個類別 obj;
StrangeDataException(某個類別 o, ...) {
obj = o;
}
handleStrangeData(...){
...
}
}
 
...
 
try {
...
if (...) { // someting-wrong
throw (new StrangeDataException(objx, ...)) ;
} else {
... // 正常處理
}
}
catch (StrangeDataException e) {
// e 是一個StrangeDataException的物件
...
// 呼叫發生 StrangeDataException 的處理方法
e.handleStrangeData(...)
...
}
Personal tools