このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
親クラスのメソッドは子クラスでオーバライドできます。 そして、子クラスのインスタンスを親クラスの変数で参照していても、メソッ ドを呼び出すと子クラスでオーバライドしたメソッドが呼び出されます。 これをポリモーフィズムと言います。 既に、 toString メソッドなどで多用してきたテクニックです。
class A {
protected String value;
public A(String value){
this.value = value;
}
public String getValue(){
return "A has "+value+".";
}
@Override public String toString(){
return "I am A.";
}
}
class B extends A{
public B(String value){
super(value);
}
@Override public String getValue(){
return "B has "+value+".";
}
@Override public String toString(){
return "I am B.";
}
}
class C extends A{
public C(String value){
super(value);
}
@Override public String getValue(){
return "C has "+value+".";
}
@Override public String toString(){
return "I am C.";
}
}
class Rei {
private static void show(A[] array){
for(A a : array){
System.out.println(a);
}
}
public static void main(String[] arg){
A[] array = { new A("an apple"), new B("an orage"), new C("a peach")};
show(array);
for(A a : array){
System.out.println(a.getValue());
}
}
}
これをポリモーフィズムを使わずに、記述すると非常に面倒で見づらいプログ ラムになります。 上記の例で、全て A 型のオブジェクトとし、表示を A, B, C と切り替えるた めに、列挙型とし、 switch 文で分岐するように書くことができます。
enum Type { typeA, typeB, typeC }
class A {
private Type type;
private String value;
public A(Type t, String value){
type = t;
this.value = value;
}
public String getValue(){
String ret="";
switch(type){
case typeA:
ret = "A has "+value+".";
break;
case typeB:
ret = "B has "+value+".";
break;
case typeC:
ret = "C has "+value+".";
break;
}
return ret;
}
@Override public String toString(){
String ret="";
switch(type){
case typeA:
ret = "I am A.";
break;
case typeB:
ret = "I am B.";
break;
case typeC:
ret = "I am C.";
break;
}
return ret;
}
}
class Rei {
private static void show(A[] array){
for(A a : array){
System.out.println(a);
}
}
public static void main(String[] arg){
A[] array = { new A(Type.typeA, "an apple"),
new A(Type.typeB, "an orage"),
new A(Type.typeC, "a peach")};
show(array);
for(A a : array){
System.out.println(a.getValue());
}
}
}
このようにデータを取り扱うすべてのメソッドの中に switch, case 文を書いて処理 を分岐する必要があります。 このような型による明示された条件分岐はオブジェクト指向言語においては誤 りとも言えるものです。 つまり、データ型に対して switch-case や if 文で区分して処理を行うよう なメソッドは全てポリモーフィズムでリファクタリングすべきということです。
例7-1 において、出力する文字列はほとんど同じです。 そして、各オブジェクトにおいて異なるのはオブジェクトクラスにおける名前 のみです。 ここで「オブジェクトごとに名前を切り替える」というのは前節で説明した通りポリ モーフィズムで片付けることになります。 つまり、各オブジェクトで、 getName なるメソッドを持てば、親クラスから getName メソッドを使用して出力文字列を作成することができます。 なお、各クラスで文字列を作成するためだけに使用するメソッドは protected 修飾をし、外部からの使用は禁止します。
class A {
protected String getName(){
return "A";
}
protected String value;
public A(String value){
this.value = value;
}
public String getValue(){
return getName()+" has "+value+".";
}
@Override public String toString(){
return "I am "+getName()+".";
}
}
class B extends A{
public B(String value){
super(value);
}
@Override protected String getName(){
return "B";
}
}
class C extends A{
public C(String value){
super(value);
}
@Override protected String getName(){
return "C";
}
}
class Rei {
private static void show(A[] array){
for(A a : array){
System.out.println(a);
}
}
public static void main(String[] arg){
A[] array = { new A("an apple"), new B("an orage"), new C("a peach")};
show(array);
for(A a : array){
System.out.println(a.getValue());
}
}
}
この、「サブクラスで表示文字列を返すメソッドをオーバライドし、親クラスで、 そのメソッドを使用した文字列を返す」手法をテンプレートメソッド デザインパターンと言います。 なお、本来のテンプレートメソッドでは親クラスでの表示文字列を返すメソッ ドは抽象メソッドとし、親クラスは抽象クラスとします。
お金を扱うクラスとして日本円を扱う Yen とアメリカドルを扱う Dollar を 考えます。 これらはどちらもお金ですから、お金のクラス Money を作ると is-a 関係に なります。 したがって、それぞれの共通の処理は Money クラスで定義し、Yen 、 Dollar それぞれのクラスが継承するようにします。 なお、Money 自体はインスタンス化する必要がありませんので、抽象クラスと して定義します。 その上で値を保持し、 toString メソッドで金額を表示するようにします。
さて、金額の表示ですが、 Yen であれば「100円」、 Dollar であれば「$50」 のように通貨記号の前置、後置の区別があります。 このため、 Money クラスの値 value に対して、通貨記号を付けたものを toString メソッドで返すにはどうすれば良いでしょうか?
ここでテンプレートメソッドを使います。 前置する記号と後置する記号を Money クラスではそれぞれ abstract メソッ ドとして宣言します。 そして、 Yen と Dollar クラスでそれぞれ実際にオーバライドして定義しま す。 このようにして作成したのが下記のプログラムです。
abstract class Money {
protected double value;
protected Money(double value){
this.value = value;
}
abstract protected String getPrefix();
abstract protected String getPostfix();
@Override public String toString(){
return getPrefix()+String.valueOf(value)+getPostfix();
}
}
class Dollar extends Money{
public Dollar(double value){
super(value);
}
@Override protected String getPrefix(){
return "$";
}
@Override protected String getPostfix(){
return "";
}
}
class Yen extends Money{
public Yen(double value){
super(value);
}
@Override protected String getPrefix(){
return "";
}
@Override protected String getPostfix(){
return "円";
}
}
class Rei {
public static void main(String[] arg){
Yen y = new Yen(100);
Dollar d = new Dollar(50);
System.out.println(y);
System.out.println(d);
Money[] array = {y,d};
for(Money m : array){
System.out.println(m);
}
}
}
前回、比較を行うのに、比較子というオブジェクトを考えました。 これは、実際に並べ変えを行う java.util.Arrays.sort や java.util.Collections.sort などの処理において重要な「比較」という操作 を抽象化し、後でユーザが自由に与えるようにしたものです。 このように、特定の処理において、オブジェクトで機能を指定するような方法 をストラテジデザインパターンと言います。 Java の場合次のようなプログラムを組みます。
なお、抽象クラスは実装をしないひとつのメソッド名のみを持ちますので、 Java の場合、 interface で定義します。
以前演習で行った、複数の combination に対するテストをこのストラテジ デザインパターンで行うことを考えます。 メソッド名はそのまま combination とし、抽象クラス名(interface) は Combi とします(同じような名前にしたのには特に意味はありません)。
interface Combi {
int combination(int n, int m);
}
class Rei {
private static void test(Combi c){
final int[][] indata = { { 0,0}, {2,1}, {10,3} };
final int[] outdata = { 1, 2, 120};
for(int i = 0; i < indata.length; i++){
if(c.combination(indata[i][0],indata[i][1])==outdata[i]){
System.out.println("Ok");
}else{
System.out.println("NG");
}
}
}
}
このようにすると、 Combi を implement して、 combination を実装したク ラスのオブジェクトは、この test メソッドに与えることができます。
ストラテジは非常に多用されるデザインパターンです。 1、2個のメソッドだけが定義されている interface のオブジェクトを引数に 持つ関数は、ストラテジになります。 従って、次の interface はストラテジを使うのが前提としています。
ストラテジデザインパターンを使用するには、次の様にします。
abstract class A {
abstract public void show();
}
class B extends A {
public B(){}
public void show(){
System.out.println("Hello");
}
}
class Rei {
private static void view(A a){
a.show();
}
public static void main(String[] arg){
B b = new B();
view(b);
}
}
このように引数の型として抽象クラスを用いれば、ポリモーフィズムが働くた め、うまく行きます。 しかし、 Generics を用いる場合はそのようには行きません。 Iterable<A> を引数に取って、複数回 show() を実行しようとしても、 下記の様にするとコンパイルエラーが出ます。
import java.util.*;
class Rei {
private static void view(Iterable<A> i){
for(A a : i){
a.show();
}
}
public static void main(String[] arg){
ArrayList<B> al = new ArrayList<B>();
al.add(new B());
al.add(new B());
view(al);
}
}
これは、変数の代入の時はサブクラスのオブジェクトを受け入れますが、 Generics の型計算においては正確に型に一致しないと受け入れ無いというルー ルによります。 上記の view において、引数の型を Iterable<B> に変更すればもちろ ん動作します。 しかし、これでは Generics の意味がありません。
import java.util.*;
private static void view(Iterable<B> i){
for(A a : i){
a.show();
}
}
Generics の引数の型として A のサブクラスを取るように指定することで、こ れは解決します。 これには次の様に指定します。
import java.util.*;
private static <E extends A> void view(Iterable<E> i){
for(A a : i){
a.show();
}
}
前節の解決策で見逃されている点があります。 これは、 view 関数が A のメソッドを明記してい使用している点です。 もし、継承を行う元のクラスが Generics を使用している場合はどうでしょう か? これを考えるため、 java.lang.Comparable を継承したクラスを引数にする関 数を考えましょう。 簡単のために、単純に compareTo 関数を返す 2 引数の関数を考えましょう。 まずは、関数の引数に実クラス名を与えます。
class A implements Comparable<A>{
private int num;
public A(int n){
num=n;
}
public int compareTo(A x){
if(num==x.num) return 0;
if(num<x.num) return -1; else return 1;
}
public boolean eqauls(Object o){
if(this == o) return true;
if(o==null) return false;
if(!(o instanceof A)) return false;
final A other = (A) o;
return num==other.num;
}
public int hashCode(){
return num;
}
}
class Rei {
private static int compare(Comparable<A> x, A y){
return x.compareTo(y);
}
public static void main(String[] arg){
A a1 = new A(1);
A a2 = new A(2);
System.out.println(compare(a1,a2));
}
}
ここで、 compare 関数の A を消して、 Comparable を実装している全てのク ラスに対して、機能させるようにするのが目標です。 そこで、 Generics を使用して、 compare 関数を次のように書き換えます。
private static <E extends Comparable<E>> int compare(E x, E y){
return x.compareTo(y);
}
これで A の記述が消えて、任意の Comparable を実装しているクラスで機能 出来るように見えます。 ところが、次のようなクラスではコンパイルエラーが出てしまいます。
class B extends A {
public B(int n){
super(n);
}
}
...
B b1 = new B(1);
B b2 = new B(2);
System.out.println(compare(b1,b2));
...
しかも呼び出し時に A にキャストしてあげるとコンパイルが通り、正しく実 行できます。
...
B b1 = new B(1);
B b2 = new B(2);
System.out.println(compare((A)b1,(A)b2));
...
これは一体何が問題なのでしょうか? 実は、キャストを一つ外すと問題が見えてきます。
...
B b1 = new B(1);
B b2 = new B(2);
System.out.println(compare(b1,(A)b2));
...
上記のように第二引数が B の型であるのがまずいことがわかります。 これはどういうことかと言うと、 B は A のサブクラスで、 A は Comparable<A> を実装しています。 つまり、 B は Comparable<B> を実装しているわけではなく、 Comparable<A> を実装しているだけです。 しかし、運用上は Comparable<A> を実装しているだけで、 B には compareTo メソッドが有効になりますから、順序づけることは可能です。 したがって、何らかの親クラスが Compare<親クラス> を実装していれ ば良いことになります。 ここで Generics で「何らかのクラス」と名前をつけずにクラスを指定する際 ?(ハテナマーク)を使用することが出来ます。 したがって、 Comparable の compareTo を使用する関数の Generics は次の ように指定します。
private static <E extends Comparable<? super E>>
int compare(E x, E y){
return x.compareTo(y);
}
実際、 java.util.Collections.sort も同じ指定になっています。
実装を動的に切り替えるために、抽象クラスのメソッドを実装したクラスのオ ブジェクトを取り替えるというのがストラテジです。 この手法をさらに発展させたデザインパターンがあります。
特定のオブジェクトの状態の変化を観測し、変化したときに連動するように動 作させるためのデザインパターンです。 「GUI のボタンを押すと、特定の機能が動作する」などによく用いられます。 観測対象のオブジェクトに特定メソッドを持つ抽象クラスを複数個登録できる ようにしておき、状態が変化したとき、登録してあるオブジェクトに対して、 特定メソッドを実行します。 Java では Listener とも呼ばれます。
抽象クラスのメソッドを実装したオブジェクトを外部から設定する代わりに、 抽象クラスの内部に抽象メソッドとしてファクトリを内蔵させるものです。
interface Hello {
void greeting();
}
class H1 implements Hello {
public H1(){}
public void greeting(){
System.out.println("Hello");
}
}
class H2 implements Hello {
public H2(){}
public void greeting(){
System.out.println("こんにちは");
}
}
abstract class A {
protected void view(){
createHello().greeting();
}
abstract protected Hello createHello();
}
class B extends A {
public B(){}
protected Hello createHello(){
return new H1();
}
}
class Rei {
public static void main(String[] arg){
(new B()).view();
}
}
さて、ここで少し実用的なアプリケーションに関して考えましょう。
Web サーバの一部の機能を実現することにします。 ここでは厳密な話をするわけではないので、もっとも単純な HTTP/1.0 につい て説明します。 なお、HTTP/1.1 の仕様にあるように、実際のアプリケーションを作るには HTTP/1.1 に準拠する必要があります (HTTP/1.0 は廃止になってませんが、新規のアプリケーションを作ることは推 奨されていません)。
Web サーバは次のような働きをします。
このうち、今回はファイルパスを与えられて、返答メッセージを作成するとい う作業をプログラムにしようと思います。
返答メッセージは三つの部分に別れます。
ステータス行 ヘッダ部 ... (空行) ボディ部
ステータス行は次のようになっています。
HTTP/1.0 ステータスコード ステータスを表す言葉
例えば、ファイルが存在して正常にファイルを送るときは次のようになってい ます。
HTTP/1.0 200 Ok
一方、ファイルが存在しなくてエラーメッセージが送られるときは次のように なっています。
HTTP/1.0 404 Not Found
ここでは単純のためにこの二つの状態のみを扱うことにします。 つまり、ファイルが存在すれば 200 でファイルを返すメッセージを作る。 一方、ファイルが無ければ 404 でファイルがないというエラーメッセージを 作ることにします。
ヘッダとはボディに含まれるコンテンツのメタデータが書かれます。 ヘッダは各行がフィールドと呼ばれ、何行にも渡って記述されます。 そして空行で終了し、ボディ部が始まります。 フィールドは「フィールド名: フィールド値」という書式になっています。 最低限必須なヘッダフィールドは以下の通りです。
content-type の処理はここでは本質ではないので 「text/plain; charset=shift_jis」に決め打ちすることにします。 さて、残りのヘッダに関してはファイルを送る時とデータを送る時で扱いかた が違います。
ファイルを送る際は次のような意味になります。
一方、エラーメッセージを送る際は次のような意味になります。
ボディ部はファイルが存在する場合はファイルの内容そのものになります。 一方、エラーメッセージに関してはユーザが読んで理解できるような(自然言 語の)メッセージを送ります。
ファイルパスからメッセージを作成するプログラムを作ります。 具体的にはメッセージのオブジェクトを作成し、 toString メソッドで全メッ セージが得られるようにします。
さて、メッセージにはファイルの内容とエラーメッセージの二つがあります。 つまり、「ファイルの内容」と「エラーメッセージ」は共に「メッセージ」に 対して is-a 関係にあることになります。 そこで、 Message クラスとそのサブクラスである FileConent クラスと ErrorMessage クラスを作ります。 ここで、Message クラスの toString メソッドはこの二つのサブクラスに応じ て出力を変化させるので、テンプレートメソッドを使います。 とりあえず、 Message クラスでの toString メソッドは次のようになります。
abstract class Message {
@Override public String toString(){
return getStatusLine()+"\n"+getHeader()+"\n"+getBody();
}
abstract protected String getStatusLine();
abstract protected String getHeader();
abstract protected String getBody();
}
なお、ステータス行は一行のため文字列の最後に改行を付けません。 また、ヘッダとボディの間には空行を入れますので、上記のように二箇所に改 行が入ります。
さて、ファイルパスからメッセージのオブジェクトを作るわけですが、作られるオブ ジェクトは FileContent のインスタンスか、または ErrorMessage のインスタン スのいずれかになります。 つまり、指定したクラスのインスタンスを生成するコンストラクタは使えませ ん。 そこで、 Message クラスにファクトリ(インスタンス生成の static メソッド) を用意します。 そして、 FileContent と ErrorMessage のコンストラクタは protected にな ります。
また、ErrorMessage クラスは、今回は一種類のエラーメッセージしか作りま せんが、HTTP/1.0 のステータスコードにおいてエラーはいくつもあります。 そこで、404 メッセージ決め打ちではなく、拡張性を考えて、ステータスコー ドをインスタンスに与えると、それに応じたエラーメッセージを持つインスタ ンスを生成することにします。
一方で、FileContent には File オブジェクトをコンストラクタに与えて生成 します。 FileContent のコンストラクタの仕事は、ファイルの内容を読み出して、ファ イルの更新日付を取得することです。 ファイルの取得に失敗する可能性がありますので、コンストラクタは FileNotFoundException と IOException を投げることになります。
ステータスはエラーになってもならなくても使います。 また、 HTTP/1.0 という単純なプロトコルでもさまざまなステータスがあり、 今回の Ok と NotFound 以外もあります。 上記の Web サーバの仕事の分析において、ステータス自体が名詞として登場 しましたので、ステータス自体をオブジェクトとします。 ステータスクラスではステータスコードで「ステータスを示す言葉」も「ユー ザへのエラーメッセージ」も管理するとします。 但し、ステータスコードでデータを管理するには HashMap が適切です。 このコードとデータの登録は前処理でしておく必要があります。 そのため、コンストラクタで前処理をし getter インスタンスを生成しインスタンスへのメソッドで各データにアク セスすることにします。 インスタンスは一個でいいので、シングルトンデザインパターンを使います。
Status クラスはステータスコードからメッセージへの変換を行う関数を持つ だけのクラスにします。 そのため、変換する関数を static な静的関数として実装します。
class Status {
private static HashMap<Integer,String> word;
private static HashMap<Integer,String> message;
private Status(){}
private static void initializeWord(){
word = new HashMap<Integer,String>();
word.put(200,"Ok");
word.put(404,"Not Found");
}
public static String getWord(int statusCode){
if(word==null){
initializeWord();
}
return word.get(statusCode);
}
private static void initializeMessage(){
message = new HashMap<Integer,String>();
message.put(404,"Not Found"); //人間用のメッセージ
}
public static String getMessage(int statusCode){
if(message==null){
initializeMessage();
}
return message.get(statusCode);
}
}
Status クラスにより、各メッセージから getStatusCode メソッドで得られたス テータスコードによりステータス行を作ることができます。 これもテンプレートメソッド的な表現になります。
abstract class Message {
abstract protected int getStatusCode();
private String getStatusLine(){
return "HTTP/1.0 "+getStatusCode()+" "
+Status.getWord(getStatusCode());
}
}
getHeader も同様にテンプレートメソッドで作ります。 ヘッダの文字列を作るのに println が使えると便利なので、 StringWriter と PrintWriter を使用します。
private String getHeader(){
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw);
pw.println("Content-Type: text/plain; charset=shift_jis");
pw.println("Date: "+getDate());
pw.println("Content-Length: "+getBody().length());
pw.close();
return sw.toString();
}
abstract protected String getDate();
FileContent クラスは File オブジェクトを引数に取るコンストラクタと、 getStatusCode, getDate, getBody を持ちます。 コンストラクタは FileNotFoundException と IOException を投げます。
FileContent では与えられた File オブジェクトより FileInputStream オブ ジェクトを作り、ファイル全体を読みます。 但し、失敗したときに発生した Exception は処理せず、呼出側にそのまま伝 えます。 ファイルを全て読んだら body, date, errorCode を所定の値に設定します。
class FileContent extends Message {
private String body;
private Date date;
private int errorCode;
protected FileContent(File f)
throws FileNotFoundException, IOException {
super();
final FileInputStream fis = new FileInputStream(f);
final CharArrayWriter caw = new CharArrayWriter();
int data;
while((data = fis.read())!=-1){
caw.write(data);
}
body = caw.toString();
date = new Date(f.lastModified());
errorCode = 200;
}
protected String getBody(){ return body; }
protected String getDate(){ return date.toString(); }
protected String getStatusCode(){ return errorCode; }
}
一方、 ErrorMessage は与えられたエラーコードを元に、 Status クラスから 情報を検索してメッセージを作ります。
class ErrorMessage extends Message {
private String body;
private Date date;
private int errorCode;
protected ErrorMessage (int errorCode) {
super();
this.errorCode = errorCode;
body = Status.getInstance().getMessage(errorCode);
date = new Date();
}
protected String getBody(){ return body; }
protected String getDate(){ return date.toString(); }
protected String getStatusCode(){ return errorCode; }
}
ここで、二つのクラスにおいて、 body, date, errorCode, getBody(), getDate(), getStatusCode() が共通になっています。 そこで、テンプレートメソッドの形が変則的になりますが、 変数を protected にして、 Message クラスに移動し、またメソッド もそのまま Message クラスに移動します。 但し、将来、追加したサブクラスで get... をオーバライドする可能性があり ますので、テンプレートメソッドを壊して変数を生で使うことは避けます。
最後に Message クラスでインスタンスを生成する static メソッド getInstance を作成します。 これは文字列を与えられたら、 FileContent オブジェクトを作成しますが、 その際に FileNotFoundException が出たら ErrorMessage オブジェクトに差 し替えるものです。 なお、 IOException も本来は処理しなければなりませんが、ここでは放置し ます。
abstract class Message {
public static Message getInstance(String filePath)
throws IOException {
final File f = new File(filePath);
Message message;
try {
message = new FileContent(f);
}catch(FileNotFoundException e) {
message = new ErrorMessage(404);
}
return message;
}
}
完成したプログラムと、テスト用のプログラムは 別ページ に示します。
HTTP のメッセージを作るプログラムにおいて、 IOException を検知したら 「500 Internal Error」というエラーメッセージが出るようにプログラムを改 造しなさい。