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

第4回 Javaクラスの継承とポリモルフィズム

クラスの継承(Class Inheritance)は、 オブジェクト指向言語プログラミング(OOP=Object-Oriented Programming)に共通する、 代表的な概念です。 関数のオーバーライド(名前引数戻り値も全く同一の関数の多重定義)(function overriding)の概念は、クラスの継承があってこそ意味があります。 一方、関数のオーバーロード(名前が同じだが、引数戻り値が異なる関数の複数定義)(function overloading)は、クラスの継承とは無関係の概念です。 オーバーロードとオーバーライドに共通するのは、 同じ名前の関数が、いろいろな形態で同一プログラム内に現れる点です。 このように多形態をとるため、まとめて、 ポリモルフィズム (polymorphism:多相性、多形態、多様性などと翻訳されます)と呼ぶことがあります。 なお、C++では、演算子のオーバーロードもポリモルフィズムの一種として存在しますが、 Java言語では演算子をオーバーロードできません。 ちなみに、科学計算や人工知能のプロタイピング用途で盛んに利用されているPython言語では、型を動的に定義できるため(dynamic typing)、関数のオーバーロードを必要としません。たとえば、 同じ関数で、あるときは引数に整数を渡したり、あるときは文字列を渡したり自由にできます。 また、クラスの継承や関数のオーバーライドはPythonでも、JavaやC++と同様に自由にできるようになっています。 いずれにせよ、本クラスでとりあげているJavaだけでなく、C++やPythonのようなオブジェクト指向言語でも、ポリモルフィズムを自然にサポートしているのです。

クラスの継承(オーバーライドされる関数)

クラスの継承に関する復習とオーバーライドにおける注意点を述べます。 基底クラスを作成しておき、 それを継承する複数の(一般的により具体的な)派生クラスから、 基底クラスにあるメソッド(関数)をオーバーライドしておき、 呼び出す側(他のクラスやmain関数など)で、

基底クラス名A 変数名vA = new 派生クラス名B(...);
派生クラス名B 変数名vB = new 派生クラス名B(...);
のように設定しておきます。 まず、最初の行で行うオーバーライドでは、 変数名vAから、 あたかも基底クラスの関数を呼び出すようにして、 派生クラスの(オーバーライドされる)関数を呼び出すことができます。 2行目の場合でも、 派生クラスBのオブジェクトが生成される際に、 デフォルトでは、 基底クラスの無引数のコンストラクタが呼び出されますので、 オーバーライドされる関数で 基底クラスの変数の値に対して、独自の演算を適用することで 派生クラスごとに、異なる値をゲットするように操作することが可能です。 なお、最初の式では、 ここで、代入の左辺と右辺でクラス名が違うことに注意してください。 左辺には「基底クラス」がきており、 右辺には「派生クラス」が現れています。 この逆の代入はできません。 例をあげます。
SimpleSuperSub.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
/*
  プログラム4-1: SimpleSuperSub.java
  基底クラス = new 派生クラスで関数はオーバーライドされます。
  複数のクラスを1つのファイルに記述することもできる。
  その場合、main関数のあるクラス名をファイル名の拡張子以外に一致させ、コンパイル+実行させる
*/
class Super {
     String greeting = "Greetings";
     String name() { return "かんきつ類"; }
}
class SubA extends Super {
     String greeting = "Good morning";
     String name(){ return "みかん"; }
}
class SubB extends Super {
     String greeting = "Good afternoon";
     String name(){ return "ぽんかん"; }
}
class SubC extends Super {
     String greeting = "Good evening";
     String name(){ return "はっさく"; }
}
public class SimpleSuperSub {
     public static void main(String[] args){
           Super [] sup = new Super[3];/* 基底クラスの配列変数オブジェクトを生成 */
           sup[0] = new SubA();/* 派生クラスAのオブジェクトを基底クラスの変数に代入 */
           sup[1] = new SubB();/* 派生クラスBのオブジェクトを基底クラスの変数に代入 */
           sup[2] = new SubC();/* 派生クラスCのオブジェクトを基底クラスの変数に代入 */
           for (int i = 0 ; i < sup.length ; i++){/* 基底クラスのループで処理できる */
               /* privateやprotected等のアクセス制限のない変数greetingには直接アクセス可能 */
                System.out.println(sup[i].greeting+" : "+sup[i].name());
           }
     }
}
プログラム4-1はSimpleSuperBus.javaという名前ですが、これをコンパイルすると、 中に含まれるSuper.class, SubA.class, SubB.class, ならびにSubC.classというクラスファイルが生成されます。

ここで注意すべき点があります。 派生クラスのオブジェクトを生成して、 基底クラスの変数(配列、リストやコレクション)に代入することで、 基底クラスをオーバーライドする派生クラスの関数に(個々の派生クラスを意識しないで)アクセスできることです。 上のプログラムを実行すると、以下のようになります。

$ javacu SimpleSuperSub.java

$ javau SimpleSuperSub
Greetings : みかん
Greetings : ぽんかん
Greetings : はっさく


オーバーライド可能な関数には制約があるでしょうか? 通常の関数(これをインスタンス関数とかインスタンスメソッドと呼びます)であれば、無事にオーバーライドされます。 しかし、static修飾子がついている場合(これをクラス関数とかクラスメソッドと呼びます)は、 オーバーライドできません。 また、当然ですが、コンストラクタのオーバーライドはありえません。 static修飾子がついた関数は、 たとえば基底クラスや派生クラスのようなクラスの階層構造に関係なく、 オブジェクトを1個だけ生成し、皆で共有したいとかといった特別の理由がある場合にのみ、 意味があります。 Math.sqrt, Math.sinなどの数学関数は、staticな関数の代表例です。

クラスの継承(抽象クラス、抽象関数(メソッド))

前回、クラスの継承に関して、 基底クラスとしての「野菜クラス」と派生クラスとしての「ほうれん草クラス」「人参クラス」 「キャベツクラス」「玉ねぎクラス」の例を線形リストを通して、紹介しました。

ここでは、クラスの継承の中でも、C++で「仮想クラス」や「仮想関数」として知られている概念と類似するJava言語での概念を述べます。 抽象クラス(Abstract class)とは、 派生クラスで継承されることを前提とした(基底)クラスの一種で、派生クラスで共通して使用したい関数(これを抽象関数といいます)があり、 それらを派生(サブ)クラスで必ず実装されることを前提とし、 自分自身のクラス内ではそのような関数のプロトタイプだけ書けばいい場合に有効なクラスです。 このため、抽象クラスは、必ず派生(サブ)クラスと同時に使用され、 クラスの継承関係を自然に構築するものであることがわかります。 以下では、例を用いて抽象クラスを示し、 この抽象クラスと同時に定義する必要のある派生(サブ)クラスの例を示します。
Shape2D.java(Colorなしバージョン)
1 
2 
3 
4 
5 
6 
7 
8 
9 
10
11
12
13
14
15
16
17
18
19
/*
	プログラム4-2 Shape2Dクラス:抽象クラス
*/
import java.io.PrintStream;

public abstract class Shape2D {
    /*** 以下3つの関数が抽象関数 ***/
     abstract double area(); /* 面積を求める抽象メソッド */
     abstract double perimeter(); /* 周囲長を求める抽象メソッド */
     protected abstract void psPrint(PrintStream pStr);/* プリントする抽象関数 (注) privateにはできない */
    /*===== ここから、変数や非抽象関数の定義 =====*/
     private String name; /* 形状の名前  */
     private String getName(){ return name; }/* 形状の名前を返す:抽象関数ではない */
     protected void setName(String name) { this.name = name; }     /* 形状の名前を設定する */
     void printHead(PrintStream cout){/* PostScriptヘッダーのプリント */
          cout.printf( "%% %s 面積 = %5.3f\n", getName(), area());
          cout.printf( "%% %s 周囲長 = %5.3f\n", getName(), perimeter());
     }
}
プログラム4-2では、2次元図形を定義する抽象クラスを定義しています。 ここで注目すべきは、abstractというキーワードです。 これはクラス名にも、また関数にも付随することがあります。 クラス名についた場合は、そのクラスは抽象クラス、 関数(メソッド)についた場合、その関数は抽象関数であることを表します。 一般にクラス内にひとつでも抽象関数が含まれる場合、 そのクラスは抽象クラスとして宣言しなければなりません。 大事なこととして、 抽象クラスのインスタンスは作れない ということは、重要なので覚えておいてください。 ただし、C++の純粋仮想関数と違って、 抽象クラス内に抽象でない関数や変数を定義することは可能です。 では、そのオブジェクト(インスタンス)が作れないならどうするのか?ということを考えましょう。 実は、前回の野菜の例もそうですが、野菜の変数は定義していました。 それに代入したのは、派生クラス(例:ほうれん草)のインスタンスでした。 基底クラスに派生クラスを代入して利用する点は同様です。 なお、基底クラスで抽象関数(メソッド)として宣言された関数は、派生クラスで必ずその実体を定義しなければなりません。 プログラム4-2の例では、3つの関数を、抽象クラスを継承する派生クラスで必ず定義する必要があります。

抽象クラスの性質を以下にまとめておきます。

抽象クラスは、以前ニュートン法の説明で紹介したインタフェースと似た側面を持ちます。すなわち、インタフェースもインスタンスは作れません。 また、関数のプロトタイプ的な記述が出来る点も共通しています。 インタフェースの場合、継承する派生クラスと呼ばず、 そのインタフェースを実装するクラスという言い方をします。 一方、抽象クラスは、 クラス内にpublic型、protected型などの変数を定義できます。 ここでは、String型のnameという変数を定義しています。 インタフェースでは、このような変数は定義できません。 例外として、final static型の変数だけは定義できるようになっています。 また、抽象クラス内に、abstractのつかない、 抽象でない関数を定義することができます。 プログラム4-2のgetName, setName, printHeadなどの関数がこれに該当します。 インタフェースでは、これはできません。

以下では、抽象クラスの派生クラス例をShape2Dクラスを基底クラスとして示します。 このため、まず、2次元座標を与えるCoord2クラスを作成しておきます。 Coord2クラスは以下のようなクラスです。 メンバー変数としてはx,yという2つの変数での座標値だけです。
Coord2.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
/*
	プログラム4-3 Coord2クラス:2次元座標クラス(課題4に含まれるため、中身は空白)
*/
public class Coord2 {
	private double x, y;/* 2次元座標値 */

	public Coord2(double x, double y){/* コンストラクタ */
	
	
	}
	public double getX()/* Xを返すメンバ関数 */
	public double getY()/* Yを返すメンバ関数 */
	public void setCoord2(double x, double y){/* 座標値をセットするメンバ関数 */
		
	}
	public static double distance(Coord2 v1, Coord2 v2){/* ユークリッド距離 */
		
		
		
		
		
		
	}
}
Coord2クラスは抽象クラスではなく、通常のクラスです。 Shape2Dの派生クラスからもCoord2クラスを利用するため、こちらを先に解説しておきます。 Coord2クラスでは、16~23行目に、double型のdistanceメソッドがあり、 これにpublic staticというアクセス修飾子がついています。つまり、 distance関数はクラスメソッド(クラス関数)で、Coord2.distance(v1,v2.)のように、 外部からインスタンスを作らずにアクセスできます。 ここでは、distance関数を2点間のユークリッド距離として定義しています。

さて、本題の抽象クラスの派生クラスの例を示します。
Circle.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
/*
	プログラム4-4 Circleクラス:派生クラス(課題4に含まれるため、中身は空白)
*/
import java.io.PrintStream;

public class Circle extends Shape2D {
	 /* radius 半径 */
	 /* center of circle 円の中心座標 */

	public Circle()//無引数のコンストラクタ
	public Circle(double radius, Coord2 v){
		// 無引数のコンストラクタを呼び出す
		/* 円の半径 */
		/* 円の中心座標 */
	}
	public double area(){ /* 面積を求める抽象メソッド */
		/* 円の面積 */
	}
	public double perimeter(){ /* 周囲長を求める抽象メソッド */
		/* 円周長を返す */
	}
	public void psPrint(PrintStream cout){/* PostScriptでの円の記述 */
		printHead(cout);
		cout.println( "newpath" );
		//省略(埋めてください)
		//省略(埋めてください)
	}
}
3つの基底クラスで定義されていた抽象関数の具体例が与えられていることがわかります。 この例では、2次元図形として「円」が定義されています。 area関数は、「円の面積」になりますので、$\pi r^{2}$となります。 同様に、perimeter関数は、「円周長」になりますので、$2\pi r$となります。 psPrint関数は、 PostScriptで描画する関数です。 PostScriptマニュアルは、 こちらが参考になるかと思います。 日本語なら、ちょっと古いですが、こちらのメモが参考になると思います。 PostcScriptは逆ポーランド記法に基づく言語です。 ちなみに、先頭に%がある行は、PostScriptではコメント行と解釈されます。 「円」を描画する命令は24~26行目あたりにあり、 メインは、25行目の"x y r 0 360 arc"の部分で、簡単な意味は、 中心が(x,y)で半径がrの円弧を0度から360度まで描画することを意味します。 次に、同様な派生クラスとして、三角形の例を示します。
Triangle.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
/*
	プログラム4-5 三角形クラス:派生クラス
*/

import java.io.PrintStream;

public class Triangle extends Shape2D {

     private Coord2 v1, v2, v3;/* 三角形の3頂点 */

     public Triangle()/* コンストラクタ(無引数)*/
     public Triangle(Coord2 v1, Coord2 v2, Coord2 v3){/* コンストラクタ */
          /* 無引数のコンストラクタ呼び出し */
          /* 頂点1 */
          /* 頂点2 */
          /* 頂点3 */
     }
     public Coord2 getV1()/* 第一座標を返すメンバ関数 */
     public Coord2 getV2()/* 第二座標を返すメンバ関数 */
     public Coord2 getV3()/* 第三座標を返すメンバ関数 */
     public double perimeter(){/* 三角形の周囲長を返すメンバ関数:オーバーライド */
          double t1, t2, t3;
          /* v1-v2の距離 */
          /* v2-v3の距離 */
          /* v3-v1の距離 */
          /* 3辺の距離の和を返す */
     }
     public double area(){/* 三角形の面積を返すメンバ関数:オーバーライド */ 
          /* X1座標 */
          /* X2座標 */
          /* X3座標 */
          /* Y1座標 */
          /* Y2座標 */
          /* Y3座標 */
          /* 一種の外積 */
          /* 外積の絶対値の1/2が三角形の面積 */
          /* return文 */ 
     }
     public void psPrint(PrintStream cout){/* 三角形のデータをPostScriptで書き出すメンバ関数:オーバーライド */
          cout.printf("%% 三角形:面積 = %5.3f\n",area());
          cout.printf("%% 三角形:周囲長 = %5.3f\n",perimeter());
          /* newpathコマンド:詳細はhttp://tutorial.jp/graph/ps/psman.pdf等を参照 */
          /* ペンを移動 */
          /* ペンで次の座標まで線を描く */
          /* ペンで次の座標まで線を描く */
          /* closepathコマンド(一筆書きを閉じる) */
          /* strokeコマンド(描画する) */
     }
}
この例では、2次元図形として「三角形」が定義されています。 area関数は、「三角形の面積」になります。 一般に多角形の(符号付き)面積は、 頂点座標を左回りに定義したとすると、以下の図の ように計算されます。

多角形の頂点を左回りで定義する例とその符号付面積


ただし、 $y_{n+1}=y_{1}$で、$x_{n+1}=x_{1}$です。 これを実装したのが27行目~37行目になります。 なお、符号付き面積では、凹多角形の場合、 負やゼロになることもありますが、凸多角形では頂点が左回りに定義されている限り、正の値になります。 同様に、perimeter関数は、「三角形の周囲長」になりますので、 Coord2クラスで定義したdistance関数を3回使った結果の総和(つまり三辺の長さの和)になります。 psPrint関数では、42行目の"x1 y1 moveto"で、 座標値(x1,y1)に移動し、 43~44行目の"x2 y2 lineto"では、現在位置から、 (x2,y2)まで直線を描画するという意味です。 さらに45行目のclosepathで最初の位置まで線を引き図形を閉じ、 折れ線ではなく、多角形を構成することができます。

最後に、これらのクラスを使ったメインプログラムとその実行例を示します。
ShapeInheritance.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
87
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/*
	プログラム4-6 ShapeInheritance.java(メインプログラム)
*/
import java.util.Random;/* 乱数 */
import java.util.Date;/* 日付 */
import java.io.PrintStream;/* 出力ストリーム */
import java.io.FileNotFoundException;/* ファイルが見つからないエラー */
import java.io.UnsupportedEncodingException;/* PrintStreamの第2引数の符号が見つからないエラー */
import java.util.ArrayList;/* 動的配列リスト(システムクラス)*/
import java.text.Format;/* フォーマットのため */
import java.text.DateFormat;/* 日付のフォーマット用 */

public class ShapeInheritance {

  final static double XRANGE = 600.0;/* X方向のキャンバスサイズ */
  final static double YRANGE = 800.0;/* Y方向のキャンバスサイズ */
  final static double RADIUS = 200.0;/* 半径の最大値 */

  public String[] myPsName = {/* PSプリントヘッダー*/
      "%%!PS-Adobe-3.0", 
      "%% 作者: 青野雅樹:01162069",
      "%% ファイル名: ",
      "%% 日付:",
  };
  public void myPsPrint(PrintStream cout, String name){/* PS出力のヘッダー */
        Date now = new Date();/* Dateクラスのオブジェクト生成 */
        Format fmt= /* 日付を日本語ロカールに従ってフォーマット */
             DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.LONG);
        for (int i = 0 ; i < myPsName.length ; i++){
             cout.print(myPsName[i]);
             switch (i){
                  case 2: cout.print(name); break;/* データファイル名 */
                  case 3: cout.print(fmt.format(now)); break; /* 日付 */
             }
             cout.print('\n');
        }
  }

  public static void main(String[] args){/* ここからmain関数 */
     if (args.length != 2){
        System.err.println("java ShapeInheritance output.ps total_shapes");
        System.out.println("[x1,x2]の間の図形の総数を引数で与えてください");
        System.exit(-1);
     }
     /* 2次元座標 */
     /* 半径 */
     /* Shape2Dクラスを要素として持つ、動的な配列リストクラスを利用 */
     ArrayList<Shape2D> list = new ArrayList<Shape2D>();
     Shape2D shape = null;/* 抽象基底クラスの変数:宣言はOK */

     Date date = new Date();/* 現在の日付(乱数の初期値用)*/
     Random rand = new Random(date.getTime());/* 乱数クラス */

     try {
       int n = Integer.valueOf(args[1]).intValue();/* 発生する図形総数:変動可 */
       if (!(x1 <= n && n <= x2)){
			System.err.println("図形の総数はx1からx2の間の数にしてください");
			System.exit(1);
       }
       for (int j = 0 ; j < n ; j++){ 
         /* 色の設定は、このプログラムではしない。課題ではこのあたりで行う */
         int shapeIndex = rand.nextInt(2);/* returns 0 or 1 */
         v1 = new Coord2(
              /* [0.0-XRANGE]の乱数を発生 */
              /* [0.0-YRANGE]の乱数を発生 */
          switch ( shapeIndex ){
             case 0:/* 三角形を生成:triangle */
                  v2 = new Coord2(
                        /* [0.0-XRANGE]の乱数を発生 */
                        /* [0.0-YRANGE]の乱数を発生 */
                  v3 = new Coord2(
                        /* [0.0-XRANGE]の乱数を発生 */
                        /* [0.0-YRANGE]の乱数を発生 */
                  shape = /* 派生クラス(三角形)のインスタンス生成*/ 
                  break;
             case 1:/* 円を生成:circle */
                  /* [0.0-RADIUS]の乱数を発生 */
                  shape =  /* 派生クラス(円)のインスタンス生成*/ 
                  break;
          }
          list.add(shape);/* ArrayListにShape2Dクラスのデータを追加 */
       }

      ShapeInheritance shapeIn= new ShapeInheritance();/* ShapeInheritanceクラス生成 */
      PrintStream cout = new PrintStream(args[0], "UTF-8");/* プリント出力するファイル名 */
      shapeIn.myPsPrint(cout, args[0]); /* ヘッダー部分のプリント */
      int count = 1;
      for (int i = 0 ; i < list.size(); i++ ){/* ArrayListのサイズ回ループ */
           /* i-番目の要素を取り出す */
           cout.println( "%%" + count + "番目の図形" );
           /* 派生クラスに応じたpsPrintメソッド */
           count++;
      }
      /* このあたりで総面積、総長、図形総数などを書き出す */
      cout.println( "showpage" );
     }
     catch (NumberFormatException e){
            System.err.println("引数は[x1-x2]の間の整数にしてください");
            System.exit(1);
     }
     catch (FileNotFoundException e){
            System.err.println("ファイル"+args[1]+"は、見つかりません");
            System.exit(1);
     }
     catch (UnsupportedEncodingException e){
           System.err.println("UTF-8符号はサポートされていません");
           System.exit(1);
     }
     catch (Exception e){
           System.err.println("想定外のエラーです");
           e.printStackTrace();
           System.exit(1);
      }
  }
}
プログラム4-6には、抽象クラスの使い方だけでなく、 これまで使ってきた自前の線形リストの代わりに利用している、 Java言語に用意されているArrayListクラス(動的配列リスト)の使用例にもなっています。 ArrayListは任意のオブジェクトの可変長の配列を実現するために、 Java5以来導入されたジェネリクスを用いています。 プログラムの43行目にある

ArrayList<Shape2D> list = new ArrayList<Shape2D>();

の三角括弧が、ジェネリクス特有の記法となっています。 つまり、この(三角)括弧内にArrayListクラスの要素のデータ型を書くことができます。 ArrayListクラスそのものに関しては、 そこにデータを追加するaddメソッド(45行目)やデータを参照するgetメソッド(57行目)が用意されています。

Shape2Dクラスの派生クラスのインスタンスは、 62行目のTriangleクラスならびに66行目のCircleクラスのインスタンスをShape2Dクラスの変数に代入しています。 ポイントは、抽象クラスのインスタンスは作れないが、派生クラスのインスタンスを抽象クラスの変数に代入することはできる、という点です。 これが80行目で、抽象関数のpsPrintが呼ばれたときに、 図形に応じた面積や周囲長のPostScript言語中のコメントとして書き出されます。 以下がそのコマンド実行例と、output.psの中身、 ならびに、これをPDFに変換したときの描画された図形の様子例でです。

$ javacu Coord2.java

$ javacu Shape2D.java

$ javacu Triangle.java

$ javacu Circle.java

$ javacu ShapeInheritance.java

$ javau ShapeInheritance output.ps 30
抽象クラスや派生クラス等をコンパイル、実行している様子


これは、コマンド実行例ですが、前回に紹介したAntを使うとまとめてコンパイルできます。 前回とほぼ同様のフォルダ構成にして、たとえば、以下のbuild.xmlを使います。
build.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<project name="Shape2D" default="compile" basedir=".">
     <!-- プロパティの定義 -->
     <property name="src" location="src"/>
     <property name="classes" location="classes"/>
     <property name="jar" value="lib/myShapeInheritance.jar"/>
     <property name="args" value="10"/>
     <property name="outFile" value="output.ps"/>
     <property name="fileEncoding" value="-encoding UTF-8"/>
     <!-- compileタスク -->
     <target name="compile">
          <javac srcdir="${src}" destdir = "${classes}" includeAntRuntime="No">
               <compilerarg line="${fileEncoding}" />
               <classpath/>
          </javac>
     </target>
     <!-- runタスク -->
     <target name="run" depends="compile">
          <java classname="ShapeInheritance">
               <arg line="${outFile} ${args}" />
               <classpath>
               <pathelement location="./classes"/>
               </classpath>
          </java>
     </target>
     <!-- jarタスク -->
     <target name="jar" depends="compile">
          <jar destfile="${jar}" basedir="${classes}" 
               manifest="META-INF/ShapeInheritance.mf">
               <fileset dir="${classes}" includes="${classes}/*.class" />
          </jar>
     </target>
     <!-- cleanタスク -->
     <target name="clean">
          <delete dir="${classes}"/>
          <mkdir dir="${classes}"/>
     </target>
</project>
以下がAntでコンパイルと実行をしたところです。

Antでコンパイルならびにjarファイルを作成する様子


実行は2種類で行っています。最初は、 jarタスクで作成したライブラリを使った実行例です。 実行の後半(runタスク)において-DoutFile="output2.ps"は出力プリントストリームをoutput2.psという名前に変更することを表します。 また、-Dargs="30"は、30個の乱数を発生することを表します。

javauでjarファイルから実行する様子、ならびにAntでパラメータを変更して実行する様子


出力されたPostScriptファイルの中身の例は以下のようです。

%%!PS-Adobe-3.0
%% 作者: 青野雅樹:01162069
%% ファイル名: output.ps
%% 日付:2019年10月28日 14:40:22 JST
%%1番目の図形
% 三角形 面積 = 19831.933
% 三角形 周囲長 = 505.349
newpath
360.049 146.142 moveto
50.941 97.498 lineto
244.312 292.672 lineto
closepath
stroke
%%2番目の図形
% 円 面積 = 6573.355
% 円 周囲長 = 287.408
newpath
218.657 63.710 45.742 0 360 arc
stroke
     PostScriptの先頭付近


PostScript出力に関して図形の色が必要であれば、たとえば
0.0 1.0 1.0 setrgbcolor とすれば、それ以降の描画が水色(シアン)になり、また、 たとえば
1.0 0.0 1.0 setrgbcolor とすれば、それ以降の描画がマゼンタになります。
一般に光の三原色は以下のように呼称します。

光の三原色とその合成色


その他、図形ごとに面積、周囲長がコメント (先頭が%記号の行がコメント行です)で記述されているのがわかるかと思います。

PostScriptを表示したところ(Acrobat等で一旦PDFにしてからも可視化できる


最後の例は、PostScriptをPDFに変換したものですが、 gsviewやgs (GhostScript)などがあれば、PostScriptのまま、可視化して結果を確認することができます。 参考までに、プログラム4-6の 出力サンプルを おいています。

クラスの継承(別の事例)

抽象クラスを使わなくても、クラスの継承はとても便利です。 素朴な対戦型ゲームっぽいクラスを例にとり、別な継承事例を紹介します。
Character.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
/*
	プログラム4-7: Characterクラス(基底クラス)
*/
public class Character {

     String name; /* 名前 */
     int level; /* レベル */
     int speed; /* すばやさ */
     int experiences; /* 経験値 */

     int HP; /* Hit Point */
     int MP; /* Magic Point */
     private boolean alive; /* HP >= 0 の状態 */

     int portion; /* 薬草 */
     final int portionRecover = 30;/* HP += 30 で固定 */

     /* コンストラクタ */
     public Character(){
          level = experiences = speed  = 1; 
          portion = 10;
          alive = true; name = "";
     }

     public Character(int level){ /* オーバーロード(その1)*/
          this();
          this.level = level;
          HP = 10 * level; MP = level; /* HPとMPの初期化 */
     }
     public Character(String name, int level){ /* オーバーロード(その2)*/
          this(level);
          this.name = name; 
     }
     /* print関数 */
     public void print(){
          System.out.println("  [Character] 名前:"+name+"    HP: "+HP + "  MP: "+MP);
     }
     public void printAll(){
          print();
          System.out.println("    レベル: "+level+" スピード: "+speed+" 経験値: "+experiences);
     }

     public int power(){ return level * experiences; }     /* 力の強さを返す関数 */
     public void checkHP(){ if (HP <= 0) alive = false; else alive = true; }
     public void attack(Character x){ if (HP >0) x.HP -= power(); }
     public void attacked(Character x){ HP -= x.power(); if (HP <= 0) alive = false; }
     public void heal(Character x){ if (portion>0) { x.HP += portionRecover; portion--;}}
     public void healed(Character x){ if (x.portion>0) { HP += portionRecover; x.portion--;} }
     public void spell(Character x){ if (MP > 0) {MP -= power(); attack(x); }}
     public void spelled(Character x){ x.MP -= power(); attacked(x); }
     public boolean isAlive(){ return alive;}
} /* Characterクラスのおわり */
上の例で着目してほしいのは、コンストラクタが2種類オーバーロードされているという点です。 しかも、オーバーロードしている関数の先頭で、

this();
this(level);
のように、this()関数が使われています。このthisはCharacterに置き換えて考えると、 意味がわかるかと思います。 superもそうですが、Javaではthisも変数として自分自身を指すだけでなく、 関数としてコンストラクタを呼び出すことができます。ただし、このthis関数は関数内の 先頭でのみ使えるという決まりになっています。
Soldier.java
1 
2 
3 
4 
5 
6 
7 
8 
9
10
/*
	プログラム4-8: 戦士クラス
*/
public class Soldier extends Character {
     final int powerScale = 2;/* 2倍のパワー */ 
     public Soldier(String name, int level){ super(name, level);}
     public void attack(Character x) { /* オーバーライド */
         x.HP -= powerScale * power(); 
    } 
} /* Soldierクラスのおわり */
Magician.java
1 
2 
3 
4 
5 
6 
7 
8 
9
10
11
12
13
14
15
/*
	プログラム4-9: 魔術師クラス
*/
public class Magician extends Character {
     final int magicScale = 2;/* 2倍のパワー */ 
     public Magician(String name, int level){ super(name, level);}
     public void spell(Character x) { /* オーバーライド */
          if (MP <= 0) {
               System.out.println("MPがありません");
               return;
          }
          MP -= magicScale; 
          x.HP -= magicScale * power(); 
     }
} /* Magicianクラスのおわり */
GameMain.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
/*
	プログラム4-10: GameMainクラス
*/
import java.util.Random;/* 乱数 */
import java.util.Date;/* 日付 */
public class GameMain {

     final static double Threshold = 0.80;/* 閾値 */
     final static double Half = 0.50;/* 50-50 閾値 */
     Random rand;
     public GameMain(){
          Date date = new Date();/* Dateクラスのオブジェクト生成 */
          rand = new Random(date.getTime());/* 乱数クラス */
     }
     void Stop(Character x){
          x.level++; x.speed++; x.experiences += 10;
          System.out.println(x.name+"が勝ちました");
          System.exit(0);
     }
     double Probability(){
          return(rand.nextDouble());/* return probability */
     }
     public static void main(String[] args){
          GameMain gm = new GameMain();
          Character[] opponent = new Character[2];/* 登場人物はとりあえず2人 */
          Character x, y;
          x = opponent[0] = new Soldier("ファイタ",20); x.printAll();
          y = opponent[1] = new Magician("マーリン",20); y.printAll();
          while (x.isAlive() || y.isAlive()){
               if (gm.Probability() > Half){
                    if ( x instanceof Soldier ) { // Character xがSoldierの場合
                         if (gm.Probability() < Threshold){
                              System.out.println(x.name+"が素手で攻撃しました");
                              x.attack(y); /* 素手で攻撃した場合 */
                         }
                         else {
                              System.out.println(x.name+"が魔法で攻撃しました");
                              x.spell(y); /* 魔法で攻撃した場合 */
                         }
                         y.print();
                         if (x.isAlive() && !y.isAlive()){ /* x wins */ gm.Stop(x);  }
                    }
               }
               else {
                    if ( y instanceof Magician ) {// Character yがMagicianの場合
                         if (gm.Probability() < Threshold){
                              System.out.println(y.name+"が魔法で攻撃しました");
                              y.spell(x);  /* 魔法で攻撃した場合 */
                         }
                         else {
                              System.out.println(y.name+"が素手で攻撃しました");
                              y.attack(x); /* 素手で攻撃した場合 */
                         }
                         x.print();
                         if (!x.isAlive() && y.isAlive()){ /* y wins */ gm.Stop(y);  }
                    }
               }

               x.checkHP();// xのHPのチェック
               y.checkHP();// yのHPのチェック
               if (x.isAlive() && !y.isAlive()){ /* x wins */ gm.Stop(x);  }
               if (!x.isAlive() && y.isAlive()){ /* y wins */ gm.Stop(y);  }
          }
     }
} /* GameMainクラスのおわり */
GameMain.javaでは、instanceofという予約語を使い、継承された派生クラス名をチェックする例が含まれています。 xとyの変数が基底クラスであるため、 どの派生クラスであるかを調べるために使っています。 このプログラムでは、xもyもあらかじめ派生クラスがわかっているため、 一見無駄なように見えますが、 もしアルゴリズムを少し改良して、プレーヤーすらもランダムに、
Character[] x = new Character[10];
for (int i = 0 ; i < x.length; i++){
	if (gm.Probability() < Half)
		x[i] = new Soldier("適当な戦士の名前",適当なレベル);
	else
		x[i] = new Magician("適当な魔法使いの名前",適当なレベル);
}
のように作成した場合、instanceofの価値を感じることができるかと思います。

以下は、このGameMainを実行した様子です。 最初はファイタが勝ち、次はマーリンが勝っています。 乱数と確率をうまく利用すると、簡単なゲームであれば、 意外とシンプルにプログラムを作ることができます。

$ ant -e run |nkf -w
run:
  [Character] 名前:ファイタ    HP: 200  MP: 20
    レベル: 20 スピード: 1 経験値: 1
  [Character] 名前:マーリン    HP: 200  MP: 20
    レベル: 20 スピード: 1 経験値: 1
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 160  MP: 20
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 160  MP: 20
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 120  MP: 20
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 80  MP: 20
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 120  MP: 14
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 40  MP: 20
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 80  MP: 12
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 0  MP: 20
マーリンが勝ちました

$ ant -e run |nkf -w
run:
  [Character] 名前:ファイタ    HP: 200  MP: 20
    レベル: 20 スピード: 1 経験値: 1
  [Character] 名前:マーリン    HP: 200  MP: 20
    レベル: 20 スピード: 1 経験値: 1
ファイタが魔法で攻撃しました
  [Character] 名前:マーリン    HP: 160  MP: 20
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 120  MP: 20
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 80  MP: 20
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 160  MP: 0
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 120  MP: 0
マーリンが魔法で攻撃しました
  [Character] 名前:ファイタ    HP: 80  MP: 0
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 40  MP: 14
ファイタが素手で攻撃しました
  [Character] 名前:マーリン    HP: 0  MP: 14
ファイタが勝ちました
シンプルなRPG的ゲームを2回実行した結果例



Java言語で利用できるユーティリティ(java.utilパッケージにあるクラス)特にジェネリクス

Java言語の代表的なユーティリティとジェネリクスに関して簡単に述べます。 実は、すでに、Scanner, Date, Random, ArrayListなどのユーティリティを使ってきています。 更に、上述のプログラムでは、ArrayListの使用例を述べました。 ArrayListでは、任意のオブジェクトを(三角)括弧内に書いて、 それを可変長配列リストに入れて保持することができます。 ただし、単純型はできません。 たとえば、 ArrayList<double> list;のような宣言はできません。 その代り、配列にするとオブジェクトになりますので、 ArrayList<double[]> list;や ArrayList<double[][]> list; のような定義は可能です。また、基本データ型のラッパークラス(大文字ではじまるJava特有のクラス)であれば、 ArrayList<Double> list;のように定義可能です。

HashMap

ユーティリティにあるクラスでジェネリクスを利用例 として有名なものに、 HashMapがあります。 ArrayListはひとつのオブジェクトを可変子の配列リストに保持しました。 ハッシングでは、ハッシュ関数に(キー、値)のペアを与えてハッシュ・テーブルを作成します。



ファイルをまとめるTips

課題をMoodleに提出するさいに、複数のファイルをまとめて提出することを勧めます。 たとえばZIPをターミナルから行う場合、kadaiXというフォルダーで作業を した場合、

$ zip -r kadaiX.zip kadaiX
のようにすればZIPを作成できます。 tgzであれば、以下のようにできます。

$ tar cvzf kadaiX.tgz kadaiX
Moodleには、kadaiX.zipやkadaiX.tgzをアップすれば結構です。 なお、必要なファイルや実行結果がすべてkadaiXというフォルダー内にあることを確認しておいてください。

ジェネリクス

Java言語には、2004年に改定されたJava5より、 ジェネリクス(Generics)という概念が登場しました。 キャッチフレーズとしては、オブジェクトの型をできるだけ明確にし、 同時に抽象化と一般化を目指すための記述ロジックと思えば、だいたいあたっています。 実質的にはC++言語のテンプレートと似ていて三角括弧を用いて、オブジェクトを指定するような記述方法を用いる各種のクラスやインタフェースのことです。 インタフェースに関する様々な事例は、 JavaのGUIを扱うSwingにおいて、より具体的な事例を述べる予定ですが、

簡単に説明すると、インタフェースとは、クラス同様、 新たな参照型を定義できるものです。 しかし、通常のクラスとは違って、 すべてのメソッド(関数)が抽象関数として定義されるものです。 前回、抽象クラスをクラスの継承例で扱いましたが、 これをさらに発展させ、便利にしたものです。 つまり、抽象クラスを定義した場合では、 それを継承するクラスですべての関数(メソッド)を実装し、その際、 extends BaseのようにBaseクラスを継承するという明示的な継承関係が必要でした。 この継承関係の問題点は、 複数の便利な(抽象クラスで定義された)基底クラスがある場合、 その派生クラスは、それかひとつの基底クラスしかextendsできない、という点です。 しかし、様々なアルゴリズムを実装する場合、 あるいはGUIを作成する場合の様々なイベント(たとえばマウスのクリックや移動などのイベント)を取得したい場合、 しばしば、複数の基本的なデータ構造を混合したアルゴリズムや、 複数のイベントを同時に取得できる手法があると有用である場合が多いかと思います。 このような要望に応えられるのがインタフェースという仕組みです。 C++言語でもっとも近い概念は、多重継承(multiple inheritance)です。 インタフェースを定義した場合、抽象クラスと同様に、 それを使いたいクラスで関数をすべて定義する必要があります。 しかし、抽象クラスと違って、extends Baseとしないで、 implements Aのように記述します。 意味は、Aというインタフェースを実装すると呼びます。 今回の資料自体には、implementsの事例は出てきませんが、 クラスの継承と違って、 implements A, B, Cのように、 いくらでも複数のインタフェースを実装している宣言が可能です。 また、 extends X implements A, B, Cのように、 Xというクラスを継承し、A,B,Cというインタフェースを実装する、といったクラスを定義することも可能です。

Set<E>

最初に、Setインタフェースの説明をします。 英語の名前の通り、Setインタフェースは集合を定義するインタフェースです。 2次元図形の継承例で ArrayListクラスを紹介しました。 Setインタフェースは、 Collectionクラス(集合体クラス)を継承するインタフェースです。 集合体を継承しますが、Collectionクラスとの違いは、 Setインタフェースに含まれるエレメント(Element)<E>には、重複する要素がありません。 また、Set自体はインタフェースなので、Setインタフェースのオブジェクトを直接作成することはありませんが、 今回のテーマであるHashMapなどのMapを実装するクラスから、 entrySet()関数でSetインタフェースを取出すことができます。 集合内のデータには、順序がありません。 従って、集合内のエントリを全部数え上げる、 ような操作が必要な場合、 ArrayListでget(i)のようなi番目の要素を得る方法とは別の方法でアクセスすることが求められます。 このために用いるSetインタフェース内の関数はiterator()というIteratorインタフェースを返す関数です。 このIteratorインタフェースこそが、 順序のない集合データ(ハッシュ表もこの一種です)を網羅的に走査できる便利なインタフェースを提供してくれます。 もし、Iteratorインタフェースが得られたとすると、 このデータを走査するには、以下のようなforループで実現できます。
for ( Iterator it = set.iterator() ; it.hasNext();){
    processObject( it.next() );
}
あるいは以下のようなwhileループでも実現できます。
Iterator it = set.iterator();
while ( it.hasNext() ) {
    processObject( it.next() );
}

Map<K,V>

HashMapもMapの一種ですが、まず、Mapインタフェースの説明をします。 Setインタフェースでは、集合の要素だけ与えました。 一方、Mapインタフェースでは、<K, V>のようにキーK (Key) と値V (Value)のペアを与え、 これを集合体で保持する仕組みを与えてくれます。 Setインタフェースと同様、キーとなる要素に重複は許されません。 個々のキーに値が付随します。 従って、キーから値を検索したいようなアプリケーションで威力を発揮します。 Mapインタフェースに用意されている関数で重要なものは、以下に列挙するものです。 Iteratorインタフェースのit.next()でオブジェクトが返ります。 このオブジェクトはMapインタフェースの場合、 キーと値のペアで、通常、java.util.Map.Entryインタフェースの要素となります。 今、仮に文字列Stringをキーとし、 Vという値クラスのオブジェクトを値とするMapの要素にアクセスしたい場合、以下のようなプログラムでアクセスすることができます。
Object o = it.next();/* 次要素は、一般的な「オブジェクト」として得られる */
Entry entry = (java.util.Map.Entry)o;/* 「オブジェクト」をエントリにキャスト */
String key = (String)entry.getKey();/* エントリキーをStringにキャスト */
V value = (V)entry.getValue();/* エントリ値をVにキャスト */
boolean containsKey(Object key);;/* keyで与えられる要素がMapにあるかどうか */

HashMap<K,V>

HashMapクラスを述べます。HashMapは、Mapインタフェースを実装するクラスです。 HashMapクラスは、オブジェクトのインスタンスをnew演算子で作成できます。 Mapインタフェースを実装しているため、 Mapインタフェースで定義される関数はどれも同じように使用できます。 通常のMapインタフェースとhashMapの違いは、 キーKと値Vのペアを内部的にハッシュ表で管理しますので、そのアクセスが高速である点です。 また、HashMapクラス内には、Setインタフェース同様、 Setインタフェースを返すentrySet()関数が利用できますので、 ここから最終的にIteratorインタフェースにアクセスでき、 HashMap内のすべての要素をIteratorを用いて網羅的に走査することができます。 なお、この際、順序がないため、たとえば、キーの値の小さい順に走査する、といったことはできません。 しかし、HashMapの進化形の SortedMap インタフェースを使うと、 キーで指定した順序(あるいは、キーに本来備わる自然な順序)で要素を走査することができます。 SortedMapはインタフェースなのでインスタンスは作れません。 その代り、SortedMapを実装するTreeMapというクラスがあるので、 TreeMapクラスをHashMapのように使いながら、 同時に、Iteratorで走査した結果がソートされた順序になる便利なクラスもあります。 このあたりは、TPOに応じて使い分けてください。 以下では、HashMapクラスと関連するTreeMapクラスを使って、プログラム2-7で紹介したものと目的はほぼ同じですが、 英語のニュースから英単語を取出し、 これをハッシュ表で管理するサンプルプログラムを紹介します。 なお、47-48行目あたりにあるデリミタは、本来は、たとえばjava.util.regexパッケージにあるPatternクラスを使った正規表現等で表現し、 split関数等で単語を分離したほうが、より洗練されたプログラムとなると思います。
WordCount.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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*
	プログラム4-11: ニュースから出現する英単語をソートして書き出すプログラム
*/
import java.io.*;
import java.util.TreeMap;/* HashMapとほぼ同じだが、キーでソーティングしてくれる */
import java.util.Set;/* Setインタフェース(集合インタフェース) */
import java.util.Iterator;/* Iteratorインタフェース(順序のないデータを数えあげる) */
import java.util.Map.Entry;/* 「ペア」のエントリを保持するインタフェース */

public class WordCount {/* 英単語とその出現頻度をハッシュ表で保持するクラス */
     /* ハッシュ表:<String,Integer>のペアをジェネリクスで与えている。HashMapでもよい。ソートのためTreeMap使用 */
     TreeMap<String,Integer>map = new TreeMap<String, Integer>();

     /* 結果のプリント */
     public int print(){/* ハッシュ表のエントリをプリントする関数 */
          String s;/* 文字列変数 */
          int total = 0; /* エントリ総数 */
          Set set = map.entrySet();/* TreeMap用の変数からentrySet集合を得る */
          Iterator it = set.iterator();/* 集合からイタレータを得る */
          while (it.hasNext()){/* 集合の(次)要素がある限り */
               Object o = it.next();/* 次要素は、一般的な「オブジェクト」として得られる */
               Entry entry = (java.util.Map.Entry)o;/* 「オブジェクト」をエントリにキャスト */
               String key = (String)entry.getKey();/* エントリキーをStringにキャスト */
               Integer value = (Integer)entry.getValue();/* エントリ値をIntegerにキャスト */
               s = String.format("%-20s",key);/* 左詰めでプリントするためのフォーマット */
               System.out.println(" 単語:"+s+"\t 出現回数:"+value.intValue());
               total++;
          }
          System.out.println("全部で"+ total +"種類の単語が現れました。");
          return(total);
     }

     public int lookup(String str){ /* 単語がすでに登録されているかどうかを調べる関数 */
          Integer value = (Integer)map.get(str);/* ハッシュ表にキーでアクセスし、Integerクラスの値にキャスト */
          if (map.containsKey(str)){/* 見つかった */
               int count = value.intValue() + 1; /* カウントアップ */
               map.put(str, new Integer(count));/* ハッシュ表を書き換える */
               return 0;
          }
          else {
               value = new Integer(1);/* 新規エントリを作る */
               map.put(str, value);/* 新規エントリをハッシュ表に追加 */
               return 1;/* 新規作成終了 */
          }
     }

     public static char[] delimiter = {/* デリミタ文字 */
          '\t', '/', ',', '.', '(', ')', '[', ']', '<', '>', '{', '}', '=', '!', '"', '\'', '?', '_', ';', ':'
     };
     public static String filter(String line){/* デリミタ文字をスペースで置き換える */
          if (line == null || line == "") return line;
          for (int i = 0 ; i < line.length() ; i++ ){
               for (int j = 0 ; j < delimiter.length ; j++){
                    if (line.charAt(i) == delimiter[j]) {
                         if (i == 0){/* 最初の文字 */
                              line = line.substring(i+1); break;
                         }
                         if (i == line.length()-1){/* 最後の文字 */
                              line = line.substring(0,i); break; 
                         }
                         line = line.substring(0,i)+" "+line.substring(i+1); break;
                    }
               }
          }
          return line;
     }

     public static void main(String[] args){/* main関数 */
          int total = 0;/* 総単語数 */
          WordCount wc = new WordCount();/* このクラスのインスタンス作成 */
          try {
               FileInputStream fis = new FileInputStream(args[0]);/* ニュースファイル読込 */
               InputStreamReader isr = new InputStreamReader(fis, "UTF-8");/* 符号を指定 */
               BufferedReader br = new BufferedReader(isr);/* 行単位でバッファリングして読込み */
               String line = null;/* 各行の文字列を保持する変数 */
               String token = null;/* 各行から英単語候補を保持する文字列変数 */
               String[] result = null;/* splitされた文字列保持用 */
               int n;/* トークン(英単語数)の総数を保持する変数 */
               char c;/* 英単語候補の文字列の最初の文字を判定用に保持する変数 */

               while ((line = br.readLine()) != null){/* 行単位で読込み、それがEOFでない間 */
                    line = filter(line);/* デリミタの削除 */
                    result = line.split("\\s+");/* splitでトークンに分割 */
                    n = result.length;/* トークン総数 */
                    for (int i = 0 ; i < n ; i++){
                         token = result[i];/* i番目のトークン(英単語候補)を読込む */
                         if (token==null || token=="" || token.length() <1) continue;
                         c = token.charAt(0);/* 先頭文字 */
                         if (!Character.isLetter(c))/* アルファベットでない場合 */
                              continue;
                         wc.lookup(token);/* 英単語を登録、またはカウントアップ */
                    }
               }
               /* 結果のプリントを総単語数のセット: TreeMapなのでソートされる */
               total = wc.print();
          }
          catch (IOException e){/* 例外処理 */
               System.out.println("ファイル"+args[0]+"は、存在しません");
               e.printStackTrace();
          }
     }
}
以下がこのプログラムの出力の一部です。
$ javau WordCount news11.txt
 単語:BarronBut                 出現回数:1
 単語:CNN                       出現回数:1
 単語:House                     出現回数:1
 単語:If                        出現回数:1
 単語:Japan                     出現回数:4
 単語:Japanese                  出現回数:1
 単語:Korea                     出現回数:1
 単語:Land                      出現回数:1
 単語:Machiavellian             出現回数:1
 単語:North                     出現回数:1
 単語:Rising                    出現回数:1
 単語:S                         出現回数:4
 単語:Sandra                    出現回数:1
 単語:States                    出現回数:1
 単語:Sun                       出現回数:1
 単語:The                       出現回数:1
 単語:Tokyo                     出現回数:2
 単語:U                         出現回数:4
 単語:United                    出現回数:1
 単語:White                     出現回数:1
 単語:a                         出現回数:3
 単語:about                     出現回数:1
 単語:affair                    出現回数:1
 単語:aghast                    出現回数:1
 単語:air                       出現回数:1
 単語:and                       出現回数:3
 単語:anyone                    出現回数:1
 単語:are                       出現回数:4
 単語:aren                      出現回数:1
 単語:as                        出現回数:2
 単語:at                        出現回数:1
なお、news11.txtの中身は以下の通りです。

(CNN) -- If the U.S. election race conjures up images of mud flying through the air for many Japanese, campaigning politicians in the Land of the Rising Sun evoke visions of a more white-gloved affair.

Japan's politics are as Machiavellian as anyone else's behind closed doors, but their public campaigns are demure compared to the United States -- and many in Tokyo are aghast at the negative campaign tactics used on the road to the White House.

Japan has plenty on its mind these days. The country is wrangling with questions about how to rebuild its tsunami-devastated coast, what to do with its idled nuclear reactors, and whether a tax hike will solve its economic woes, so it's no surprise if people in Tokyo aren't riveted to the lead-up to the U.S. elections.

Sandra BarronBut the U.S. is important to Japan's economy and to Japan's increasingly rocky relationship with North Korea, so people are not ignoring it completely -- even if U.S. politics falls somewhere below the retirement of a young pop idol in the morning shows' news order.

簡単なジェネリクスのサンプル

プログラム4-11では、TreeMapというシステムが用意したクラスに キーとなる文字列と値としての整数を用いて、

TreeMap<String,Integer>map = new TreeMap<String, Integer>();
という文でジェネリクスを利用しました。 ジェネリクスを用いると、データ型を汎化することができます。 よりシンプルなジェネリクスの利用例を以下に紹介します。

GenericsType.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
/*
	プログラム4-12: 簡単なジェネリクスのサンプル
*/
import java.util.ArrayList;/* ArrayListクラスをインポート */

public class GenericType<T> {/* 型Tを一般化したクラス */

    private T t;/* 型Tのデータ */
    public T get(){/* 型Tのデータを返す */
        return this.t;
    }
    public void set(T t){/* 型Tのデータをセットする */
        this.t = t;
    }

    public static void main(String args[]){
        GenericType<String> g1 = new GenericType<String>();/* String型でオブジェクト生成 */
        g1.set("文字列"); /* 文字列をセット */
        System.out.println("クラス名 ("+g1.getClass()+") value = "+g1.get());
         
        GenericType <Integer>g2 = new GenericType<Integer>(); /* Integerで生成 */
        g2.set(100); /* 整数値100をセット */
        System.out.println("クラス名 ("+g2.getClass()+") value = "+g2.get());
        
        GenericType <double[]> g3 = new GenericType <double[]>(); /* double[]で生成 */
        double[] a = {1.0, -1.0, 2.0, -2.0};/* double型の配列 */
        g3.set(a);/* <double[]>型のオブジェクトをセット */
        System.out.print("クラス名 ("+g3.getClass()+") value = ");
        double[] b = g3.get();/* g3オブジェクトをゲット */
        for (int i = 0 ; i < a.length ; i++)/* 配列の長さの間、要素をプリント */
            System.out.print(b[i]+" ");
        System.out.print("\n");/* 改行 */

        GenericType <ArrayList<Double>> g4 = new GenericType<ArrayList<Double>>(); 
        ArrayList<Double> alist = new ArrayList<Double>();/* ArrayListオブジェクト生成 */
        alist.add(1.0);/* 1.0を追加 */
        alist.add(-1.0);/* -1.0を追加 */
        alist.add(2.0);/* 2.0を追加 */
        alist.add(-2.0);/* -2.0を追加 */
        g4.set(alist);/* <ArrayList<Double>>型のオブジェクトをセット */
        System.out.print("クラス名 ("+g4.getClass()+") value = ");
        ArrayList<Double> blist = g4.get();/* blistにget関数の結果を代入 */
        for (int i = 0 ; i < blist.size(); i++)/* ArrayListの長さの間、要素をプリント */
            System.out.print(blist.get(i)+" ");
        System.out.print("\n");/* 改行 */
    }
}
実行結果は以下のようです。

$ javacu GenericType.java

$ javau GenericType
クラス名 (class GenericType) value = 文字列
クラス名 (class GenericType) value = 100
クラス名 (class GenericType) value = 1.0 -1.0 2.0 -2.0
クラス名 (class GenericType) value = 1.0 -1.0 2.0 -2.0
簡単なジェネリック型のテストプログラムの実行結果