このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
プログラミング言語にはいろいろな種類があります。 これらの書きやすさはいろいろありますが、 特定の処理をさせるのに関して、可能不可能に関してはほとんど差がありません。 オブジェクト指向言語で無くてもオブジェクト指向的なプログラムは組むこと ができます。 但し、手間やメリットが言語によって異なるだけです。
プログラミング言語的な側面で Java 言語を見ると次のような特徴があります。
静的型付き言語とは、コンパイル時に全ての変数の型が確定して いて、関数の呼出しや、式の解釈などで型のチェックを行うことのできるプロ グラミング言語です。 変数になんでも代入はできなくなりますが、型の不一致をコンパイル時に発見 することができるので、プログラミングミスを予防できる効果があります。 また、 Java ではクラスごとにコンパイルができます。 なお、現行の Java では Object 型を使い、ダウンキャストというコンパイル 時に型チェックできない仕組を使う必要があるので、必ずしも全ての型チェッ クがコンパイル時に終わるわけではありません。
パーソナルコンピュータが普及する前は、コンピュータのプログラムをコン ピュータで作成していたわけではありませんでした。 いわゆる「机上」というもので、多くの書類を作成しながらプログラムを紙に 書いて作成していました。
この時代に開発されたプログラムの開発手法が ウォーターフォールモデル と呼ばれる手法です。 これは、開発工程を例えば「設計」「製作」「テスト」に分割したとき、それ ぞれの工程に期間を別に割り当て、設計が終わったら製作、製作が終わったら テストという具合に開発を行う方法です。 この長所はテスト以外で実際のコンピュータが不要な点です。 しかし、最大の欠点としてはすべての工程が 100% 完璧にできないと、次の工 程が不可能なことと、開発中に設計の誤りが判明した場合、すべての工程をや り直さなければならないことです。 人間は誤りを犯すものですし、複雑なシステムは開発中に理解が進んで、より 良い設計を考案することもあります。 このウォーターフォールモデルは、このような観点では柔軟性が無く欠点の多い ものです。
ウォーターフォールモデルの欠点を補うには、これらの工程を繰り返し行い、 各工程の完成度や責任を軽減することです。 そのためには、少しずつ設計、製作し、テストを行った結果を設計にフィードバックしていき、 繰り返しこれらの工程を行ってシステムを作っていくことです。 このような開発方法を スパイラルモデル と言います。 スパイラルモデルの中で短期間で少人数で行う開発手法が アジャイル開発です。 アジャイル開発にはさまざまな具体的な手法がありますが、 XP(エクストリームプログラミング)はその中の一つの有名な手法です。
アジャイル開発は短期間で工程を繰り返すのが特徴です。 この繰り返しの動機付けを行うものを「駆動」と呼びます。 例えば、「ユーザ機能駆動」とは、ユーザを開発工程に組み込み、 開発の繰り返しの動機付けを行わさせるものです。 そのためには次のように開発を行います。
この他に、ユーザを直接組み入れなくても、ソフトウェアのマニュアルを先に 作ってマニュアルを満足させるように開発を行う手法もあります。
また、「テスト駆動」や「テストファースト」と呼ばれる開発手法もあります。 これは、ソフトウェアの製作を行う前に、自動テストプログラムを作成すると いう開発手法です。 テストファーストにより、次のような利点が生じます。
テストは、考えうるすべての入力ケースを生成するのではなく、エラーが生じ そうな入力のみを生成するように作ります。 このテスト駆動は作業を区分化でき仕様が明確になります。さらに仕様変更が 生じた場合も、テストを書き換えた後は通常の開発作業と変わらない作業にな るため柔軟に対応できます。
ケントベックが開発した xUnit はプログラムのユニットテストを自動化する プログラムライブラリーです。 Java 用のライブラリは JUnit と呼ばれ、 http://www.junit.org/で配布されて います。 しかし、 Eclipse では特に何の設定もせずに、標準で使用可能です。
JUnit は ver.3 と ver.4 があります。 ver.3 は広く使われてましたが制約が大きいので、ここでは ver.4 を紹介し ます。
特定のクラス A のメソッド bと c をテストすることを考えます。 サンプルとして次を考えます。
class A {
public A(){}
public int b(int x){ return x+1; }
public boolean c(int x){ return x==1;}
}
このクラスを Eclipse で選択して、「New」→「JUnit Test Case」を選択す ると、テストクラスを作成するウィザードが表示されます。 「New JUnit 4 Test」を指定し、「setUP」を選択して Next を押します。 そして、次の画面でテストしたいメソッドを選択します。 すると、Eclipse に含まれている JUnit のライブラリを使うかのメッセージ 「Add JUnit 4 library to the build path」のメッセージが出ますので、承 認します。
すると、次のコードを生成します。
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class ATest {
@Before
public void setUp() throws Exception {
}
@Test
public void testB() {
fail("Not yet implemented");
}
@Test
public void testC() {
fail("Not yet implemented");
}
}
まず、テストクラスの名前は「テストするクラス名+Test」になります。 また、各メソッドのテストメソッドは 「test+メソッド名(先頭は大文字)」で す。 但し、これは ver.3 のルールで、 ver.4 では特に従う必要はありません。 ver.4 ではスタティックインポートとアノテーションでコントロールをします。 この場合、前処理のメソッドは @Before を付け、テストメソッドは @Test を 付けると自動的にテストが順番に行われます。 この段階でこのクラスを JUnit として実行すると、JUnit 用の専用のテスト 集計画面が出て、上に結果、下にメッセージが表示されます。 この場合、テストが失敗したことを表す赤いバーが出ます。 そして、下には「java.lang.Assertion Error: Not yet implemented」という メッセージが出ています。 これは上のプログラムの fail() という必ずテストが失敗するメソッドの中の メッセージが表示されています。
テスト作成の基本は setUp でメンバ変数にインスタンスを生成し、各テスト メソッドで値を入れて、チェックします。 各テストメソッドでは org.junit.Assert の 各 メソッド を使用してテストの成否を決めます。 値のチェックは assertEquals(目標値, 式) とします。 なお、 assertEquals(true, 式) は assertTrue(式)、 assertEquals(false, 式) は assertFalse(式) を使えます。 また、equals ではなく、 == で比較したい場合は assertSame と assertNotSame を使用します。
上記のサンプルクラスのテストは次のように作成します。
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class ATest {
private A a;
@Before
public void setUp() throws Exception {
a = new A();
}
@Test
public void testB() {
assertEquals(2,a.b(1));
assertEquals(11,a.b(10));
}
@Test
public void testC() {
assertFalse(a.c(0));
assertTrue(a.c(1));
assertFalse(a.c(2));
}
}
テストは基本的に値を入れて取り出して確認するだけです。 必要最低限のテストを用意すべきですが、少なすぎると、テストを通っても使 用できないプログラムが出来上がる可能性があります。
なお、このテストプログラムを先に作成して、テストが通るようにプログラム を作成することを「テストファースト」と言います。
例外をテストする場合は次のようにします。 下記は例外を発生するサンプルプログラムです。
class ExceptionSample {
public ExceptionSample() {
super();
}
public void run(){
throw new RuntimeException();
}
}
これの run メソッドをテストするには次のようにします。
import static org.junit.Assert.*;
import org.junit.Test;
public class ExceptionSampleTest {
@Test(expected=java.lang.RuntimeException.class)
public final void testRun() {
ExceptionSample e = new ExceptionSample();
e.run();
}
}
つまり、例外が発生することをテストするメソッドでは、@Testの後に発生す
る例外を (expected=例外名.class)
で指定します。
すると、テスト時に、メソッドが終了する前に例外が発生すればテストが成功
します。
Eclipse ではテストは自動化されていましたが、コマンドプロンプトで手動でテス トを行うこともできます。
まずjunitのjarファイルを取得します。 http://www.junit.org/から、ダウン ロードします。 仮に、ダウンロードしたファイルを junit-4.8.2.jar だとします。 これを、プロジェクトのあるディレクトリに置いておきます。
テストのプログラムは次のようにコンパイルします。
つまり、クラスパスにjunitのjarファイルを加えれば良いです。 また、固定したディレクトリに保存したのち、環境変数 CLASSPATH に設定し ておけばコンパイル時に省略できます。
テストの起動は次のようにします。
main メソッドは JUnitCore にあり、実行したいクラスは引数で指定しなけれ ばなりません。
実行すると次のような表示になります。
c:\work>java -classpath .;junit-4.8.2.jar org.junit.runner.JUnitCore ExceptionSampleTest JUnit ver 4.8.2 . Time: 0.004 OK (1 Test) c:\work>
これを簡便にするために、次のようなファイルを用意しておくと良いです。
@echo off
set JUNIT=junit-4.8.2.jar
javac -classpath .;%JUNIT% %1
@echo off
set JUNIT=junit-4.8.2.jar
java -classpath .;%JUNIT% org.junit.runner.JUnitCore %1
このようにしておくと、
Linux 系では JUnit はパッケージとしてインストールできる場合があります。 ここでは ubuntu 10.10 でのテスト方法を示します。
ubuntu では、
テストのプログラムは次のようにコンパイルします。
Windows ではセミコロンだったのが、コロンになっていることに注意。
テストの起動は次のようにします。
classpath に junit4.jar の他に、hamcrest-core.jar を指定しなければ動き ませんでした。
オブジェクト指向のプログラミングではクラスを作るのが重要な作業になりま す。 ここでは様々なクラスの作成法を紹介します。
オブジェクト指向型のプログラミング言語の特徴はカプセル化できる点です。 これはプログラムの部分を変更しても他の部分への影響が少ないという利点が あるため、プログラムを修正しながら作成するアジャイル開発に適しています。
オブジェクト指向言語でプログラムを作る場合は、自分の癖で自由に作り、その 後、リファクタリングしても他のプログラミング言語ほど修正が困難ではあり ません。
リファクタリングで目指す目標は次の点です。
これは以下の根拠に基づきます。
実は前回の 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 を指定した型でキャストしたのが 上記のプログラムです。
このプログラムをリファクタリングします。 まず着目するのは、複数の静的関数の引数が全て同じで冗長であることです。 そこで、この引数を持っている静的関数をまとめてひとつのクラス 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 修飾を機械的に消 していきます。 このような、クラスの追加、関数のクラス間の移動のような大幅かつ機械的な 変更の後でも、プログラムの動作やテストは容易です。
関数の大幅な移動を行った後、体裁を整えるために微調整を行います。
まず、 main で実際に Test のオブジェクトを起動するのに test.test() と いう表現をつかってます。これは字面的に意味が不明です。 実際はもともと test というのを動詞として使っていたわけですが、 Collection のインスタンスと int[] の配列をひとまとまりにして、さまざま なテストメニューを集めたものを名詞の Test というクラスにしたわけです。 したがって、名詞 test に対してふさわしい動詞を選ぶことにします。 do でも良いのですが、いちおう examine という単語を選びました。 これは意味が通ればどんな動詞でも構いません。 なるべくふさわしい名前を選びましょう。
また、main では Collection の配列を用意していましたが、あらかじめ Test の配列を作り、初期化を行うことにします。 そうすると、 Collection の配列を作って、それに応じて Test のオブジェクトを作ってという二段階の手間だったものを一段階にすることが できます。 なお、ここの判断では、配列の初期化などでトリッキーな構文が無ければこの ままでもよいのですが、このようにすることで Test の配列にすると奇妙な構 文を避けることができます。
また、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
}
}
}
大分すっきりしましたが、 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(Class-Responsibility-Collaborator)クラス分析はプログラムの仕様書か らクラスを設計する手法です。 始めにプログラムの仕様書と CRC カードを用意します。
CRC カードとは次のような書式のカードです。
Class | (クラス名) |
---|---|
Responsibility(責務) | Collaborator(協調クラス) |
責務の列挙 | 責務の対象となる他のクラス |
... | ... |
CRC 分析ではまず、プログラムの設計書を日本語などの自然言語で作成します。 すると、日本語の文章には必ず文の要素として主語、目的語などの名詞句と、 動詞が現れます。 設計書から名詞を抜き出し、クラスの仮候補として CRC(Class Responsibility Collaborator) カードを作ります。 そして、その名詞に関与する動詞を責務に列挙し、また目的語である名詞を対 象となるクラスに指定します。
次はプログラムの設計書の例です。
ArrayList, LinkedList, HashSet, TreeSet に対して、速度の比較のテストを 行う。 そのため、それぞれのオブジェクトに対して、ダミーの要素を追加、検索、削 除を行う。 そして、それぞれの所要時間を計り表示する。
この設計書において名詞 と動詞を分析すると次のようになります。
ArrayList, LinkedList, HashSet, TreeSet に対して、速度 の比較のテスト を行う。 そのため、それぞれのオブジェクトに対して、ダミーの要素を追加、検索、削除を行う。 そして、それぞれの所要時間を 計り表示する。
クラス名 | ArrayList |
---|---|
責務 | 協調クラス |
追加 | ダミーの要素 |
検索 | ダミーの要素 |
削除 | ダミーの要素 |
クラス名 | LinkedList |
---|---|
責務 | 協調クラス |
追加 | ダミーの要素 |
検索 | ダミーの要素 |
削除 | ダミーの要素 |
クラス名 | HashSet |
---|---|
責務 | 協調クラス |
追加 | ダミーの要素 |
検索 | ダミーの要素 |
削除 | ダミーの要素 |
クラス名 | TreeSet |
---|---|
責務 | 協調クラス |
追加 | ダミーの要素 |
検索 | ダミーの要素 |
削除 | ダミーの要素 |
クラス名 | 速度 |
---|---|
責務 | 協調クラス |
比較 | 速度 |
クラス名 | テスト |
---|---|
責務 | 協調クラス |
行う | |
追加 | それぞれのオブジェクト、ダミーの要素 |
検索 | それぞれのオブジェクト、ダミーの要素 |
削除 | それぞれのオブジェクト、ダミーの要素 |
計測と表示 | それぞれ(それぞれのオブジェクトへの追加検索 削除)、所要時間 |
クラス名 | ダミーの要素 |
---|---|
責務 | 協調クラス |
クラス名 | 所要時間 |
---|---|
責務 | 協調クラス |
計測 | |
表示する |
このようにして作成したカードに対して、ウォークスルーを行い ます。 ウォークスルーとはこれらのうち、どれが適切なクラスとしてふさわしいかを 検討する会議のことです。
例えばこの例は、速度と所要時間というクラスの役割が重複しています。 そこでカードの内容を見ると、速度には比較がありますが、テストの場合、通 常比較自体は人間が行います。 つまりコンピュータは所要時間を列挙して、それを人間が見て速度を比較する ものです。 ですから、ソフトウェアの機能としては所要時間の表示のみを行えば、人間が 速度を比較できます。 したがって、速度のクラスは作成しないことにします。
一方、テストの中で、「行う」と「計測と表示」がうまく整理されていません。 実際にはテストを行うということが、それぞれのオブジェクトにダミーの要素 を追加、検索、削除を行い、その所用時間を計測表示するということです。 したがって、このカードを次のように書き換えます。
クラス名 | テスト |
---|---|
責務 | 協調クラス |
行う(追加、検索、削除の所要時間の表示) | それぞれのオブジェクト、ダミーの要素、所要時間 |
追加 | それぞれのオブジェクト、ダミーの要素 |
検索 | それぞれのオブジェクト、ダミーの要素 |
削除 | それぞれのオブジェクト、ダミーの要素 |
オブジェクト指向の機能は 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 という本で解説されています。
一方、よく陥りやすく、やってはいけないオブジェクトの構成は アンチパターン として、実例の調査などにより研究されています。 これらをよく知ることにより、多くのプログラミングの局面で解決策となるオ ブジェクト設計の定石をあてはめられるようになります。
オブジェクト指向のプログラミングでは、従来の狭い意味でのデータ構造やア ルゴリズムの他に、このようにデザインパターンやアンチパターンなどを学ぶ 必要があります。
ここでは、オブジェクト指向の初歩ということで、もっとも基本的なデザイン パターンを紹介します。
オブジェクト指向言語では、自然な形で無い限り、関数やメソッドに多くの引 数が来ることはありません。 したがって、特定の関数やメソッドを作る際、それに渡す引数の塊が必要になっ た場合は、その引数の塊自体をひとつのオブジェクトとしてまとめることを検 討します。
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 で扱われよう と、大局的には関係ありません。 むしろ、個別のデータ構造はカプセル化されるべきです。 そのため、オブジェクトの集まり自体も、集まりとしての責務などを持ちますの で、それ自体が独立したクラスになるのは自然です。
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 を使って、各要素を出力する ような関数を定義すると次のようになります。
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);
}
...
}
}
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);
}
}
組合せの数の関数をテストするプログラムを作りなさい。 関数のシグネチャは次の通りです。
private static int combination(int n, int m)
テストは複数の入力を与え、あらかじめ計算しておいた値と全て一致すれば Ok を、どれかひとつでも異なれば NG を画面に表示するものとします。 そして、以下の関数に対して、それぞれテストを行いなさい。
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;
}
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);
}
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;
}
次の仕様を満たすクラスを設計しなさい。
売上げデータは商品の情報と売上げ数を含むものである。 商品には商品コードと商品名と単価が与えられている。 この時、商品コードを指定すると売上げデータから売上げ数が得られる。 また、売上げデータの総売上額を求めることができる。
「売上げ」でクラスを作り、「売上げデータ」はその売上げの集まりのクラス とすると良い。 また売上げごとに商品を格納する手もあるが、「商品」の集まりのクラス 「商品データ」を作って、商品コードから商品の情報が得られるようにする手 もある。
上限値を設定して、 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