第 5 回 クラスの作成

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

5-1. プログラムの作成法

プログラミング言語

プログラミング言語にはいろいろな種類があります。 これらの書きやすさはいろいろありますが、 特定の処理をさせるのに関して、可能不可能に関してはほとんど差がありません。 オブジェクト指向言語で無くてもオブジェクト指向的なプログラムは組むこと ができます。 但し、手間やメリットが言語によって異なるだけです。

プログラミング言語的な側面で Java 言語を見ると次のような特徴があります。

  1. 静的な型付き言語
  2. 基底クラス(java.lang.Object)がある
  3. 基本型を直接参照できない
  4. 例外処理を扱える
  5. プログラムそのものは変数に代入できない(リフレクションはできる)

静的型付き言語とは、コンパイル時に全ての変数の型が確定して いて、関数の呼出しや、式の解釈などで型のチェックを行うことのできるプロ グラミング言語です。 変数になんでも代入はできなくなりますが、型の不一致をコンパイル時に発見 することができるので、プログラミングミスを予防できる効果があります。 また、 Java ではクラスごとにコンパイルができます。 なお、現行の Java では Object 型を使い、ダウンキャストというコンパイル 時に型チェックできない仕組を使う必要があるので、必ずしも全ての型チェッ クがコンパイル時に終わるわけではありません。

ウォーターフォールモデルと、アジャイル開発

パーソナルコンピュータが普及する前は、コンピュータのプログラムをコン ピュータで作成していたわけではありませんでした。 いわゆる「机上」というもので、多くの書類を作成しながらプログラムを紙に 書いて作成していました。

この時代に開発されたプログラムの開発手法が ウォーターフォールモデル と呼ばれる手法です。 これは、開発工程を例えば「設計」「製作」「テスト」に分割したとき、それ ぞれの工程に期間を別に割り当て、設計が終わったら製作、製作が終わったら テストという具合に開発を行う方法です。 この長所はテスト以外で実際のコンピュータが不要な点です。 しかし、最大の欠点としてはすべての工程が 100% 完璧にできないと、次の工 程が不可能なことと、開発中に設計の誤りが判明した場合、すべての工程をや り直さなければならないことです。 人間は誤りを犯すものですし、複雑なシステムは開発中に理解が進んで、より 良い設計を考案することもあります。 このウォーターフォールモデルは、このような観点では柔軟性が無く欠点の多い ものです。

ウォーターフォールモデルの欠点を補うには、これらの工程を繰り返し行い、 各工程の完成度や責任を軽減することです。 そのためには、少しずつ設計、製作し、テストを行った結果を設計にフィードバックしていき、 繰り返しこれらの工程を行ってシステムを作っていくことです。 このような開発方法を スパイラルモデル と言います。 スパイラルモデルの中で短期間で少人数で行う開発手法が アジャイル開発です。 アジャイル開発にはさまざまな具体的な手法がありますが、 XP(エクストリームプログラミング)はその中の一つの有名な手法です。

アジャイル開発は短期間で工程を繰り返すのが特徴です。 この繰り返しの動機付けを行うものを「駆動」と呼びます。 例えば、「ユーザ機能駆動」とは、ユーザを開発工程に組み込み、 開発の繰り返しの動機付けを行わさせるものです。 そのためには次のように開発を行います。

  1. 設計ではユーザの意見を取り入れます
  2. 個々のメソッドの単体テストにはユーザは参加しませんが、全体を結合し て行うようなテストではユーザの意見を取り入れ、テストの可否や仕様への 反映を行い、繰り返し工程を行います。

この他に、ユーザを直接組み入れなくても、ソフトウェアのマニュアルを先に 作ってマニュアルを満足させるように開発を行う手法もあります。

また、「テスト駆動」や「テストファースト」と呼ばれる開発手法もあります。 これは、ソフトウェアの製作を行う前に、自動テストプログラムを作成すると いう開発手法です。 テストファーストにより、次のような利点が生じます。

テストは、考えうるすべての入力ケースを生成するのではなく、エラーが生じ そうな入力のみを生成するように作ります。 このテスト駆動は作業を区分化でき仕様が明確になります。さらに仕様変更が 生じた場合も、テストを書き換えた後は通常の開発作業と変わらない作業にな るため柔軟に対応できます。

5-2. クラスの作成

オブジェクト指向のプログラミングではクラスを作るのが重要な作業になりま す。 ここでは様々なクラスの作成法を紹介します。

リファクタリング

オブジェクト指向型のプログラミング言語の特徴はカプセル化できる点です。 これはプログラムの部分を変更しても他の部分への影響が少ないという利点が あるため、プログラムを修正しながら作成するアジャイル開発に適しています。

オブジェクト指向言語でプログラムを作る場合は、自分の癖で自由に作り、その 後、リファクタリングしても他のプログラミング言語ほど修正が困難ではあり ません。

リファクタリングで目指す目標は次の点です。

  1. メソッド、関数を増やす
  2. メソッド、関数の名前は役割をきちんと表す名前(動詞)にする
  3. クラスを増やす
  4. クラスも役割をきちんと表す名前(名詞)にする
  5. 不必要な public メンバを減らす
  6. 静的変数を減らす
  7. メソッド内でのローカル変数を減らす

これは以下の根拠に基づきます。

  1. メソッド、関数の内部には外部からアクセスできない。入出力の仕様だけ 決まっていれば内部は自由に変更できるようになる
  2. クラスにはさまざまなカプセル化の機能があるため、さまざまなデータを クラスに分割すると相互の関係を整理することができる
  3. public メンバが減ればクラス間の関係が疎になり、カプセル化が強化さ せる。特に変数の public メンバは特例が無い限り避けるべきである
  4. 不必要な静的変数は、複数のオブジェクトからアクセスを受けるため、ク ラス内でのデータの結び付きが密になり、個々のインスタンスのカプセル化を 妨げる
  5. ローカル変数はメソッドや関数の中でどのようにも変更を受けるため、常 に特定の意味付けが行われているわけではない。 計算式に意味があればそれだけのメソッドを作成するなどし、関数の最初から 最後までさまざまな値を取るような変数を、複数のメソッドで処理を分割でき ないか検討する。

例5-1

実は前回の Collection のテストを行うプログラムは、初めて動作した時点で はクラスが 1 つしかない、べたなプログラムでした。 それをリファクタリングして整形したのが、前回示したプログラムです。 まずは、リファクタリング前のプログラムをお示しします。

初期のプログラム

テストプログラムが動き出すまでは、意味ある操作を単純に静的関数(private static)として定 義するだけの工夫しか考えてませんでした。 これはプログラムが大して大きくならないと想定したからです。 しかし、できあがったものはそれなりのサイズでした。


import java.util.*;
class Rei {
    private static void initialize(int[] array){
	for(int i=0; i<array.length; i++){
	    array[i]=array.length-i;
	}
    }
    private static void add(Collection<Integer> col, int[] array){
	for(int i : array){
	    col.add(i);
	}
    }
    private static void contains(Collection<Integer> col, int[] array){
	for(int i : array){
	    col.contains(i);
	}
    }
    private static void remove(Collection<Integer> col,int[] array){
	for(int i : array){
	    col.remove(i);
	}
    }
    private static void test(Collection<Integer> col, int[] array){
	Date t0 = new Date();
	add(col,array);
	Date t1 = new Date();
	System.out.print((t1.getTime()-t0.getTime())/1000.0+" ");
	contains(col,array);
	Date t2 = new Date();
	System.out.print((t2.getTime()-t1.getTime())/1000.0+" ");
	remove(col,array);
	Date t3 = new Date();
	System.out.println((t3.getTime()-t2.getTime())/1000.0);
    }
    public static void main(String[] arg){
	int[] array = new int[Integer.parseInt(arg[0])];
	initialize(array);
	@SuppressWarnings({"unchecked"})
	Collection<Integer>[] cols = (Collection<Integer>[])
	    new Collection[] { new ArrayList<Integer>(),
       			 new LinkedList<Integer>(),
			 new HashSet<Integer>(),
			 new TreeSet<Integer>()};
	System.out.println("add, contains, remove");
	for(Collection<Integer> col : cols){
	    test(col,array);
	}
    }
}

さて、リファクタリングの前にプログラムの注意点を説明します。 まず、 @SuppressWarnings を使用している箇所ですが、ここは次のようにす るとコンパイルができなくなります。


	Collection<Integer>[] cols = 
	                { new ArrayList<Integer>(),
       			 new LinkedList<Integer>(),
			 new HashSet<Integer>(),
			 new TreeSet<Integer>()};

これは次のようなコンパイルエラーが出ます。

collection.java:38: 汎用配列を作成します。
        Collection<Integer>[] cols = { new ArrayList<Integer>(),
                                     ^

これに何が問題があるかというと、 Generics を使用した配列を new で作る ことができないということです。 実は上の配列の初期化の構文は Java では次の構文の簡略形です。


	Collection<Integer>[] cols 
          = new Collection<Integer>[]{ new ArrayList<Integer>(),

したがって、この構文から Generics の指定を取った型で初期化された配列を 作り、さらに出来上がった配列を Generics を指定した型でキャストしたのが 上記のプログラムです。

リファクタリング1

このプログラムをリファクタリングします。 まず着目するのは、複数の静的関数の引数が全て同じで冗長であることです。 そこで、この引数を持っている静的関数をまとめてひとつのクラス Test にし てしまいます。 main からは test 関数だけを呼んでますので、これ以外のメソッドは private のままのメソッドにしています。


import java.util.*;
class Test { // クラスの新設
    private int[] array; // 関数の引数をインスタンス変数に
    private Collection<Integer> col;
    public Test(Collection<Integer> col, int[] array){
	this.array = array;
	this.col = col;
    }
    private void add(){
	for(int i : array){
	    col.add(i);
	}
    }
    private void contains(){
	for(int i : array){
	    col.contains(i);
	}
    }
    private void remove(){
	for(int i : array){
	    col.remove(i);
	}
    }
    public void test(){ // public に変更
	Date t0 = new Date();
	add();
	Date t1 = new Date();
	System.out.print((t1.getTime()-t0.getTime())/1000.0+" ");
	contains();
	Date t2 = new Date();
	System.out.print((t2.getTime()-t1.getTime())/1000.0+" ");
	remove();
	Date t3 = new Date();
	System.out.println((t3.getTime()-t2.getTime())/1000.0);
    }
}
class Rei {
    private static void initialize(int[] array){
	for(int i=0; i<array.length; i++){
	    array[i]=array.length-i;
	}
    }
    public static void main(String[] arg){
	int[] array = new int[Integer.parseInt(arg[0])];
	initialize(array);
	@SuppressWarnings({"unchecked"})
	Collection<Integer>[] cols = (Collection<Integer>[])
	    new Collection[] { new ArrayList<Integer>(),
			 new LinkedList<Integer>(),
			 new HashSet<Integer>(),
			 new TreeSet<Integer>()};
	System.out.println("add, contains, remove");
	for(Collection<Integer> col : cols){
	    Test test = new Test(col,array);
	    test.test();
	}
    }
}

同じ引数の関数を Test クラスに集め、インスタンスの変数としてその引数 と同じ変数を作成します。 コンストラクタを作り、その変数を初期化します。 次に各メソッドの引数の指定と static 修飾を機械的に消 していきます。 このような、クラスの追加、関数のクラス間の移動のような大幅かつ機械的な 変更の後でも、プログラムの動作やテストは容易です。

リファクタリング 2

関数の大幅な移動を行った後、体裁を整えるために微調整を行います。

  1. まず、 main で実際に Test のオブジェクトを起動するのに test.test() と いう表現をつかってます。これは字面的に意味が不明です。 実際はもともと test というのを動詞として使っていたわけですが、 Collection のインスタンスと int[] の配列をひとまとまりにして、さまざま なテストメニューを集めたものを名詞の Test というクラスにしたわけです。 したがって、名詞 test に対してふさわしい動詞を選ぶことにします。 do でも良いのですが、いちおう examine という単語を選びました。 これは意味が通ればどんな動詞でも構いません。 なるべくふさわしい名前を選びましょう。

  2. また、main では Collection の配列を用意していましたが、あらかじめ Test の配列を作り、初期化を行うことにします。 そうすると、 Collection の配列を作って、それに応じて Test のオブジェクトを作ってという二段階の手間だったものを一段階にすることが できます。 なお、ここの判断では、配列の初期化などでトリッキーな構文が無ければこの ままでもよいのですが、このようにすることで Test の配列にすると奇妙な構 文を避けることができます。

  3. また、int[] という配列を渡す効率も考慮します。 変数 array は一度初期化したら変更されません。 これを Test のそれぞれのコンストラクタに毎回与えるのは非効率です。 そこで、この変数は static に Test に持たせ、初期化も Test で行うようにします。 それに伴って、コンストラクタからは array を除きます。 一方、毎回 add, contains, remove などで array の値が参照されてますが、 この時オートボクシングが働いています。 これを Integer[] という配列にすると、最初の初期化だけオートボクシング が働くことになり変換の頻度が減ります。

このように微調整を行ったのが下記のプログラムです。


import java.util.*;
class Test {
    private static Integer[] array; // Integer の配列を static に持つ
    public static void initialize(int size){ // 3
	array = new Integer[size];
	for(int i=0; i<array.length; i++){
	    array[i]=array.length-i; // ここだけオートボクシングが働く
	}
    }
    private Collection<Integer> col;
    public Test(Collection<Integer> col){
	this.col = col;
    }
    private void add(){
	for(Integer i : array){
	    col.add(i);
	}
    }
    private void contains(){
	for(Integer i : array){
	    col.contains(i);
	}
    }
    private void remove(){
	for(Integer i : array){
	    col.remove(i);
	}
    }
    public void examine(){
	Date t0 = new Date();
	add();
	Date t1 = new Date();
	System.out.print((t1.getTime()-t0.getTime())/1000.0+" ");
	contains();
	Date t2 = new Date();
	System.out.print((t2.getTime()-t1.getTime())/1000.0+" ");
	remove();
	Date t3 = new Date();
	System.out.println((t3.getTime()-t2.getTime())/1000.0);
    }
}
class Rei {
    public static void main(String[] arg){
	Test.initialize(Integer.parseInt(arg[0])); // 3
	// Test の配列にする 2
	Test[] tests = { new Test(new ArrayList<Integer>()),
			 new Test(new LinkedList<Integer>()),
			 new Test(new HashSet<Integer>()),
			 new Test(new TreeSet<Integer>())};
	System.out.println("add, contains, remove");
	for(Test test : tests){
	    test.examine(); // メソッド名の変更 1
	}
    }
}
リファクタリング 3

大分すっきりしましたが、 examine メソッドの中がまだごちゃごちゃしてい ます。 examine はメソッドを呼ぶたびに時刻を計ってから、経過時間を表示しています。 ここで、時刻をはかって表示するというのは、 Test クラスが持たなければな らない機能というより、いわゆるストップウォッチのような独立した機能と考 えることができます。 そのため、 StopWatch という新しいクラスを作成して、時刻計測の機能を全 て持たせることにします。

時刻計測の仕組は、 java.util.Date を使います。 コンストラクタで現在時刻をもつインスタンスを作成し、 getTime メソッド でミリ秒単位の時刻が得られます。 これを利用して StopWatch クラスを作成します。 StopWatch オブジェクトは計測のたびに次の計測が始まることとしましょう。 このためには、 始めに開始時刻を覚えておき、その後、計測時に時刻を覚 え差を表示します。 そして、次の計測では今の終了時刻を開始時刻とします。 これを Java 風にアレンジします。 まず、始めの開始時刻を覚えさせるのはコンスト ラクタにやらせます。 一方、計測では計測と表示を分けずに、表示をさせる際に終了時刻を覚えて表 示させ、次の開始時刻の設定を行わせることにします。 そこで、時刻の計測と表示については、 toString をオーバライドさせるのが 簡単だと思われます。 つまり、 toString() を呼ぶことにより、計測値を文字列で得ますが、その度 に文字列を返すだけではなく、次の計測を始めるようにします。 このようにして StopWatch クラスを作成したのが次のプログラムです。


import java.util.*;
class Test {
    private static Integer[] array;
    public static void initialize(int size){
	array = new Integer[size];
	for(int i=0; i<array.length; i++){
	    array[i]=array.length-i;
	}
    }
    private Collection<Integer> col;
    public Test(Collection<Integer> col){
	this.col = col;
    }
    private void add(){
	for(Integer i : array){
	    col.add(i);
	}
    }
    private void contains(){
	for(Integer i : array){
	    col.contains(i);
	}
    }
    private void remove(){
	for(Integer i : array){
	    col.remove(i);
	}
    }
    public void examine(){
	StopWatch sw = new StopWatch();
	add();
	System.out.print(sw+" ");
	contains();
	System.out.print(sw+" ");
	remove();
	System.out.println(sw);
    }
}
class StopWatch {
    private Date time;
    public StopWatch(){
	time = new Date();
    }
    @Override public String toString(){
	Date newtime = new Date();
	String str = String.valueOf((newtime.getTime()-time.getTime())/1000.0);
	time = newtime;
	return str;
    }
}
class Rei {
    public static void main(String[] arg){
	Test.initialize(Integer.parseInt(arg[0]));
	Test[] tests = { new Test(new ArrayList<Integer>()),
			 new Test(new LinkedList<Integer>()),
			 new Test(new HashSet<Integer>()),
			 new Test(new TreeSet<Integer>())};
	System.out.println("add, contains, remove");
	for(Test test : tests){
	    test.examine();
	}
    }
}

CRCクラス分析

前章のように、リファクタリングによるオブジェクト指向プログラミングでは 一度プログラムを作成してから、オブジェクトクラスを作ってプログラムを整 理しました。 これは、二度手間な感覚がし、効率が悪い気がします。 そこで、プログラムを作成する前にクラスを設計する手法を紹介します。

CRC(Class-Responsibility-Collaborator)クラス分析はプログラムの仕様書か らクラスを設計する手法です。 始めにプログラムの仕様書と CRC カードを用意します。

CRC カードとは次のような書式のカードです。

Class(クラス名)
Responsibility(責務)Collaborator(協調クラス)
責務の列挙責務の対象となる他のクラス
......

CRC 分析ではまず、プログラムの設計書を日本語などの自然言語で作成します。 すると、日本語の文章には必ず文の要素として主語、目的語などの名詞句と、 動詞が現れます。 設計書から名詞を抜き出し、クラスの仮候補として CRC(Class Responsibility Collaborator) カードを作ります。 そして、その名詞に関与する動詞を責務に列挙し、また目的語である名詞を対 象となるクラスに指定します。

例5-2

  1. 次はプログラムの設計書の例です。

    ArrayList, LinkedList, HashSet, TreeSet に対して、速度の比較のテストを 行う。 そのため、それぞれのオブジェクトに対して、ダミーの要素を追加、検索、削 除を行う。 そして、それぞれの所要時間を計り表示する。

  2. この設計書において名詞動詞を分析すると次のようになります。

    ArrayList, LinkedList, HashSet, TreeSet に対して、速度 比較テスト 行う。 そのため、それぞれのオブジェクトに対して、ダミーの要素追加検索削除行う。 そして、それぞれの所要時間計り表示する

  3. この分析に対して CRC カードを作成します。 これは 1 枚ずつのカードにします。
    クラス名ArrayList
    責務協調クラス
    追加ダミーの要素
    検索ダミーの要素
    削除ダミーの要素
    クラス名LinkedList
    責務協調クラス
    追加ダミーの要素
    検索ダミーの要素
    削除ダミーの要素
    クラス名HashSet
    責務協調クラス
    追加ダミーの要素
    検索ダミーの要素
    削除ダミーの要素
    クラス名TreeSet
    責務協調クラス
    追加ダミーの要素
    検索ダミーの要素
    削除ダミーの要素
    クラス名速度
    責務協調クラス
    比較速度
    クラス名テスト
    責務協調クラス
    行う
    追加それぞれのオブジェクト、ダミーの要素
    検索それぞれのオブジェクト、ダミーの要素
    削除それぞれのオブジェクト、ダミーの要素
    計測と表示それぞれ(それぞれのオブジェクトへの追加検索 削除)、所要時間
    クラス名ダミーの要素
    責務協調クラス
    クラス名所要時間
    責務協調クラス
    計測
    表示する
  4. このようにして作成したカードに対して、ウォークスルーを行い ます。 ウォークスルーとはこれらのうち、どれが適切なクラスとしてふさわしいかを 検討する会議のことです。

    例えばこの例は、速度と所要時間というクラスの役割が重複しています。 そこでカードの内容を見ると、速度には比較がありますが、テストの場合、通 常比較自体は人間が行います。 つまりコンピュータは所要時間を列挙して、それを人間が見て速度を比較する ものです。 ですから、ソフトウェアの機能としては所要時間の表示のみを行えば、人間が 速度を比較できます。 したがって、速度のクラスは作成しないことにします。

    一方、テストの中で、「行う」と「計測と表示」がうまく整理されていません。 実際にはテストを行うということが、それぞれのオブジェクトにダミーの要素 を追加、検索、削除を行い、その所用時間を計測表示するということです。 したがって、このカードを次のように書き換えます。

    クラス名テスト
    責務協調クラス
    行う(追加、検索、削除の所要時間の表示) それぞれのオブジェクト、ダミーの要素、所要時間
    追加それぞれのオブジェクト、ダミーの要素
    検索それぞれのオブジェクト、ダミーの要素
    削除それぞれのオブジェクト、ダミーの要素
  5. このようにして作成したカードで矛盾無くそれぞれの責務と協調クラスの関係 が記述できたら、クラス名を実際のクラス名に、責務をメソッドにします。 協調クラスに関してはクラスの中のインスタンス変数か、静的変数か、メソッ ドの引数にするなどの選択があるため、どのようなプログ ラムにするかは固定しません。

UML

オブジェクト指向の機能は Java 言語だけに使える知識ではなく、さまざまな オブジェクト指向の言語に共通して活用できます。 そのため、プログラミングの手法やプログラムやデータの構造などは Java の プログラムによる字だけの表現より、何らかのルールを決めて図示した方が情 報のやりとりに便利です。

プログラムを図示する手法は古くはフローチャートがありました が、オブジェクト指向のプログラミングを図示する手法は別に開発されていま す。 これが UML(Unified Modeling Language)です。 UML にはフローチャートのようなプログラムの流れを示すような図も含まれて いますが、 CRC カードのようなクラスやメソッドの定義なども含まれていま す。 フローチャートもそうだったように UML もプログラミングのアイディアを分 析するために使うもので、細部まで正確に記述して使用するものではありませ ん。 但し、 UML にはさまざまなツールが作られており、 Eclipse にもプラグイン が作られています。 Eclipse でプログラムを作成すると、プラグインが自動的にクラス図と呼ばれ る図を生成するなどの機能があります。 Eclipse の UML のプラグインに関する情報は EclipseWiki http://eclipsewiki.net/eclipse/ などを参照し てください。

定石を使う

様々なアプリケーションの形態に応じたオブジェクトの設計方法が研究されて います。 オブジェクトの作り方の定石は デザインパターンと呼ばれていま す。 デザインパターンは数多く開発されています。 特に Gang of Four(4 人組)と呼 ばれる Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides らにより開発されたデザインパターンは Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 著、本位田真一、吉田和樹(監訳)、『オブジェクト指向における再利用のためのデザインパターン』、ソフトバンクパブリッシング、1995 という本で解説されています。

一方、よく陥りやすく、やってはいけないオブジェクトの構成は アンチパターン として、実例の調査などにより研究されています。 これらをよく知ることにより、多くのプログラミングの局面で解決策となるオ ブジェクト設計の定石をあてはめられるようになります。

オブジェクト指向のプログラミングでは、従来の狭い意味でのデータ構造やア ルゴリズムの他に、このようにデザインパターンやアンチパターンなどを学ぶ 必要があります。

ここでは、オブジェクト指向の初歩ということで、もっとも基本的なデザイン パターンを紹介します。

関数やメソッドに複数の引数が渡されているとき

オブジェクト指向言語では、自然な形で無い限り、関数やメソッドに多くの引 数が来ることはありません。 したがって、特定の関数やメソッドを作る際、それに渡す引数の塊が必要になっ た場合は、その引数の塊自体をひとつのオブジェクトとしてまとめることを検 討します。

例5-3

public static double distance(double x, double y){
...
}
...
double x = 3.1;
double y = 4.1;
double d = distance(x,y);
...

この x と y をまとめて Point というクラスを作ると、とりあえず次のよう になります。


class Point {
  private double x;
  private double y;
  public Point(double x, double y){
    this.x = x;
    this.y = y;
  }
}
...
public static double distance(double x, double y){
...
}
...
Point p = new Point(3.1,4.1);
double d = distance(p);
...

さらに、 distance 自体を Point のメソッドにした方が良いので次のように なります。


class Point {
  private double x;
  private double y;
  public Point(double x, double y){
    this.x = x;
    this.y = y;
  }
  public double distance(){...}
}
...
Point p = new Point(3.1,4.1);
double d = p.distance();
...

オブジェクトの集まり

オブジェクトの集まりに対して、もっとも基本的なデータ構造として配列があ ります。 但し、通常のプログラムの処理においては、オブジェクトの配列に対してさまざ まな操作が行われるのが普通です。 これらの操作は一般的には添字などを使用したループにより実現されます。 つまり、プログラムで配列をそのまま扱ってしまうと、配列に対する一つの操 作が複数行にわたるプログラムとして書かれてしまいます。 これは本来は「集まりオブジェクト」に対する「メソッド」として記述される のが理想です。 一方、 Java では配列の他に前回紹介したような java.util.Collection のサ ブクラスが存在し、配列と類似していますが、様々な複雑な操作ができるよう なデータの集まりを扱うことができます。 ここで、プログラムの流れとしては、データの集まりがどのような仕組になっているか は本質的では無く、実際はデータの集まりに対してどのような操作ができる かが本質です。 データの集まりが配列で扱われようが、 java.util.ArrayList で扱われよう と、大局的には関係ありません。 むしろ、個別のデータ構造はカプセル化されるべきです。 そのため、オブジェクトの集まり自体も、集まりとしての責務などを持ちますの で、それ自体が独立したクラスになるのは自然です。

例5-4

class User {  
    private String id;
    private String name;
    public User(String id, String name){
	this.id = id;
	this.name = name;
    }
    @Override public String toString(){
	return id+": "+name;
    }
}
class UserCollection { 
    private User[] users;
    public UserCollection(User[] users){
	this.users = users;
    }
    public void show(){
	for(User u : users){
	    System.out.println(u);
	}
    }
}
class Rei {
    public static void main(String[] arg){
	UserCollection uc = new UserCollection(
	new User[]{new User("07ec990","あそう"),
	     new User("07ec991","おざわ"),
	     new User("07ec992","ふくしま")});
	uc.show();
    }
}

イテレータデザインパターン

デザインパターンの中にイテレータと呼ばれるものがあります。 これは、オブジェクトの集まりを表すオブジェクトに対して、集められたオブ ジェクトをひとつずつ順番に指すようなオブジェクトを定義するものです。 今、クラス A の集まりとして、クラス B があり、クラス C がクラス B のイ テレータだとすると、次のような定義になります。


class A { // 要素
...
}
class B { // A の集まり
  public C iterator(){...}
}
class C { // B のイテレータ
  public boolean hasNext(){...}
  public A next(){...}
}

ここで、 next() は、 B からひとつ A の要素を取り、次の要素に進めます。 hasNext() は指している要素が無ければ false になります。 これらが定義されていると次のようなプログラムで B を処理できます。


B b = new B();
... // b へ A の要素を追加する。
C c = b.iterator();
while(c.hasNext()){
  A a = c.next(); // 登録した要素を次々取り出す
  ... // a の処理
}

実は、 Java のクラスライブラリに含まれる java.util.Collection は、この iterator を持っています。 java.util.Iterator というクラス(interface)が既に有り、各 Collection の クラスで実装されています。 なお、 Collection は Generics に対応していますが、 Iterator も Generics に対応しています。

Collection のクラスに対して、この Iterator を使って、各要素を出力する ような関数を定義すると次のようになります。

例5-5

import java.util.*;
class Rei {
    private static <E> void print(Collection<E> c){
	Iterator<E> i = c.iterator();
	while(i.hasNext()){
	    E e = i.next();
	    System.out.println(e);
	}
    }
    public static void main(String[] arg){
	Integer[] ai = {1,2,3};
	List<Integer> a = Arrays.asList(ai);
	String[] as = {"abc","xyz","def", "abc"};
	HashSet<String> h = new HashSet<String>(Arrays.asList(as));
	print(a);
	print(h);
    }
}

さらに、 iterator メソッドを持つクラスが interface java.util.Iterable を implements していると宣言すると、 for each 構文を使えるようになりま す。 なお、 java.util.Iterator を実装するには、 hasNext と next と remove を実装しなければなりません。 したがって、上記の A,B,C の例では次のようになります。


class A {
...
}
class B implements java.util.Iterable<A>{
  public C iterator(){...}
}
class C implements java.util.Iterator<A>{
  public boolean hasNext(){...}
  public A next(){...}
  public void remove(){...}
}
class Rei {
  public static void main(String[] arg){
...
    for(A a: B){ // クラス C は明示されないが暗黙に使われる
      System.out.println(a);
    }
...
  }
}
例5-6

import java.util.*;
class User {
    private String id;
    private String name;
    public User(String id, String name){
	this.id = id;
	this.name = name;
    }
    @Override public String toString(){
	return id+": "+name;
    }
}
class UserCollection implements Iterable<User>{
    private User[] users;
    public UserCollection(User[] users){
	this.users = users;
    }
    public UserIterator iterator(){
	return new UserIterator(users);
    }
}
class UserIterator implements Iterator<User> {
    private User[] users;
    private int index;
    UserIterator(User[] users){ 
	// コンストラクタは UserCollection だけに使わせたい
	// そのため private, protected, public のどれでもない
	this.users = users;
	index = 0;
    }
    public boolean hasNext(){
	return index < users.length;
    }
    public User next(){
	return users[index++];
    }
    public void remove(){} //ダミー
}
class Rei {
    private static <E> void print(Iterable<E> c){
	for(E e : c){
	    System.out.println(e);
	}
    }
    public static void main(String[] arg){
	UserCollection uc = new UserCollection(
             new User[]{new User("07ec990","あそう"),
	                new User("07ec991","おざわ"),
	                new User("07ec992","ふくしま")});
	print(uc);
    }
}

5-3. 演習問題

演習5-1

組合せの数の関数をテストするプログラムを作りなさい。 関数のシグネチャは次の通りです。


private static int combination(int n, int m)

テストは複数の入力を与え、あらかじめ計算しておいた値と全て一致すれば Ok を、どれかひとつでも異なれば NG を画面に表示するものとします。 そして、以下の関数に対して、それぞれテストを行いなさい。

  1. 
    private static int combination(int n, int m){
      return factorial(n)/factorial(m)/factorial(n-m);
    }
    private static int factorial(int n){
      int res = 1;
      for(int i=2; i<=n; i++){
        res*=i;
      }
      return res;
    }
    
  2. 
    private static int combination(int n, int m){
      if(m==0) return 1;
      if(n==m) return 1;
      return combination(n-1,m-1)+combination(n-1,m);
    }
    
  3. 
    private static int combination(int n, int m){
      final int[][] c = {{1, 1, 1 }, {1, 1, 1}, {1, 2, 2 }};
      if(n < c.length){
        if(m<c[n].length){
          return c[n][m];
        }
      }
      return 1;
    }
    

演習5-2

次の仕様を満たすクラスを設計しなさい。

売上げデータは商品の情報と売上げ数を含むものである。 商品には商品コードと商品名と単価が与えられている。 この時、商品コードを指定すると売上げデータから売上げ数が得られる。 また、売上げデータの総売上額を求めることができる。

ヒント

「売上げ」でクラスを作り、「売上げデータ」はその売上げの集まりのクラス とすると良い。 また売上げごとに商品を格納する手もあるが、「商品」の集まりのクラス 「商品データ」を作って、商品コードから商品の情報が得られるようにする手 もある。

演習5-3

上限値を設定して、 iterator を得ると 1 から n まで順に生成するような Generator と GeneratorIterator クラスを作りなさい。 さらに、次のテストプログラムと結合し動作を確認しなさい。


import java.util.*;
class Generator implements  Iterable<Integer> {
  public Generator(int max){
  ...
  }
  public GeneratorIterator iterator(){
  ...
  }
}
class GeneratorIterator implements Iteartor<Integer> {
...
}
class Ex {
  public static void main(String[] arg){
    Generator g = new Generator(10);
    for(Integer i : g){
      System.out.println(i);
    }
  }
}

出力

1
2
3
4
5
6
7
8
9
10

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科