「ソフトウェア演習(B)Java言語」
ベーシックコース

第3回 クラスの継承、クラスと関数、線形リスト、2分木等のデータ構造

今回は、クラスの継承をメインテーマとして述べます。 継承という概念は、オブジェクト指向言語に共通の重要な概念です。 一方、クラス自体は、変数や関数(コンストラクタを含む)からなります。 クラス内で定義される関数が、継承でどう扱われるかも重要ですが、 関数そのものを使いこなすことに慣れていることが重要です。 そこで、継承の概念や事例を見る前に、関数をどう定義するか、また、どう使うか、オブジェクトの生成とともに毎回生成される関数と、全体から共有されるstatic関数など、大きな違いを理解しておくことも重要です。 さらに、関数自体を抽象化し、interfaceとして利用する方法も、内容は高度に見えるかもしれませんが、SwingのようなGUIパッケージを扱う際に一気に覚えるかわりに、ちょっと上級的な作法を事前に垣間見ておきましょう。また、線形リストや二分木などの基本的なデータ構造も、Javaがそもそも有するArrayListTreeSetなどを使いはじめる前に、ゼロからでも作れる、ということもぜひ知っておきましょう。

Java言語での関数:static関数を中心に

Java言語では、C言語と同様に、関数を定義することができます。 ただし、Java言語では、 基本的にすべての関数はクラス内で定義するため、 main関数や他のクラスから呼び出すとき、 通常は関数を含むクラスのインスタンスを生成してから関数を呼び出すことになります。 例外は、平方根、対数関数、指数関数、三角関数などの頻繁に使用する数学関数です。 このように頻繁に使用する関数の場合は、 毎回インスタンスを生成するとメモリに無駄が生じますので、 Java言語では、直接アクセスできる仕組みを用意しています。 このような関数には、staticという修飾子がついています。 Java言語でのstatic関数static変数は、そのインスタンスがメモリ内にひとつだけ確保され、 関数の呼び出し側のすべてのクラスから共有できることを意味します。 上述の数学関数は、java.langパッケージのMathクラスに含まれています。 通常は、このjava.langのようなパッケージ名を含めてピリオドで繋げた関数名を指定することで、 static関数を呼び出すことができます。 ただし、java.lang以下にあるクラスだけは、 デフォルトでパッケージ名をつけることなく呼び出せますので、クラス名から、 Math.sqrt, Math.log, Math.exp, Math.sin Math.cosなどのように 呼び出すことができます。 また、三角関数のときに必要となる $ \pi $(パイ)は、 Math.PIの名前で使用することができます。 その他の数学関数は Math クラスのマニュアルを参照してください。 以下では代表的な数学関数を表にしておきます。

関数名(定数名) 関数(定数) 意味
円周率 Math.PI 円周とその直径の比πにもっとも近い double 値
自然対数の底 Math.E 自然対数の底 e (ネイピア数)にもっとも近い double 値
正弦関数 Math.sin(double x) 指定された角度(ラジアン)の正弦 (サイン)
余弦関数 Math.cos(double x) 指定された角度(ラジアン)の余弦 (コサイン)
正接関数 Math.tan(double x) 指定された角度(ラジアン)の正接 (タンジェント)
逆正弦関数 Math.asin(double t) 値がtとなる正弦の角度(ラジアン)を-π/2から+π/2の間の角度
逆余弦関数 Math.acos(double t) 値がtとなる余弦の角度(ラジアン)を0から+πの間の角度
逆正接関数 Math.atan2(double x, double y) 極座標 (r, θ) への矩形座標 (x, y) の変換からの角度(-πから+π)
指数整数 Math.pow(double x, double y) xのy乗
指数整数 Math.exp(double x) オイラー数 e を double t値で累乗した値
対数関数 Math.log(double t) 指定された double t値の自然対数値 (底は e)
平方根関数 Math.sqrt(double t) double t値の正しく丸めた正の平方根
立方根関数 Math.cbrt(double t) double t値の立方根
絶対値関数 Math.abs(T t) 型Tの絶対値(Tはint, long, float, double)


線形リストデータ構造

線形リストデータ構造(リンク付きリストデータ構造)は、 動的なデータ構造として、もっとも基本的なデータ構造です。 線形リストは配列としばしば比較されますが、 一番の違いは、配列と異なり、まさに、 動的にメモリを割当られること、編集が可能であり、 リスト中の特定の要素を削除したり、リストにキーとなる要素がある場合は、 そのキーでソート順にリンクを生成し、 新規データをソート順序を壊さずに、リストの途中に挿入することができます。 ここでは、以下の図のように、 文字列(String)をキーとして線形リストを作成し、 その後、別のノードを文字列の(ソート)順に挿入するプログラムを考えます。 このため、以下の図のように、先頭の要素を指すhead、 最後の要素を指すtailというメンバー変数を用意することにします。 挿入対象の要素の前後を指すために、p1とp2また、 挿入対象自身を指すqという一時変数を用意することにします。

p1とp2で指定される要素間にqで指定するデータを線形リストに挿入する前


挿入後は以下のようになります。

線形リストにデータを挿入した後


線形リスト例を紹介するにあたり、リストのノード(Nodeクラス)と 線形リスト自体(SingleListクラス)を異なるファイルに 作成することにします。
Node.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
	プログラム3-1: 線形リストのノードクラス
*/
public class Node {

    private String name; /* 線形リストのノードに保持するString */
    private Node next; /* ノードの次の要素 */

    /* コンストラクタ */
    public Node(String name, Node next){
        this.name = name;/* Nodeの名前を設定 */
        this.next = next;/* Nodeの次の要素を設定 */
    }
    /* キーとなる文字列を返す */
    public String getName(){
        return name;/* 名前を返す */
    }
    /* next要素を返す */
    public Node getNext(){
        return next;/* 次の要素を返す */
    }
    /* キーを設定 */
    public void setName(String name){
        this.name = name;/* 名前を設定 */
    }
    /* next要素を設定 */
    public void setNext(Node next){
        this.next = next;/* 次の要素を設定 */
    }
    /* ノードのプリント */
    public void print(String cont){
        if (cont.equals(""))/* 引数の文字列が""のとき */
            System.out.print(" "+name);
        else
            System.out.print(" "+name+" ->");/* リストの途中は->をつける */
    }
} /* Nodeクラスのおわり */
一方、このNodeクラスのもとで、 SingleListクラスを以下のように、文字列をキーとして、 整列順序で作成するとします。
SingleList.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/*
	プログラム3-2: 線形リストの生成と動的修正
*/
public class SingleList {
    private Node head; /* headを指す */
    private Node tail; /* 末尾を指す */
    private long size; /* 線形リストのサイズ */

    /* コンストラクタ */
    public SingleList(){
        head = tail = null;/* 先頭と末尾の要素をnullで初期化 */
        size = 0L;/* サイズをlong型のゼロで初期化 */
    }
    /* リストに昇順に挿入 */
    public void insert(String name){
        if (head == null){/* insert to head */
            head = tail = new Node(name, null);/* 先頭も末尾も同じオブジェクトでインスタンス作成 */
            size++;/* サイズをインクリメント */
            return;
        }
        Node p1 = head;/* 今見ているノードを指す */
        Node p2 = null;/* 今見ているノードの次を指す */
        Node temp = new Node(name, null);/* 新規ノード */
        size++;
        Node q = null;/* 前のノード保持用 */
        while (p1 != null){/* 線形リストの現在要素がnullでない間 */
            p2 = p1.getNext();/* 次の要素をゲット */
            q = p1;
            if (p2 == null){/* 線形リストの末尾に挿入  */
                q.setNext(temp);/* 末尾に新規データを付ける */
                tail = temp;
                return;
            }
            else if (p1.getName().compareTo(name)>0) { // if (p1.getName() > name){
                /* 線形リストの先頭に挿入*/
                if (p1 == head){
                    head = temp;/* headに新規データを付ける */
                    head.setNext(p1);
                    return;
                }
                else {
                    System.err.println("Incredible!(ありえない)");
                    return;
                }
            }
            else if (p1.getName().compareTo(name) < 0 && 
                   p2.getName().compareTo(name)>=0){
                 /* p1とp2の間に挿入 */
                 p1.setNext(temp);/* p1.nextに新規データを付ける */
                 temp.setNext(p2);/* 新規データのnextにp2をつなげる */
                 return;
            }
            p1 = p1.getNext();/* 順序が正しければ、リストのnext要素との比較に進む */
        }
    }
    public void print(){/*反復的にプリント*/
        Node p = head;/* 先頭の要素をセット */
        while (p != null){/* nullでない間 */
            if (p.getNext() != null)/* 次の要素がnullでないとき */
                p.print("something");/* nullでない文字列で呼び出し */
            else
                p.print("");/* null文字列で呼び出し */
            p = p.getNext();/* 次の要素をセット */
        }
        System.out.print("\n線形リストのサイズは"+size+"です\n");
    }
    public static void main(String[] args){
        String[] names = {"高橋", "鈴木", "佐藤", "山田"};
        /* 最初の順序 */
        System.out.println("最初の順序");
        for (int i = 0 ; i < names.length ; i++){
            System.out.print(" "+names[i]);/* 要素を追加 */
            if (i != names.length-1)
                System.out.print(" ->");
            else
                System.out.print("\n");
        }
        SingleList sl = new SingleList();/* クラスのインスタンス生成 */
        for (int i = 0 ; i < names.length ; i++)
            sl.insert(names[i]);/*要素を追加 */
        /* まず最初に初期リスト要素をプリント */
        System.out.println("線形リスト作成後の順序");
        sl.print();
        /* もう一人追加 */
        sl.insert("田中");/* 田中を挿入 */
        sl.print();
    }
}
以下が実行結果です。
$ javacu SingleList.java

$ javau SingleList
最初の順序
 高橋 -> 鈴木 -> 佐藤 -> 山田
線形リスト作成後の順序
 佐藤 -> 山田 -> 高橋 -> 鈴木
線形リストのサイズは4です
 佐藤 -> 山田 -> 田中 -> 高橋 -> 鈴木
線形リストのサイズは5です
      SingleListクラスの実行結果



2分探索木データ構造

2分探索木(Binary Search Tree)構造とは、2分木の一種で、 「順序」を決めるための 「キー」となる要素を木構造のノードに含み、 「順序関係」が左部分木と右部分木で定義されるデータ構造のことです。 図がその例である。具体的には、この例では、ルートノードのキーから見て その左部分木には、ルートの"7"より小さいキーを持つ要素からなり、 右部分木には、ルートの"7"より大きいキーを持つ要素からなります。

2分木の例


一般に、あるノードのキーを k とし、その左部分木をT(L)、 右部分木をT(R)とすると、 T(L)のすべてのキーに対して、≤ k が成り立ちます。
キーとしては、上述のように、どのようなものでも、順序がつくもので いいので、まずは、整数値があると仮定して2分探索木を作成してみます。
BinaryTree.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
	プログラム3-3: 2分探索木クラス
*/

public class BinaryTree {/* 再帰的なデータ構造 */

    private BinaryTree left; /* 左部分木 */
    private BinaryTree right; /* 右部分木 */
    private int key; /* キー */
    private int count; /* カウント */
    
    /* コンストラクタ */
    public BinaryTree(){
        left = right = null; /* 無引数のコンストラクタ:初期化 */
    }
    public BinaryTree(int key){/* キーを引数として持つコンストラクタ */
        left = right = null;
        this.key = key;/* キーを代入 */
        this.count = 1;/* カウントは1で初期化 */
    }
    
    /* insert data (再帰的関数例!)*/
    public void insert(int key){
        if (this == null){/* nullならばコンストラクタでオブジェクト生成 */
            BinaryTree node = new BinaryTree(key);
            return;
        }
        if (key < this.key){/* 現在見ているノードのキーより小さい場合 */
            if (left == null) 
                left = new BinaryTree(key);/* 左部分木にノードを作成 */
            else left.insert(key);/* leftがnullでなければ再帰的に呼び出す */
        }
        else if (this.key < key){/* 現在見ているノードのキーより大きい場合 */
            if (right == null) 
                right = new BinaryTree(key);/* 右部分木にノードを作成 */
            else right.insert(key);/* rightがnullでなければ再帰的に呼び出す */
        }
        else {/* キーが等しければ、カウントを増やす */
            this.count++;
        }
        return;
    }

    /* print */
    public void print(BinaryTree node){
        System.out.println("key = "+node.key);/* キーをプリント */
    }
    
    /* 再帰的に2分探索木をトラバース */
    public void inOrder(BinaryTree node){
        if (node != null){
            inOrder(node.left);/* 左部分木を調べる */
            node.print(node);
            inOrder(node.right);/* 右部分木を調べる */
        }
    }
    
    /* main関数*/
    public static void main( String[] args ) {
        int[] keys = { 17, 25, 15, 25, 16, 13, 43, 14 };/* キーデータ例 */
        BinaryTree root = new BinaryTree(keys[0]);
        for (int i = 1 ; i < keys.length; i++)
            root.insert(keys[i]);/* データを2分木にルートから挿入 */
        root.inOrder(root);
    } /* main関数のおわり */
} /* BinaryTreeクラスのおわり */
以下が実行結果です。in-orderで再帰的に2分探索木を走査し、キーの値をプリントすると、ソーティングされることがわかります。
$ javacu BinaryTree.java

$ javau BinaryTree
key = 13
key = 14
key = 15
key = 16
key = 17
key = 25
key = 43
      2分木プログラムの実行例

Apache Antに関して

Antとは、Another Neat Tool(直訳すると「また別の、いけてるツール」) の略で、 Apache(アパッチ)でサポートされている無償のツール です。 Unix系のOSに詳しい読者の方は、機能的に“Makefile”と類似するツールであると理解していただいて結構です。 ただし、Makefileと根本的に異なるのは、 Antではコンパイルや実行などの設定がすべてXMLで書かれていることです。 Makefileは特殊な記法ですので、習得に時間を要します。 Antに関する詳細や最新のダウンロード版等の情報は、 Apache AntのWebサイト を参照してください。

Antのインストール

まず、各自のホームディレクトリに"bin"ディレクトリを作成してください。

$ mkdir bin
$ cd bin
次に最新のAnt(2019年10月29日現在バージョン1.10.7)をダウンロードします。 これをunzipしてください。

$ unzip apache-ant-1.10.7-bin.zip
次に、ホームディレクトリにある".bashrc"ファイルをエディタで開き、 PATH=~/bin/apache-ant-1.10.7/binをPATHに追加してください。 たとえば以下のような行になります。
PATH=~/bin/apache-ant-1.10.7/bin:/usr/local/bin:$PATH
以上で、Antの環境設定は終わりです。 設定後、必要に応じて
$ source ~/.bashrc
を実行しておいてください。最後に、端末から

$ ant -version
とタイプして、Antのバージョンを確認しておいてください。

なお、Antを使う場合は、PATH以外の環境変数を設定(確認)する必要がある場合があります。 特に、Windows環境等でAntを使う場合は、JAVA_HOMEという環境変数が正しく設定されていることを確認しておいてください。(たとえば、演習室なら、各自のホームディレクトリにある.bashrcを編集し、最終行に、JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commandsを入れておき、$ source ~/.bashrcを一回実行すれば大丈夫です) また、javaをインストールしたときにできるJRE(Java Runtime Environment)関連のライブラリ中のtools.jarを必要とする場合があります。

Antのサンプル

Antの中核をなすのは、"build.xml"という名前のXMLファイルです。 Antを動作させるための、build.xmlに関する書き方の取り決めに関してサンプルを紹介し、 その中でJava言語のプログラムをどのようにしてコンパイルし、どのようにして実行するかについて解説します。 そこで、RandomBinaryTree.javaとBinaryTree.java, の2つのJavaソースプログラムを準備する場合で例示します。
  1. これら2つのJavaプログラムをsrcという名前のフォルダ以下に配備することにします。
  2. これら2つのJavaプログラムをコンパイルした結果のクラスファイルをclassesという名前のフォルダにまとめて配備することにします。
  3. クラスファイルをまとめたライブラリをmyRandomBinaryTree.jarという jarファイルに作成するとします。 jarファイルとは、Java Archiveファイルの略です。
Java言語の実行では、javaというインタープリタを使いますが、 このとき、mainが含まれるクラス名を指定して実行するのが通常です。 クラスファイルを直接指定してJavaプログラムを実行する場合が多いですが、 Java言語の実行をクラスファイルのとりまとめをしているjarファイルから実行することもできます。 ただし、ひとつ注意点があります。 Javaでは、すべてのクラスファイルにmain関数を記述することができます。 従って、クラスファイルをまとめたjarファイルから実行するとき、 javaインタープリタに、どのクラスファイルのmain関数で実行するかを教える必要があります。 このためマニフェストファイルに、「Main-Class:クラス名」という形式の行を指定する必要があります。 ここで「クラス名」には、そのmain関数を実行したいクラスファイル名を表します。 マニフェストファイルは簡単なものですが、その内容は後述します。

まとめると、以下の図で示すようにフォルダを作成します。

Ant用フォルダ構成例


この図の中で、srcフォルダに2つのJavaプログラムを以下のように入れます。 ソースファイルのアイコンは適当なエディタのアイコンで表現されている例となっています。

srcフォルダ以下のJavaソースプログラム例


RandomBinaryTree.javaの中身は、以下のようです。
RandomBinaryTree.java
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/*
	プログラム3-4: ランダムにノードの値を生成する2分探索木クラス
*/
import java.util.Date;/* 日付クラス */
import java.util.Random;/* 乱数クラス */

public class RandomBinaryTree {
	/* コンストラクタ */
	Random rand; /* Randomクラスの変数と保持 */
	int min = Integer.MIN_VALUE; /* 最小値 */
	int max =Integer.MAX_VALUE; /* 最大値*/
	int[] count;  /* 乱数発生回数 */
	int[] sequence; /* 乱数そのもの */
	public int[] getCount() {/* 乱数発生回数を返す関数 */
		return count;
	}
	public int[] getRandSequence() {/* 生成した乱数をそのまま返す関数 */
		return sequence;
	}
	public int getRand(){/* [min, max) の範囲の整数の乱数を発生 */
		int x = min + rand.nextInt(max-min+1);/* 乱数を発生 */
		return(x);
	}
	public void init(){
		Date date = new Date();/* Dateクラスのオブジェクト生成 */
		/* 現在時刻で初期化する */
		rand = new Random(date.getTime());/* Randomクラスのオブジェクト生成 */
	}
	public void init(int min, int max){
		init();/* 無引数のinit関数の呼び出し */
		this.min = min;/*最小値 */
		this.max = max;/* 最大値 */
		count = new int[max-min+1];/* カウント配列オブジェクト生成 */
	}
	public void generate(int num){
		sequence = new int[num];/* 生成した乱数を保持するメモリ確保 */
		for (int i =  0 ; i < num ; i++){
			int k = getRand();/* 乱数を発生 [min,max) */
			sequence[i] = k;/* 乱数を保持 */
			count[k-min]++;/* 乱数のカウントを保持 */
		}
	}
	/* main関数*/
	public static void main( String[] args ) {
		RandomBinaryTree rb = new RandomBinaryTree();
		int num = Integer.valueOf(args[0]).intValue();/* 発生する乱数の総数 */
		int min=0, max=1;
		if (args.length > 2){
			min = Integer.valueOf(args[1]).intValue();/* 最小値 */
			max = Integer.valueOf(args[2]).intValue();/* 最大値 */
			rb.init(min, max);/* init関数の呼び出し */
		}
		else
			rb.init();/* 無引数のinit関数の呼び出し */
		rb.generate(num);/* 生成 */
		int[] cnt = rb.count = rb.getCount();/* カウントをゲット */
		rb.sequence = rb.getRandSequence();/* 乱数のシーケンス */
		for (int i = 0 ; i < rb.sequence.length ; i++)
			System.out.println("乱数発生順序["+(i)+"] = " +rb.sequence[i]);
		for (int i = min ; i < max+1 ; i++)
			System.out.printf("count[%d] = %d\n",i,cnt[i-min]);

		BinaryTree root = new BinaryTree(rb.sequence[0]);/* 2分木の生成 */
		for (int i = 1 ; i < rb.sequence.length; i++)
			root.insert(rb.sequence[i]);/* 挿入する */
		System.out.println("2分探索木のルート:");
		root.print();/* ルートからプリント */
		System.out.println("ルートから中順序で再帰的に出力:");
		root.inOrder(root);/* inOrderでプリント */
	} /* main関数のおわり */
} /* RandomBinaryTreeクラスのおわり */
classesフォルダとlibフォルダは最初は中身が空のフォルダです。 META-INFフォルダの中身は以下のようにひとつのマニフェストファイル からなります。

srcフォルダ以下のJavaソースプログラム例


ここでは、MANIFEST.MFという名前のファイルからなっています。 ファイル名は適当な名前でいいですが、拡張子は.mf(.MF)のものがあることが期待されます。 これはテキストファイルで中身に以下のような記述があるものです。 この拡張子が".mf" (".MF")のファイルを、Javaではマニフェストファイルと呼びます。

$ cat MANIFEST.MF
Manifest-Version: 1.0
Created-By: Apache Ant 1.10.7
Main-Class: RandomBinaryTree
META-INFフォルダ以下のMANIFEST.MFファイル


たった3行ですが、基本はキーワードと値がコロンの両側で行単位で現れます。 このうち大事なのは3行目です。 すなわち、Main-Classというキーワードが、 どのクラスにあるものをjarファイルとして使用するかを、公約(マニフェスト)しています。 ここでは、Main-Class: RandomBinaryTreeとありますのでRandomBinrayTreeクラスにあるmain関数を全体のmainとしてjarファイルを作成することを公言していることになります。

さて、ここまで準備ができると、Antの実行に必要な build.xml ファイルの中身を見ていきます。
build.xml (for RandomBinaryTree)
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project name="RandomBinaryTree" default="compile" basedir=".">

     <property name="src" location="src"/>
     <property name="classes" location="classes"/>
     <property name="jar" value="lib/myRandomBinaryTree.jar"/>
     <property name="args" value="20 -10 10"/>
     <property name="fileEncRuntime" value="encoding=UTF-8"/>
     <property name="fileEncoding" value="-encoding UTF-8"/>

     <target name="compile"><!-- compileタスク-->
          <javac srcdir="${src}" destdir = "${classes}" includeAntRuntime="No">
               <compilerarg line="${fileEncoding}" />
               <classpath/>
          </javac>
     </target>

     <target name="jar" depends="compile"><!-- jarタスク-->
          <jar destfile="${jar}" basedir="${classes}" >
          <manifest>
          <attribute name="Main-Class" value="RandomBinaryTree" />
          <attribute name="Class-Path" value="${jar}"/>
          </manifest>
          </jar>
     </target>

     <target name="run" depends="compile"><!-- runタスク:compileタスクに依存-->
          <java classname="RandomBinaryTree" fork="true">
               <arg line="${args}" />
               <jvmarg line="-Xmx100m -D${fileEncRuntime}" /><!-- javaインタープリタへの引数を指定-->
               <classpath>
               <pathelement location="./classes"/>
               </classpath>
          </java>
     </target>

     <target name="runjar" depends="compile,jar"><!-- runjarタスク:compileとjarタスクの両方に依存-->
          <java jar="${jar}" fork="true" classpath=".">
              <arg line="${args}"/>
          </java>
     </target>

     <target name="clean">
          <delete dir="${classes}"/>
          <mkdir dir="${classes}"/>
     </target>
</project>
build.xmlは、記述言語としては、XMLで書かれています。 XMLとは、Extensible Markup Languageの略で、 W3Cと呼ばれるインターネットの標準を定める非営利団体によって国際標準として規格化されています。 簡単に言えば、HTMLと似たタグ構造を持ち、HTMLよりも厳しく、 開始タグ(たとえば<target>タグ)があるとかならず、 終了タグ(たとえば</target>タグ)があることが保証されている記述言語です。

さて、Antが使用するbuild.xmlの中身ですが、 <property>タグが幾つか定義されたあと、 <target>タグで記述されるブロックが幾つかあることがわかります。 ひとつの<target>タグで行うブロックを、 ひとつのAntのタスクと呼びます。 以下で、この代表的なタスクを紹介します。

Antのコンパイルタスク

まず、srcフォルダに置いてある Java言語プログラムをコンパイルするコンパイルタスクは、11行目~16行目になります。 12行目に<javac>タグがあり、これがJavaのコンパイル部分に対応します。ここでは、 ソースファイルたちが収納されているフォルダ(フォルダ内に別のフォルダが階層的にあっても大丈夫です)をsrcdir(ソースディレクトり)とし、 コンパイルした結果のクラスファイルはdestdir(デスティネーションディレクトリ)に結果が書き込まれます。 ここでは、srcdirが
"${src}$"
で、destdirが
"${classes}$"
となっており、 これが上の方で定義されている<property>タグから、 それぞれ"src"と"classes"という名前であることがわかります。 13行目の<compilerarg>タグでコンパイラのオプションをして指定ます。 ここでは、UTF-8符号であるためのオプションを指定しています。 14行目にある<classpath/>タグは、ここでは特に指定していないですが、 コンパイルに必要な他のクラスやjarファイルがある場合、 22行目~24行目にあるような方法で指定します。 Antでのコンパイルは以下のようにターミナル窓から実行します。

$ ant compile
Buildfile: /home/1/ma002/Java/3/myArchive/build.xml

compile:
    [javac] Compiling 4 source files to /home/1/ma002/Java/3/myArchive/classes

BUILD SUCCESSFUL
Total time: 0 seconds
     Ant comileタスクの実行例

これからもわかるように、ターミナル窓からは、
$ ant compile
のようにAntのコマンド(ant)と、タスク名compileを併記します。 もし、出力がUTF-8でなく、文字化けが発生する場合は、たとえば、
$ ant compile | nkf -w
とすることで、 Antの出力をUTF-8に切り替えることができます。 このパイプ部分は、nkfコマンドの-wオプションでUTF-8符号に変換しています。 なお、通常のコンパイルとAntが異なるのは、 上述の3つのJavaプログラムに関して一度コンパイルに成功したあと、どれかひとつだけプログラムを修正した場合、 次の
$ ant compile 
の実行では、修正したプログラムだけがコンパイルされる
点です。

Antのjavaタスク

次に、クラスファイルが準備できたあとに、 プログラムを実行するjavaタスクに関して説明します。 これは、build.xmlの中では、18行目~26行目に該当します。 18行目の<target>タグ内にdepends="compile"とあるのは、 実行するためには、事前にコンパイルタスクが終了していて、 それに「依存する」とぴう宣言です。 19行目から始まる<java>タグの中では、 classname="RandomBinaryTree"とあることで、 これをmain関数の含まれるクラスとして実行することを表します。 ちなみに、classname=の代わりに、jar=でjarファイルを指定しても、 プログラムを実行することができます。 20行目の<args>タグは、 javaコマンドに与えるプログラム用のパラメータ入力のために使っていて、 デフォルト値は、 7行目の<property>タグにあるvalue="20 -10 10"(前回の課題である、 20個の乱数を[-10,10]の間で発生させる、という意味です。 ちなみに、この値を変更したいときは、

$ ant -Dargs="30 -20 20" run
のように、-Dオプション直後に<property>タグにあるname=で指定される名前と="30 -20 20"のような値(vakue)を付けて実行することができます。 21行目の<jvmarg>タグも、javaプログラムに別のパラメータを与える方法で、ここでは、 ヒープメモリを100MB(-Xmx100m)とり、UTF-8符号で実行するようにとのオプションになります。 以下が実行例です。

im170:myArchive ma002$ ant -Dargs="10 -5 5" run
Buildfile: /home/1/ma002/Java/3/myArchive/build.xml

compile:

run:
     [java] 乱数発生順序[0] = 3
     [java] 乱数発生順序[1] = 5
     [java] 乱数発生順序[2] = -3
     [java] 乱数発生順序[3] = 3
     [java] 乱数発生順序[4] = -4
     [java] 乱数発生順序[5] = -1
     [java] 乱数発生順序[6] = 5
     [java] 乱数発生順序[7] = -5
     [java] 乱数発生順序[8] = -4
     [java] 乱数発生順序[9] = 3
     [java] count[-5] = 1
     [java] count[-4] = 2
     [java] count[-3] = 1
     [java] count[-2] = 0
     [java] count[-1] = 1
     [java] count[0] = 0
     [java] count[1] = 0
     [java] count[2] = 0
     [java] count[3] = 3
     [java] count[4] = 0
     [java] count[5] = 2
     [java] 2分探索木のルート:
     [java] key = 3 count = 3
     [java] ルートから中順序で出力:
     [java] key = -5 count = 1
     [java] key = -4 count = 2
     [java] key = -3 count = 1
     [java] key = -1 count = 1
     [java] key = 3 count = 3
     [java] key = 5 count = 2

BUILD SUCCESSFUL
Total time: 0 seconds
Ant runタスクで外からパラメータを与える例


なお、Antを実行すると、左端に[java]というタスク名 が実行後の各行に自動的に付加されます。 [java]のようなタスク名を、表示したくない場合は、以下のように Antの-eオプションをつけてください。

$ ant -e -Dargs="10 -5 5" run

Antのjarタスク

最後に、Javaのクラスファイルたちをまとめてライブラリを作成するjarタスクに関して簡単に述べます。 jarは、上述したように、Java Archiveの略です。 これは、UNIX系のOSでよく使われているtarコマンド(tape archive)と似たシンタックスを持ちます。 ソースプログラムも入れることができますが、 一番多い使い方は、あるプログラムを実行するために必要なクラスファイルだけをまとめて、 一般にプログラムを公開するような場合に便利です。 以下がjarファイルを作るためのコマンド例と、 できたjarファイルの中身を見るためのコマンド例です。

im170:myArchive ma002$ ant jar
Buildfile: /home/1/ma002/Java/3/myArchive/build.xml

compile:

jar:
      [jar] Building jar: /home/1/ma002/Java/3/myArchive/lib/myRandomBinaryTree.jar

BUILD SUCCESSFUL
Total time: 0 seconds
Ant jarタスクの実行例


jarの中身は、

$ jar tvf myRandomBinaryTree.jar

で確認しています。jarのパラメータのtvfの意味はtがjarの中身の確認、vがverboseオプション、 fは、このオプションの直後にjarファイル名がくることを意味します。 tarコマンドを使用した経験があれば、 これは、tar tvf ***.tarとほぼ同じ意味であることがわかります。 実行例から、もともと、 classesの下にあったクラスファイルとMETA-INFの下にあったマニフェストファイルが含まれているのがわかるかと思います。 このマニフェストファイルがあるため、3つ含まれるクラスファイルのうち、RandomBinaryTree.classにあるmain関数を実行で使用することがわかります。 このjarファイルを用いた実行例を以下に示します。

im170:myArchive ma002$ ant runjar
Buildfile: /home/1/ma002/Java/3/myArchive/build.xml

compile:

jar:

runjar:
     [java] 乱数発生順序[0] = -6
     [java] 乱数発生順序[1] = 7
     [java] 乱数発生順序[2] = -7
     [java] 乱数発生順序[3] = 4
     [java] 乱数発生順序[4] = 10
     [java] 乱数発生順序[5] = -2
     [java] 乱数発生順序[6] = 10
     [java] 乱数発生順序[7] = 4
     [java] 乱数発生順序[8] = -8
     [java] 乱数発生順序[9] = 10
     [java] 乱数発生順序[10] = -5
     [java] 乱数発生順序[11] = 1
     [java] 乱数発生順序[12] = -10
     [java] 乱数発生順序[13] = -1
     [java] 乱数発生順序[14] = -5
     [java] 乱数発生順序[15] = 7
     [java] 乱数発生順序[16] = -10
     [java] 乱数発生順序[17] = -10
     [java] 乱数発生順序[18] = -5
     [java] 乱数発生順序[19] = -5
     [java] count[-10] = 3
     [java] count[-9] = 0
     [java] count[-8] = 1
     [java] count[-7] = 1
     [java] count[-6] = 1
     [java] count[-5] = 4
     [java] count[-4] = 0
     [java] count[-3] = 0
     [java] count[-2] = 1
     [java] count[-1] = 1
     [java] count[0] = 0
     [java] count[1] = 1
     [java] count[2] = 0
     [java] count[3] = 0
     [java] count[4] = 2
     [java] count[5] = 0
     [java] count[6] = 0
     [java] count[7] = 2
     [java] count[8] = 0
     [java] count[9] = 0
     [java] count[10] = 3
     [java] 2分探索木のルート:
     [java] key = -6 count = 1
     [java] ルートから中順序で出力:
     [java] key = -10 count = 3
     [java] key = -8 count = 1
     [java] key = -7 count = 1
     [java] key = -6 count = 1
     [java] key = -5 count = 4
     [java] key = -2 count = 1
     [java] key = -1 count = 1
     [java] key = 1 count = 1
     [java] key = 4 count = 2
     [java] key = 7 count = 2
     [java] key = 10 count = 3

BUILD SUCCESSFUL
Total time: 0 seconds
Ant runjarタスクからの実行例


Antを使わない場合、以下のようにjarファイルだけ用いてjavaインタープリタで実行させることができます。

$ javau -jar lib/myRandomBinaryTree.jar 6 -3 3

Javaにおけるクラス継承の概念とその事例

クラスの継承

クラスの継承は、オブジェクト指向言語では、必須の機能のひとつで、C++言語にも、 class B : public A { ... }; のように書き、クラスBがクラスAを継承する、というような定義ができました。 Java言語でのクラスの継承は、以下のようにして定義します。

public class B extends A { ... }
このようにextendsキーワードを使い、クラスBがクラスAを継承する、と記述します。

「継承」という言葉から類推できるように、 親クラスに相当する基底クラス(スーパークラス)(base class)と、 それを継承する派生クラス(サブクラス)(derived class)の間では、 幾つかの性質が継承されます。 アクセス制御子に関して、 public またはprotected がついている基底クラスのメンバー変数やメンバー関数は、派生クラスから直接アクセスすることができます。 ただし、基底クラスでprivateがついているメンバー変数やメンバー関数は、派生クラスから直接アクセスすることはできません。

継承関係にある基底クラスと派生クラスのコンストラクタの間には暗黙のルールがあります。 すなわち、派生クラスのオブジェクトが生成されるとき、 自動的に基底クラスのコンストラクタが呼び出されます。 派生クラスにコンストラクタがない場合、 Javaコンパイラによって以下のデフォルトコンストラクタがあると思って解釈されます。

B() { super(); }

ここで、super()とは、基底クラスの無引数のコンストラクタを表します。 もし、基底クラスに明示的なコンストラクタがない場合は問題ありません。 また、基底クラスに無引数のコンストラクタが定義されている場合も問題ありません。 基底クラスに無引数のコンストラクタがなく、 1つ以上の引数を持つコンストラクタ(だけ)がある場合、 派生クラスのオブジェクトが生成されるとき、 基底クラスのコンストラクタの引数に合わせたコンストラクタを用意しなければなりません。 そうでない場合、コンパイルエラーが発生します。 これを回避するための一番シンプルな解決策は、 基底クラスに無引数のコンストラクタを用意しておくことです。

一方、上述のデフォルトの動作で示唆したように、 派生クラスのコンストラクタから、 親クラスのコンストラクタをsuperという関数で呼び出すことができます。 これは、C++言語にはない関数です。 また、継承されるものがある一方で、 基底クラスで定義したメンバー関数と名前も型も引数も全く同じ関数を定義することができます。 このような関数をオーバーライド(override)された関数と呼びます。 クラスの継承では、関数のオーバーライドをうまく使うと、とても便利な機能を享受できます。 たとえば後述の例では、基底クラスに線形リストからアクセスするだけで、 実際の個々の野菜を定義する派生クラスのprint関数(オーバーライドされる関数)が、 自動的に呼び出されます。 後掲のプログラム3-5の15-17行目あたりの基底クラスからprint関数を呼び出しているあたりが、 このオーバーライド機能に相当します。 なお、Java言語には、 C++言語の純粋仮想関数と類似する抽象クラスという概念があります。 抽象クラスで定義される関数は、派生クラスで必ずすべてオーバーライドしないといけません。 同様に、ニュートン法の例で示したように、インタフェースという概念も、 C++言語の純粋仮想関数と類似する概念です。

関数のオーバーライド関数のオーバーローディング(overloading)は、全く異なる概念ですので注意してください。 関数のオーバーローディングは、同じ名前であるが、引数や型の違う関数を再定義できる機能のことで、 後述するプログラム3-18で示す階乗関数int Factorial(int n), long Factorial(long n), BigInteger Factorial(BigInteger n)のような関数の再定義が関数のオーバーローディングの例です。 また、型はありませんがコンストラクタは、しばしばオーバーローディングの対象となります。

さて、話をクラスの継承にもどします。クラスの継承がふさわしい適用例に共通するものは何でしょうか? 以下は野菜の例ですが、「野菜」という全体、あるいは抽象的なものを基底クラスとし、 「ほうれん草」や「キャベツ」といった個々の事例、すなわち具体的なものを派生クラスとすることがもっとも典型的なクラスの継承がふさわしい適用例となります。 また、「四辺形」と基底クラスとし、「平行四辺形」「ひし形」「長方形」などを派生クラスとする、 というように、全体を包含するものを基底クラスとする場合も、ふさわしい適用例となります。 クラスの継承を簡単な例で示します。

クラスの継承事例


この例では、「野菜」クラスが基底クラス(ベースクラス)で、4つの野菜「ほうれん草」「キャベツ」「人参」「たまねぎ」が、 それを継承する派生クラスです。 図には、各野菜の100gあたりのビタミンCの含有量と、 それぞれ1kg買ったとしたときの値段を表しています。 このクラスの継承に関して、メインプログラムとしては、 4種類の野菜が前提として、1kg単位で売られていると仮定し、 1kgあたりで、これを何セットかまとめて買うとします。 そのときに、ビタミンCの総含有量と、販売価格の総計を求め、プリントするプログラムを考えます。 これを線形リスト(プログラム3-8とプログラム3-9)を使って、 以下の図に示すような8つのプログラムに分割して作成するとします。

野菜クラスの継承の実装で複数の野菜を線形リストで管理する事例


この図にあるクラスの実装例を以下で紹介します。