第九章
介面和多型
9.1 開發可重複使用的解決方案
DataSet類別用來計算一組數值的平均和最大值。
BankAccount 的 DataSet
如果要在一組銀行帳戶中,找出最大的存款金額最大的, 則類別 DataSet 可修改為
public class DataSet // modified for BankAccount objects
{
. . .
public void add(BankAccount x)
{
sum = sum + x.getBalance();
if (count == 0
|| maximum.getBalance() < x.getBalance())
maximum = x;
count++;
}
public BankAccount getMaximum()
{
return maximum;
}
private double sum;
private BankAccount maximum;
private int count;
}
Coin 的 DataSet
同樣要計算一把硬幣中找出最大的硬幣值, 則類別 DataSet 修改為
public class DataSet // modified for Coin objects
{
. . .
public void add(Coin x)
{
sum = sum + x.getValue();
if (count == 0
|| maximum.getValue() < x.getValue())
maximum = x;
count++;
}
public Coin getMaximum()
{
return maximum;
}
private double sum;
private Coin maximum;
private int count;
}
Measurable 介面
- 以上三個類別, 分析資料的架構都一樣, 只是取得資料的方法不同。
- 假設這三個類別都用相同的方法名稱 getMeasure,
- 則這三個類別可用一個可重複使用的 DataSet 類別取代, 其 add 方法如下:
public void add([double | Coin | BankAccount] x) // Error
{
sum = sum + x.getMeasure();
if (count == 0
|| maximum.getMeasure() < x.getMeasure())
maximum = x;
}
- 參數 x 的型式可能是double, Coin 或 BankAccount。
x 應屬於具有 getMeasure 方法的類別(假設稱為 Measurable)。 interface型式宜告就是解決這種困境。
- 該介面可設計如下:
public interface Measurable
{
double getMeasure();
}
介面與類別
- 介面中所有方法都是 abstract -- 只有名稱,參數,和回值型式, 沒有實作部分。
- 介面中所有方法都自動是 public
- 介面沒有 instance fields
Measurable 物件的通用 DataSet
public class DataSet // modified for Coin objects
{
. . .
public void add(Measurable x)
{
sum = sum + x.getMeasure();
if (count == 0
|| maximum.getMeasure() < x.getMeasure())
maximum = x;
count++;
}
public Measurable getMaximum()
{
return maximum;
}
private double sum;
private Measurable maximum;
private int count;
}
實現介面
修改 BankAccount 和 Coin 類別成為 Measurable
class BankAccount implements Measurable
{
public double getMeasure()
{
return balance;
}
additional methods and fields
}
class Coin implements Measurable
{
public double getMeasure()
{
return value;
}
additional methods and fields
}
File DataSetTest.java
/**
This program tests the DataSet class.
*/
public class DataSetTest
{
public static void main(String[] args)
{
DataSet bankData = new DataSet();
bankData.add(new BankAccount(0));
bankData.add(new BankAccount(10000));
bankData.add(new BankAccount(2000));
System.out.println("Average balance = "
+ bankData.getAverage());
Measurable max = bankData.getMaximum();
System.out.println("Highest balance = "
+ max.getMeasure());
DataSet coinData = new DataSet();
coinData.add(new Coin(0.25, "quarter"));
coinData.add(new Coin(0.1, "dime"));
coinData.add(new Coin(0.05, "nickel"));
System.out.println("Average coin value = "
+ coinData.getAverage());
max = coinData.getMaximum();
System.out.println("Highest coin value = "
+ max.getMeasure());
}
}
DataSet 和相關類別的 UML 圖
- 注意 DataSet 和 BankAccount, Coin 脫鉤
Syntax 9.1: 介面的設計
|
public interface InterfaceName
{
method signatures
}
|
例題:
|
public interface Measurable
{
double getMeasure();
} |
用途:
To define an interface and its method signatures. The methods
are automatically public. |
Syntax 9.2: 介面的實現
|
public class ClassName
implements InterfaceName, InterfaceName, ...
{
methods
instance variables
}
|
例題:
|
public class BankAccount implements Measurable
{
// other BankAccount methods
public double getMeasure()
{
// method implementation
}
}
|
用途:
To define a new class that implements the methods of an
interface |
9.2 型式的轉換
- 可以從類別型式轉換成所實現的介面型式:
BankAccount account = new BankAccount(10000);
Measurable x = account; // OK
- 同一介面型式的變數可以容納指到 Coin 的參考
x = new Coin(0.1, "dime"); // OK
- 無相關的型式不能互轉
x = new Rectangle(5, 10, 20, 30); // ERROR
Casts
- 添加 coin 物件到 DataSet
DataSet coinData = new DataSet();
coinData.add(new Coin(0.25, "quarter"));
coinData.add(new Coin(0.1, "dime"));
...
- 用 getMaximum() 方法取得最大的硬幣:
Measurable max = coinData.getMaximum();
- 如何取得知 max 是那一種硬幣? 它並不是 Coin 的型式
String name = max.getName(); // ERROR
- 我們知道它是硬幣, 但是 compiler 並不知道. 這就要用到 cast:
Coin maxCoin = (Coin)max;
String name = maxCoin.getName();
- 假如錯了, max 並不是硬幣, 則程式丟出例外後終止。
instanceof 運算子
Syntax 9.3: instanceof 運算子
| object instanceof ClassName |
例題: | if (x instanceof Coin)
{
Coin c = (Coin)x;
} |
用途:To return true if the object is an instance of ClassName (or one of its subclasses), false otherwise |
9.3 多型
- 介面變數容納有實現該介面的類別中物件的參考
Measurable x;
x = new BankAccount(10000);
x = new Coin(0.1, "dime");
- 絕不能建構一介面!
x = new Measurable(); // ERROR
- 可以呼用介面中的方法:
double m = x.getMeasure();
- 到底呼用那一個方法? BankAccount 的, 還是 Coin 的?
- 這就要看實際的物件是屬於那一類別:
- 假如 x 指的是銀行帳戶, 則呼用 BankAccount.getMeasure
- 假如 x 指的是硬幣, 則呼用 Coin.getMeasure
- 多型(polymorphism): 物件的型式決定所呼用的方法
- 呼用那一個方法是在跑程式的時候決定, 所以稱做 late binding。
- 這和覆載(overloading)不同。 覆載是在編譯程式的時候決定。
9.4 使用策略介面以改進可重複使用性
使用策略介面
- Measurable 介面的缺點:
- 必須修改類別, 添加介面和方法
- 系統預先定義的類別, 如 Rectangle 不能用此介面。
- 只能用一種方式量測一類別
- 這是因為物件要做量測的工作
- 補救: 將物件所要的量測工作交給另一介面的 measure 方法:
public interface Measurer
{
double measure(Object anObject);
}
- Object 是所有類別的 "最小公倍數"
- 在 add 方法中由 measurer (並不是所添加的物件) 做量測的工作,
public void add(Object x)
{
sum = sum + measurer.measure(x);
if (count == 0
|| measurer.measure(maximum) < measurer.measure(x))
maximum = x;
count++;
}
完整的程式在 DataSet.java。
- Measurer 類別的物件可用以量測任一物件。 例如,
量測長方形面積
class RectangleMeasurer implements Measurer
{
public double measure(Object anObject)
{
Rectangle aRectangle = (Rectangle)anObject;
double area = aRectangle.getWidth() * aRectangle.getHeight();
return area;
}
}
注意:
- measure 方法的參數必須是 Object 型式。
- 必須將 Object cast 成 Rectangle。
- 將量測者 m 代入 DataSet 建構式:
Measurer m = new RectangleMeasurer();
DataSet data = new DataSet(m);
Measurer 介面和相關類別的 UML 圖
- 注意 Rectangle 類別和 Measurer 介面脫鉤
內部類別
- RectangleMeasurer 是個非常平凡的類別, 沒有狀態。
- 平凡類別可以在方法中定義, 稱為內部類別(inner class)。 例如,
public static void main(String[] args)
{
class RectangleMeasurer implements Measurer
{
. . .
}
Measurer m = new RectangleMeasurer();
. . .
// RectangleMeasurer class not used beyond this point
}
File DataSet.java
/**
Computes the average of a set of data values.
*/
public class DataSet
{
/**
Constructs an empty data set with a given measurer
@param aMeasurer the measurer that is used to measure data values
*/
public DataSet(Measurer aMeasurer)
{
sum = 0;
count = 0;
maximum = null;
measurer = aMeasurer;
}
/**
Adds a data value to the data set
@param x a data value
*/
public void add(Object x)
{
sum = sum + measurer.measure(x);
if (count == 0
|| measurer.measure(maximum) < measurer.measure(x))
maximum = x;
count++;
}
/**
Gets the average of the added data.
@return the average or 0 if no data has been added
*/
public double getAverage()
{
if (count == 0) return 0;
else return sum / count;
}
/**
Gets the largest of the added data.
@return the maximum or 0 if no data has been added
*/
public Object getMaximum()
{
return maximum;
}
private double sum;
private Object maximum;
private int count;
private Measurer measurer;
}
File DataSetTest.java
import java.awt.Rectangle;
/**
This program demonstrates the use of a Measurer.
*/
public class DataSetTest
{
public static void main(String[] args)
{
class RectangleMeasurer implements Measurer
{
public double measure(Object anObject)
{
Rectangle aRectangle = (Rectangle)anObject;
double area
= aRectangle.getWidth() * aRectangle.getHeight();
return area;
}
}
Measurer m = new RectangleMeasurer();
DataSet data = new DataSet(m);
data.add(new Rectangle(5, 10, 20, 30));
data.add(new Rectangle(10, 20, 30, 40));
data.add(new Rectangle(20, 30, 5, 10));
System.out.println("Average area = " + data.getAverage());
Rectangle max = (Rectangle)data.getMaximum();
System.out.println("Maximum area = " + max);
}
}
File Measurer.java
/**
Describes any class whose objects can measure other objects.
*/
public interface Measurer
{
/**
Computes the measure of an object.
@param anObject the object to be measured
@return the measure
*/
double measure(Object anObject);
}
Syntax 9.4: Inner Classes
| 在方法中宣告 class OuterClassName { method signature { . . . class InnerClassName { nethods fields } . . . } . . . }
在類別中宣告 class OuterClassName { nethods fields accessSpecifier class InnerClassName { nethods fields } . . . }
|
例題: |
public class Test
{
public static void main(String[] args)
{
class RectangleMeasurer implements Measurer
{
. . .
}
}
}
|
用途:
To define an inner class whose methods have access to the same variables and methods as the outer class methods |
9.5 處理 Timer 事件
- javax.swing.Timer 產生等距間隔的定時器事件
- 事件發生時, 定時器通知聽候者 ActionListener
public interface ActionListener
{
void actionPerformed(ActionEvent event);
}
- 實現 ActionListener 介面
class MyListener implements ActionListener
{
void actionPerformed(ActionEvent event);
{
// this action will be executed at each timer event
place listener action here
}
}
- 將 ActionListener 物件加到定時器
MyListener listener = new MyListener();
Timer t = new Timer(interval, listener);
t.start();
例題: Countdown
- 程式列印出:
10
9
. . .
2
1
0
Liftoff!
- 各次列印相隔一秒
File TimerTest.java
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import javax.swing.Timer;
/**
This program tests the Timer class.
*/
public class TimerTest
{
public static void main(String[] args)
{
class CountDown implements ActionListener
{
public CountDown(int initialCount)
{
count = initialCount;
}
public void actionPerformed(ActionEvent event)
{
if (count >= 0)
System.out.println(count);
if (count == 0)
System.out.println("Liftoff!");
count--;
}
private int count;
}
CountDown listener = new CountDown(10);
final int DELAY = 1000; // milliseconds between timer ticks
Timer t = new Timer(DELAY, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit?");
System.exit(0);
}
}
例題: Add Interest
內部類別可以用得到在它定義所在的類別中的變數
class InterestAdder implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
double interest = account.getBalance() * RATE / 100;
account.deposit(interest);
System.out.println("Balance = " + account.getBalance());
}
}
- 內部類別可以用得到在它定義所在的類別中的個體變數(instance variable)和方法
- 內部類別可以用得到在它定義所在的方法中所有的 final 變數
File TimerTest.java
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import javax.swing.Timer;
/**
This program uses a timer to add interest to a bank
account once per second.
*/
public class TimerTest
{
public static void main(String[] args)
{
final BankAccount account = new BankAccount(1000);
class InterestAdder implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
double interest = account.getBalance() * RATE / 100;
account.deposit(interest);
System.out.println("Balance = " + account.getBalance());
}
}
InterestAdder listener = new InterestAdder();
final int DELAY = 1000; // milliseconds between timer ticks
Timer t = new Timer(DELAY, listener);
t.start();
JOptionPane.showMessageDialog(null, "Quit?");
System.exit(0);
}
private static final double RATE = 5;
}
Timer 類別的 UML 圖