このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
この講義ではオブジェクト指向言語 Java のデータ構造であるオブジェクトに 焦点をあて、オブジェクト指向におけるデータ構造とアルゴリズムについて学 びます。 特に、継承という仕組みを重点的に学びます。継承を理解せずにこの科目を習 得することは難しいでしょう。 前半はオブジェクト指向の基礎と、継承を学びます。 後半はコレクションクラスライブラリと数学的な補足を行います。
期末試験は行わず、評価はレポート二通で行います。
なお、大学院を他大学で学びたい人は、2007年までのデータ構造とアルゴリズム の方が入学試験範囲をカバーしていると思われます。 この講義の後半部分とデータ構造とアルゴリズム II の一部が若干重複してい ますが、ほぼ関連はありません。 したがって、他大学大学院をめざすには 2007年の講義資料も学習すると良いでしょう。
この講義を終えた後、引き続いて「データ構造とアルゴリズム II」という選 択科目が始まります。 内容は「作りたいプログラムをどうやって作るか?」をテーマにし、 前半は「ルールとプログラム」の関係に関する理論を、後半は XML というデー タ構造と処理をテーマに学びます。
本講義では最新版の Java の開発環境と、ドキュメントをもっとも重視します。 http://java.sun.com/で、 SUN の SDK のインストールと、ドキュメントのダウンロードは忘れずに行っ て下さい。 Eclipse に付属してくる Java のコンパイラではコンパイルできないものがあ るかもしれません。
この講義では Java 言語を主に使用しますが、対比のために C 言語も多少取 り上げます。 本講義を受講すると C 言語がどのような機能を持つ言語かは分かるようにな りますが、昨年までの講義と違い C 言語のプログラミングテクニックを多く 説明したりはしません。
Java に関しても、本講義で重要と思われることのみを説明します。 つまり、 Java の全ての機能を説明するわけではありません。 機能の一部やクラスライブラリの一部しか紹介しません。 つまり、この講義では Web アプリケーションや、データベースとのやりとり などは行いません。 これらに関しては対応する講義で学んでください。
講義は座学が中心で、演習などは行いません。 各自、サンプルプログラムや例題などを実際に動かしたりして、動作を確認す る復習を怠らないように願います。 又、参考文献に挙げた書籍を読むのも良いでしょう。 但し、 Java は Version 5 から大きく変わり、プログラミングテクニックも 変化しています。 しかし、それに対応した書籍は少ないです。 Version 1.4 以前の書籍を読む場合は、サンプルプログラムのコンパイル時に ワーニングすら出る場合がありますので、書籍を選ぶ際は十分に気をつけてく ださい。
レポート課題は例年通り、講義の資料にある演習問題を組み合わせたような 内容を出題します。
この講義は勉強せずに単位を取ることは難しいです。 他人に頼らずプログラムを書けるようになるよう目指してください。
大規模なコンピュータプログラムは、多人数で別々に作られたり、複数の工程 で作られたりと、必ず分割して段階的に作成されます。 今回ははじめにこのプログラムの分割法を学びます。
プログラミングに必要な最低限の知識とは何でしょうか? 実は、あらゆるプログラムは次の三要素の組み合わせで作れることが分かって います。
このあらゆるプログラムが作れるという性質をTuring Completeと 言います。
これらの要素は既に 1 年次で学んだことです。 つまり、プログラムを書く上で最低限の学習は済んでいます。 これ以上のプログラミングテクニックは「より良いプログラムを書く」ための ものです。
この講義におけるプログラミング手法を学ぶ目的は、複雑なプログラムを組む ためと、大量のデータを扱えるようになることです。 講義の前半は複雑なプログラムを扱うため、プログラムに構造を与えて分割す る方法を学びます。
例えば、ある小説に対して、その小説の続編を書くことを考えましょう。 すると、小説すべてを読まないと、続編を書くことが難しいことがわかります。
一方でWindows Vista は数千万行のプログラムでできていると言われています。 これが一本のプログラムでできているとすると、修正、拡張、改良などを行う にはすべてを把握しなければならず、とても困難な作業になります。 しかし、 実際には Windows のプログラムを作る際に Windows そのもののプログラムを すべて読んだりはしません。 さらに、 Java では一旦プログラムを組むと、Windows で動くばかりでなく、 MacOSX や Java 対応の携帯電話など、全く異なる装置や搭載されている OS の異な るコンピュータで、同じプログラムが動作します。
この違いはなにかと言うと、小説は先頭から最後まで一直線に読むということ のみを想定して書かれているのに対して、コンピュータのプログラムは小さな プログラムの部品からなっていて、それらの部品を組み合わせて一つのプログ ラムができているからです。 しかも、それらの部品は続きが書いてあるわけではありません。 それぞれが、一つずつ抽象的な意味を持つ部品であり、抽象的な意味における 関係により結びついています。 これは、例えば、異なる装置が接続されているコンピュータに対してでも、 「画面に字を書く」、「キー入力」などの同等の操作で、同じように使用でき るようなプログラムがそれぞれに用意されています。 そして、さらに Java ではその操作の組合せだけでプログラムを組むことがで きます。
なお、ここで言う「抽象」とは具体的な手続きの組み合わせを、意味のある一 つの名前で表現することです。 手続きの集まりに「A」とか「山田」とか「田中」とか名前をつけても抽象化 とは言いません。 「文字表示」「ファイル出力」など手続き全体の機能を簡潔に表すような名前 が付いているとき、抽象的な部品であると言えます。
この考え方の重要なポイントは次の通りです。 コンピュータは CPU にキーボードや画面などの I/O が接続されていますが、 これらを制御するには「メモリ XXXX 番地に、メモリ YYYY 番地の 値をコピーする」というような操作をします。 この操作はコンピュータの設計毎に異なりますので、機種ごとに異なって当然 です。 例えば画面に点を打つというような操作を行うためのメモリの番地は 機種ごとに異なります。 しかし、ほとんどのアプリケーションプログラムでは画面の解像度やフォント の美しさなどという機種毎の性能を追求するよりは、機種間の相違を吸収して 「文字列を画面に書く」というような抽象的な概念で操作したい 場合がほとんどです。 そのため、画面に点を打つような機種毎に異なる装置の操作は、その機種専用 のプログラムを用意し(これをデバイスドライバと呼びます)、そ れを利用して画面に文字を出すようなプログラムをさらに共通に用意し(これ をアプリケーションプログラムインターフェイス、 APIと呼びます)、アプリケーションプログラムを組む際はこの API を利用してプログラムを書きます。 API の例としては C 言語の printf などがあります。 printf を C 言語から呼び出すと、どんな解像度のコンピュータでもコンソー ル画面と呼ばれる領域に文字列を出力することができます。 このコンソール領域に目を近づけて見ると、文字列自体はすべて点の集まりで あることがわかります。
コンピュータプログラムは人間が作るもので、人間のために、人間が使うよう に作られます。 そのため、プログラムが指示する動作自体は人間の思考が記述されています。 そのため、プログラミング言語は人間の考え易いように抽象化の方法が備わっ ています。 人間の思考には「複数の事柄に一つの名前を付ける」などの抽象化が行われま すが、プログラミング言語でも複数のことがらをまとめて一つの名前で取り扱 う仕組が用意されています。 この機能を使って書かれている長いプログラムは、単に長いのではなくて、様々 な抽象的な事柄が事柄毎に分割されて記述されています。
さて、この講義の当初の目的はプログラムを分割し、抽象化して書く手法を紹 介することです。 分割をする目的は、分割したプログラム同士が抽象的な意味を持ち、互いのプ ログラムの細部をすべて読まなくても、機能を理解できて相互に活用できるよ うにすることです。
多くのプログラミング言語が実現しているプログラムの構造として、 サブルーチンや関数と呼ばれるプログラムの分割手法 があります。 これは、大きなプログラムのうち、まとまった処理を抽象化し、名前で呼び出 すものです。 関数とは処理に必要な入力値に対して、処理の結果得られる出力値を計算す るという部分を切り出したものです。
なお、関数には名前を付けますが、この名前に関しては何をするものか具体的 に分かるように比較的長い名前を付ける流儀があります。
関数の例を示すため、 n 個のものから m 個のものを 取り出す組合せの数を考えます。この組合せの数は次の式で表せます。
これを素朴に定義通り計算することを考えます。つまり、階乗を求め、その階 乗を使って組合せの数の計算をします。 下記のようなプログラムを作成したいという意図ではありません。
#include <stdio.h>
main(){
int n,m;
int i;
int f1, f2, f3;
printf("Input n and m: ");
scanf("%d%d",&n,&m);
f1=1;
for(i=2;i<=n;i++){
f1*=i;
}
f2=1;
for(i=2;i<=m;i++){
f2*=i;
}
f3=1;
for(i=2;i<=n-m;i++){
f3*=i;
}
printf("%dC%d = %d\n",n,m,f1/f2/f3);
}
このプログラムでは階乗を 3 回にわたって似たようなプログラムにより計算 しています。 データの流れとしては確かに階乗を計算して、組合せの数を計算していること になっていますが、プログラム的には「階乗を計算する部分」や、「組合せの 数を計算する部分」といった、人間の思考に相当する部分には分割できていま せん。 今回の目標はこのようなプログラムではなく、「階乗を計算する部分」「組合 せの数を計算する部分」「入力値にしたがって、計算値を出力する部分」に分 割することです。
プログラムの分割法として、C 言語では関数という仕組みがあります。 これを使うことにより、この計算をするのに階乗を計算してから最終的な値を 求めることができます。 プログラムの構成は、階乗を計算する関数、その関数を元に組合せの数を求め る関数、さらに入力値にしたがって、計算値を出力する関数の 3 つの部分に 分割できます。
ここでは階乗を表す関数を factorial(n) と表すことにしましょう。 この関数は整数 n 与えられると、 n! の値となる整数を一つ返します。 C 言語で関数を使用するには、このように、数学と同様に、入力する値を丸カッ コの中に入れます。 そして、得られた値は sin(x) などの数学の関数と同じように数式内で使うこと ができます。
factorial(n) の定義は、次のように行います。
int factorial(int n){
n から factorial の値を計算する仕方
return 求めた値;
}
最初の int は出力される値の型を表し、カッコの中の int は入力され る値の型を表しています。 factorial の計算法の中では入力された値は n を使っ て計算します。
さらに、組合せの数の計算も関数で書くと次のようになります。
int combination(int n, int m){
n と m と factorial 関数を使った組合せの数の計算の仕方
return 求めた値;
}
プログラムをまとめて書くと次のようになります。
#include <stdio.h>
int factorial(int n){
factorial の計算法
return 求めた値;
}
int combination(int n, int m){
factorial 関数を使った組合せの数の計算
return 求めた値;
}
main(){
int n,m;
printf("Input n and m: ");
scanf("%d%d",&n,&m);
printf("%dC%d = %d\n",n,m,combination(n,m));
}
このようにプログラムを 3 分割できました。 main() は combination(n,m) を呼び出し、 combination(n,m) は factorial(n) を呼び出します。 このようにすると、factorial や combination や main をそれぞれ別々 に作ることができますし、場合によっては複数の人間で作ることもできます。 また、それぞれの関数をテストすることも可能になります。
なお、今までプログラムを実行させるためにおまじないとして
main()
というものを書いて来ました。
しかし、実は C 言語ではプログラムは
全て関数の形で書きます。つまり main()
も実は関数の一種で
す。
main という名前の関数はプログラム実行時に呼び出されると言う特別な関数
です。
なぜこの記述で main 関数が定義されるかですが、次の二つの省略のルールが
あるからです。
この、出力の型を省略することは、本来は値を返すべき定義であることを隠
してしまうので良い記述法ではありません(が、記述がシンプルになるという
利点はあります)。
さて、main 関数は int 型であると決まっているので、本来は値を返す必要が
あります。
値はプログラムが正常に終了したか、異常に終了したかを返します。
そのために return 文を書く必要があります。
この、正常終了、異常終了を表す値は stdlib.h に登録されています。
正常に main 関数を終了するには EXIT_SUCCESS, 異常終了するには
EXIT_FAILURE を返します。
但し、EXIT_SUCCESS の代わりに 0 も使って良いので、 stdlib.h を読み込ん
で return EXIT_SUCCESS;
とする代わりに、単純に
return 0;
と返しても良いです。
なお、この返した値は OS から参照できます。
Windows の場合 ERROR_LEVEL 環境変数に値が渡されます。
ここで、もし、 return を記述しないと値は不定になります。
int main(){
return 5;
}
このプログラムをコンパイルし、下記のように実行すると 5 が出力されます。
c:\work> .\a.exe
c:\work> echo %ERROR_LEVEL%
5
c:\work>
次に、 2 の説明をします。 C++ や Java では引数がない場合は何も書かないのが流儀ですが、 C 言語の 場合は、何も書かないと古い関数宣言の流儀と互換性がなくなります。 そのため、通常の関数で引数なしを示すには void を記述すべきです。
以上をまとめると、以下の 1 のように記述していた main 関数は、今後、 2 のように記述するよう改めるべきです。
main(){
/* プログラム */
}
int main(void){
/* プログラム */
return 0;
}
なお、この他に C で作ったプログラムを起動する際に、 コマンドプロンプトから複数の文字列を引数として与えることがで きます。 その場合、main 関数は次のように記述します。
int main(int argc, char *argv[]){
/* プログラム */
return 0;
}
argc には文字列の数が入ります。また、 argv は文字列の配列になります。 なお、 argv[0] は起動されたプログラムの名前(a.exe など)を指します。
つまり、 C 言語では main関数の引数にはこのように void のみと、 int, char *[] の組の二種類の宣言の仕方があります。
以下は例に対して動作するようにプログラムを作成したものです。
#include <stdio.h>
int factorial(int n){
int result,i;
result=1;
for(i=2; i<=n; i++){
result*=i;
}
return result;
}
int combination(int n, int m){
return factorial(n)/factorial(m)/factorial(n-m);
}
int main(void){
int n,m;
printf("Input n and m: ");
scanf("%d%d",&n,&m);
printf("%dC%d = %d\n",n,m,combination(n,m));
return 0;
}
さて、次に Java での関数の記述法についてついて学びます。 基本的には Java の文法は C 言語と同等です。 しかし、 Java では関数などあらゆるものは class に属していないといけません。 また、 main 関数自体が static 宣言されているので、 main か ら呼び出される関数は static 宣言がされてなければなりません。
また、 Java では関数にアクセス修飾子を付ける必要があります。 アクセス修飾子には次のような種類があります。
アクセス修飾子 | 効能 |
---|---|
private | 同一クラス内のみでアクセス可能 |
protected | 同一クラス、派生クラスのみでアクセス可能 |
なし | 同一パッケージ(同じフォルダ内)のみでアクセス可能 |
public | どこからでもアクセス可能 |
ここで、どのアクセス修飾子を選ぶかですが、 先に述べたように、プログラムのすべてを読まずにプログラムを作っていくに は、各部分部分が抽象化されて一まとまりの部品になり、なるべく余計なもの が見えなくなる必要があります。 そのため、同一クラス内で使うだけの関数は、外部に見えないようにするべきです。 そのため、このアクセスはできるだけ厳しく、つまり基本的には private 修飾をするようにします。
class Rei1 {
private static int factorial(int n){
factorial の計算法
return 求めた値;
}
private static int combination(int n, int m){
factorial 関数を使った組合せの数の計算
return 求めた値;
}
public static void main(String[] arg){
int n,m;
n と m の入力
System.out.println(n+"C"+m+"= "+combination(n,m));
}
}
Java プログラムでは「 java クラス名」で起動させるとクラスに所属してい る public static 修飾された main 関数が実行されます。 main 関数の引数は java.lang.String の配列型のみになります。 また、main は C 言語と違い OS に何も返さないので、引数の宣言は void 型 になります。
static 修飾されている関数から別の関数を呼ぶには static 修飾されている 必要があります。 したがって、 main から combination を呼ぶために combination も static 修飾、 combination から factorial を呼ぶために factorial も static 修 飾をしなければなりません。
さて、Java でプログラムを動作させるには、 C 言語のように変数宣言が実行文の前 に無いとならないという制約がありません。 そのため使う直前に変数宣言をした方がプログラムがみやすくなります。
Java で C 言語の scanf のような働きをするものとして、 1.5 から追加され た java.util.Scanner クラスがあります。 標準入力である java.lang.System.in オブジェクトを渡してオブジェクトを 作成することにより、文字列、数値などを順に取り込むことができます。 下記の様にして使用します。
java.util.Scanner sc = new java.util.Scanner(System.in);
このようにすると、変数 br は next() メソッドで文字列、 nextInt() メソッ ドで整数、 nextDouble() で実数などを読み込むことができます。
int n = sc.nextInt();
入力された値を保持する変数を表す意味で、入力時に変数宣言するようにしていま す。
以下はこれまで述べた説明を元に動作するように作ったプログラムです。
class Rei1 {
private static int factorial(int n){
int result=1;
for(int i=2; i<=n; i++){
result*=i;
}
return result;
}
private static int combination(int n, int m){
return factorial(n)/factorial(m)/factorial(n-m);
}
public static void main(String[] arg) throws java.io.IOException {
java.util.Scanner sc = new java.util.Scanner(java.lang.System.in);
System.out.print("Input n: ");
int n = sc.nextInt();
System.out.print("Input m: ");
int m = sc.nextInt();
System.out.println(n+"C"+m+"= "+combination(n,m));
}
}
Java には標準で様々な関数が提供されています。 例えば、数学関数は java.lang.Math クラスに定義されています。 平方根は次のように定義されています。
package java.lang;
public class Math {
public static double sqrt(double x){...}
}
これを呼び出すには java.lang.Math.sqrt(x) のように書くのが原則です。 但し、次のような場合はパッケージ名を省略できます。
そのため、平方根は Math.sqrt(x) とだけ書けば良いことになります。
また、import java.util.*;
とすることで、標準入力から数値
を読み込むためのオブジェクトの宣言を次のようにできます。
Scanner sc = new Scanner(System.in);
さらに、 Java 5 からは import static Math.sqrt;
とするこ
とで、平方根をsqrt のみで呼び出すこともできます。
これについては、データ構造とアルゴリズム II で用法を説明します。
プログラムを見やすく、保守しやすくするためのプログラムの書き換えを リファクタリング と言います。 ここでは関数を利用したリファクタリングの手法を紹介します。
長いプログラムはそのもの自体が一つの機能を表しているわけではなく、 ほとんどの場合いくつかの部分に分割できます。 その場合、プログラムを読みやすくするための手軽な方法として次の方法がと られることがあります。
これはプログラマーが性善説に基づいている場合は有効な手法ですが、仮に自 分一人でプログラムを書いていても、過去の自分に裏切られることがあります。 これはそれぞれ次のような理由によります。
int sum=0; //sum は合計を表してない
for(int i=1; i<=n ; i++){
sum+=i; //sum は合計を表してない
}
// ここで初めて sum は示すべき合計の値を持っている
そこで出てくるのが、たとえ一回しか使わなくても、プログラムの適当な部分 を取り出して、意味のある長い名前の関数にしてしまう方法です。 さらに、計算に使う変数が計算が終わるまで意図した値にならないことを避け るために、変数を使う代わりに関数を使用します。
例を示します。
class Rei2 {
public static void main(String[] arg){
int num=10;
int sum=0;
for(int i=1; i<=num ; i++){
sum+=i;
}
System.out.print("1 から "+num+"までの合計は");
System.out.println(sum);
}
}
これをリファクタリングすると次のようになります。
class Rei2 {
private static int sum(int n){
int s=0;
for(int i=1; i<=n ; i++){
s+=i;
}
return s;
}
public static void main(String[] arg){
int num=10;
System.out.print("1 から "+num+"までの合計は");
System.out.println(sum(num));
}
}
このようにすると main が短くなります。 さらに、入力、出力と計算法が main で混在していましたが、 sum(num) が num の合計を表すことが直感的に分かり 計算法を分離できました。 そして計算用変数 sum を main から取り除けます。 又、関数 sum は合計を計算するだけのプログラムです。 さらに、この中の変数 i や s は外部から決して影響を受けない、合計を求め るためだけに使われる変数であるということが、関数の中で宣言しているため に保証されます。 さて、この sum の部分は入力が n で出力が 1 から n までの合計を返すと いう仕様がはっきりしています。 そこで、他に効率が良い方法があれば入力と出力だけ合っていれば別の計算法 に置き換えても問題ありません。
private static int sum(int n){
return n*(n+1)/2;
}
関数に置き換えることで、計算途中の変数の保護、主プログラムからの変数の 削除などができます。 また、コメントや変数名と違い、計算内容にそぐわない誤った関数名によるプ ログラムは、間違ったアルゴリズムに読めます。 そのため、 誤った関数名を直そうという動機が保たれ、保守の優先順位は高くなるはずです。
関数を一回しか使わないとか、一行で終わるとかは関係ありません。 このようなリファクタリングはプログラムが読みやすくなるだけではなく、後 に説明するようにオブジェクト指向プログラミングにつながります。
今回はプログラムの計算部分を取り出して関数化し、 main から計算用の変数 を取り除く練習をします。
次のプログラムのうち、判別式を計算する関数
double discriminant(double x, double y, double z)
を作り、変数 d を無くしなさい。
class Enshu1 {
public static void main(String[] arg){
double a=1;b=2;c=3;
double d=b*b-4*a*c;
Sysmtem.out.print("方程式"+a+"x^2+"+b+"x+"+c+"=0");
System.out.println("の判別式は"+d);
}
}
次のプログラムで、判別式を計算する関数
double discriminant(double x, double y, double z)
を作り、変数 d を無くしたあと、
さらに、その関数を利用して実根判定をする関数
boolean hasRealRoots(double x, double y, double z)
を作り、 if 文の中の条件式を取り除きなさい。
class Enshu2 {
public static void main(String[] arg){
double a=1;b=2;c=3;
double d=b*b-4*a*c;
Sysmtem.out.print("方程式"+a+"x^2+"+b+"x+"+c+"=0");
if(d>=0){
System.out.println("は実根を持つ");
}else{
System.out.println("は実根を持たない");
}
}
}
次のプログラムで、判別式を計算する関数
double discriminant(double x, double y, double z)
を作り、変数 d を無くしたあと、
さらに、その関数を利用して実根の数を計算する関数
int numberOfRoots(double x, double y, double z)
を作り、変数 num を取り除きなさい。
class Enshu3 {
public static void main(String[] arg){
double a=1;b=2;c=3;
double d=b*b-4*a*c;
int num;
if(d>0){
num=2;
}else if(d==0){
num=1;
}else{
num=0;
}
Sysmtem.out.print("方程式"+a+"x^2+"+b+"x+"+c+"=0");
System.out.println("の実根の数は"+num);
}
}