Skip to content.

Sections
Personal tools
You are here: Home » 技術文書 » Java » Java Generics概説

Document Actions
Java Generics 概説 - Java への型総称性の導入

(株)永和システムマネジメント    平鍋健児
作成日:2002, 2/20

Java の言語仕様に型総称性(Generics)を導入しようという提案, 「JSR14」の ドラフトが公開された.実際に正式な言語仕様となるかどうかは今後JCP (Java Community Process)でのレビューが進むにつれて明らかになるであろうが, 現時点も仕様の完成度は非常に高い.公開に合わせて,参照コンパイラも提供 されており,Java2 SDK 1.5でこの機能が入ることが期待されている.

この記事では,このJSR14(Adding Generics to the Java Programming Language: Participant Draft Specification, April 27, 2001)について解説する.

   はじめに

最初にJava Genericsの導入動機,必要性などを解説した後, その仕様について見ていこう.また,Genericsを使ったサンプルも解説する.

なお,JSR14について詳細は,以下から参照できる.

http://java.sun.com/aboutJava/communityprocess/review/jsr014/index.html


   Hello Generics

まずは,簡単なコードを見てみよう.List 1 にJava Generics使用前と使用後 のコードを比較した.プログラムは,Vectorに文字列を詰めそれを取りだす,という簡単なものだ.


List.1 Hello Generics
J2SE1.3(Java2 SDK 1.3)のコード
import java.util.Vector;

public class HelloGenerics {
  public static void main(String[] args) {
	// Stringだけ入れたいVector
	Vector strings = new Vector();
	// Stringを追加
	strings.add("Hello Generics");
	// 取得にキャスト必要
	String s = (String)strings.get(0);
	
	// 誤ってIntegerを追加できる
	strings.add(new Integer(0));
	Integer i = (Integer)strings.get(1);
  }
}
Java Generics を用いたコード
import java.util.Vector;

public class HelloGenerics {
  public static void main(String[] args) {
	// StringのVector
	Vector<String> strings = new Vector<String>();
	// Stringを追加
	strings.add("Hello Generics");
	// 取得にキャスト不用
	String s = strings.get(0);
	
	// 誤ってIntegerを追加.コンパイルエラー!
	strings.add(new Integer(0));
	// 誤ってIntegerを取得.コンパイルエラー!
	Integer i = strings.get(1);
  }
}

JavaGenericsでは,Vector<String>と書いてStringを要素に持つVectorを表現 する.これによって,Vectorへの要素の追加,取得の際にStringかどうかをコンパイラが型チェックできる.

Genericsを使ったコードの特徴は以下の通りだ.

  • Vectorの宣言と生成に型パラメータ<String>などが指定できる
    • J2SE1.3 : Vector strings = new Vector();
    • Java Generics: Vector<String> strings = new Vector<String>();
  • Vectorの要素の取り出し時にキャストが不用
    • J2SE1.3 : String s = (String)strings.get(i); // 取得にキャスト必要
    • Java Generics: String s = strings.get(i); // 取得にキャスト不用
  • 型パラメータ指定されたVectorへの要素追加は型チェックされる
    • J2SE1.3 : strings.add(new Integer(0)); // 誤ってInteger を追加できる
    • Java Generics: strings.add(new Integer(0)); // 誤ってInteger を追加.コンパイルエラー!
  • 型パラメータ指定されたVectorからの要素取得は型チェックされる
    • J2SE1.3 : Integer i = (Integer)strings.get(1);
    • Java Generics: Integer i = strings.get(1); // Integerを取得.コンパイルエラー!

Vectorをはじめとするjava.utilパッケージ内のCollection系クラス/インター フェイス群は,Object型を要素として扱うようになっている.これによって, どんなクラスのオブジェクトでもコレクションに追加することができる.しか し,Javaプログラミングに慣れている読者の方は「Vectorに詰め込んだ 要素はキャストして取り出さなくてはならない」という制約に苛立ちを感じ続 けていたはずだ.また,どんなクラスのオブジェクトでも追加できてしまうた め,追加される要素と取得する要素の型の整合性をプログラマが管理しなけれ ばならないという問題もある.

java.util.Collection系クラスの問題点

  1. 要素取り出す際にはキャストが必要.
  2. 意図しない型の要素も追加できてしまう.

実はもう一つ,「intなどの基本型を直接扱うことができず,Integerなどのラッパー オブジェクトに入れなおす必要がある」という問題もある.残念ながらこの問 題点に対する回答は示されていない.他の言語(C++, C#)では,この問題に対する 解決策が示されているが,ここではそれには触れない.C# の統一型システムについては,別記事, 「C#とJavaの言語仕様の比較」を参照してほしい.

Java Genericsは,この2つの問題点に対する解決である.これまで,Vector に入れた要素の型は「プログラマ」が管理していた.すなわち,プログラマの 意識の中に「このVectorにはStringを保持する」という約束があり,要素の追 加時には意図しない型を追加しないように,要素の取得時にはキャストを行っ て具体的なString型の参照に戻して代入していた.特にキャスト操作は,面倒 であるのと同時にバグを生みやすい.List. 1 のようにソースコード上の比較 的近くにadd()/get()の組があればまだよいが,コード上で遠く離れてしまう と,その約束事を人がチェックすることは難しくなる.Genericsを使えば, この型の管理は「プログラマ」でなく「コンパイラ」の仕事となるわけだ.

例えば,Vectorを使う時に要素の実際の型をコメントで書いた経験はないだろうか.


List.2 コメントとGenerics
J2SE1.3(Java2 SDK 1.3)のコード
class Account {
    private Vector transactions = new Vector(); // Transaction の列
    //  ...
    Transaction getTransaction(int index) {
        return (Transaction)transactions.get(index);
    }
}
Java Generics を用いたコード
class Account {
    private Vector<Transaction> transactions = new Vector<Transaction>();
    //  ...
    Transaction getTransaction(int index) {
        return transactions.get(index);
    }
}

List.2左では,「// Transactionの列」というコメント,およびtransactions という変数名でVectorの要素型に関する制約をかろうじて表現している. List.2右では,型パラメータ(<>内の型)としてコンパイラが分かる形で要素型 をコードに表現できる.例えば,List.2の左ではgetTransaction メソッドで キャストを行っている.しかし,Vectorであるtransactionsに本当に Transaction(あるいはそのサブクラス)のオブジェクトが入っていることは誰 が保証するのだろうか.ここに表れたコードだけでは不明だ.Vector内に違う 要素が入っていた場合は,List.2左の例では実行時にClassCastException例外 が発生する.しかし,Genericsを使った右の例では,transactionsに Transaction クラスのオブジェクトしか追加できないようにコンパイラが チェックしてくれる.また,getTransactionメソッドでもキャストは不要となる.

このように,Java Genericsを使えばコレクションの要素に関する制約を,コ ンパイラが分かる形で明示的にプログラマが表現できる.


   JavaへのGenerics導入の方針

Java言語仕様にGenericsを導入するにあたってのJSR14の方針は次のようなものだ.

  • 既存のJava言語仕様と上位互換である.すなわち,既存のコードは新しいコンパイラでコンパイルできる.
  • 既存のJava VM仕様と完全互換である.すなわち,JavaVMは変更の必要がなく, 新しいバイトコードは既存のバイトコードと共にそのまま利用できる.
  • 既存のクラスライブラリと互換である.すなわち,既存のクラスライブラリは そのまま利用できる.

このような比較的強い制約のため,JSR14で提案されている仕様はイレイジャ (erasure)というOberon言語で知られる手法である.基本的には「GJ」と呼ばれる Javaに型総称性を導入するプロジェクトがこの提案の元になっている.

イレイジャでは,コンパイラが前処理に近い形で型総称性を扱う.すなわち, コンパイラがプログラマに代わって,

  • 型パラメータを消す
      Vector<String> strings = new Vector<String>();
    Vector strings = new Vector();

  • キャストを挿入する
    String s = strings.get(0)
    String s = (String)strings.get(0);

  • 型チェックを行う.

    という操作を行ってくれると考えると分かりやすい.コンパイラが型情報を知っ ているので,より厳密な型チェックがコンパイル時に行える.しかし,一旦コ ンパイルされてしまえば,オブジェクトの総称的な型情報はなくなってしまう. Vector<String>もVector<Integer>も,実行時にはVector<Object> すなわち, Vectorクラスを利用することになる.型パラメータ(<>内の型)は省略されると 自動的にObjectとして認識されるため,実行時にはVector<Object> はすなわ ち,既存のコレクションライブラリのVectorクラスそのものなのだ.


   Java Genericsを使ってみよう

では,簡単にJava Genericsを使ってコーディングしてみよう. 要素のペアを表す Pair というクラスを自作する.

List.3 Pair.java
public class Pair<T1,T2> {
    public T1 first;
    public T2 second;
    public Pair(T1 first, T2 second) {
        this.first = first;
        this.second = second;
    }
    public static <U1,U2> Pair<U1,U2> create(U1 first, U2 second) {
        return new Pair<U1,U2>(first, second);
    }
    public static <T> Pair<T,T> duplicate(T x) {
        return new Pair<T,T>(x, x);
    }
    public String toString() {
        return "[" + first + "," + second + "]";
    }
    public boolean equals(Object o) {
        if (o instanceof Pair) {
            Pair other = (Pair)o;
            return first.equals(other.first)
                && second.equals(other.second);
        }
        return false;
    }
}

Pairは型パラメータとして,T1,T2の2つを取る.コンストラクタでは,引数 を2つ取ってそれをそれぞれ first, second というインスタンス変数に入れ ている.ただそれだけのクラスだが,いろんなもののペアを表現できる.

さらに,staticなGenericメソッドを2つ作ってみた.createというメソッド は,二つの引数からPairを生成する.メソッド宣言の戻り値型はPair<U1,U2> であり,その直前に置かれた<U1,U2>がこのメソッドの型パラメータである. このメソッドはコンストラクタの替わりに使える.コンストラクタではクラス の型引数を<>で囲って明示的に指定する必要があるが,このメソッドでは型引 数は与えた引数からコンパイラが推論する.

duplicateというメソッドは,同じ型<T>の同じ要素をもつPairを1つnewする. 戻り値型は,Pair<T,T>である.Tは,T1ともT2とも別物であることに注意. ついでに,標準のメソッドであるtoStringやequalsを提供してみた. そして,これを使う簡単なテストプログラム,PairTest1 はList.4のようになる.


List.4 PairTest.java
import java.util.Vector;
import java.util.Iterator;

public class PairTest1 {
    public static void main(String []args) {
        Pair<String,Integer> one = new Pair<String,Integer>("one", new Integer(1));
        System.out.println("one = " + one);

        Pair<String,Integer> another = Pair.create("one", new Integer(1));
        System.out.println("another = " + another);

        Pair<String,Integer> two = Pair.create("two", new Integer(2));
        System.out.println("two = " + two);

        if (one.equals(another))
            System.out.println("one == another");
        else
            System.out.println("one != another");

        if (one.equals(two))
            System.out.println("one == two");
        else
            System.out.println("one != two");

        Pair<Integer,Integer> twins = Pair.duplicate(new Integer(10));
        System.out.println("twins = " + twins);
    }
}

List.4のテストでは,one として,[one, 1], anotherとして同じく[one,1] を作っ ている.oneはコンストラクタを利用して,anotherはstaticメソッドを利用して生成 する例だ(このコードは正しいコードだが,Java2 SDK 1.3のランタイム環境ではコン パイルエラーとなってしまった.1.4 Beta2ではコンパイルできるようだ).create ソッドの呼び出しは,Pair.createとし,型パラメータを指定する必要がない.コン パイラが与えた実パラメータから推測するのだ.そして,twoとして[two,2]という Pair<String,Integer>のオブジェクトを作成し,それぞれの同値性を検査して いる.最後に,双子(twin)のPair<Integer,Integer> を作っている.このプロ グラムの出力は,List.5のようになる.


List.5 PairTest1.javaの出力
C:\HOME>java PairTest1
one = [one,1]
another = [one,1]
two = [two,2]
one == another
one != two
twins = [10,10]

ここで,実行にはJava2 SDK 1.3のJavaVM(javaコマンド)をそのまま利用して いることに注意して欲しい.コンパイルされたバイトコードは,そのまま既存 のJavaVMとクラスライブラリ(rt.jar)で動作する.この出力から,toStringと equalsメソッドがうまく機能しているのが分かる.もう少し複雑な例として, VectorやIteratorといった標準コンテナと組み合わせてみる.


List.6 PairTest2.java
import java.util.Vector;
import java.util.Iterator;

public class PairTest2 {
    public static void main(String []args) {
        Pair<String,Integer> zero = new Pair<String,Integer>("zero", new Integer(0));
        Pair<String,Integer> one = new Pair<String,Integer>("one", new Integer(1));
        Pair<String,Integer> two = new Pair<String,Integer>("two", new Integer(2));

        Vector<Pair<String,Integer>> numbers = new Vector<Pair<String,Integer>>();

        numbers.add(zero);
        numbers.add(one);
        numbers.add(two);

        Iterator<Pair<String,Integer>> i = numbers.iterator();
        while (i.hasNext())
            System.out.println(i.next());
    }
}

この例はちょっと混み入ったコードに見えるが,Pairを順にVectorに入れてIteratorで 取り出して表示しているだけだ.VectorがVector<Pair<String,Integer>>となっている のに対応して,IteratorもIterator<Pair<String,Integer>>となっている. ここにはキャストが全く含まれておらず,型安全な美しいコードだと言えるだろう. List.7がこのプログラムの出力だ.


List.7 PairTest2.javaの出力
C:\HOME>java PairTest2
[zero,0]
[one,1]
[two,2]

   型パラメータの制約

Java Genericsでは,C++のtemplateなどと違って型パラメータ<T>に対する制約を明示的に 表現することができる.すなわち,型パラメータが実装すべきインターフェイスや継承すべき クラスを型パラメータに指定できるのだ.これを型パラメータの制約(bound)といい, このような方式の型総称性を,閉じた多態(bounded polymorphism)という.言語では Eiffelがこの方式を採用している.

List.8 Comparer.java
public class Comparer {
    public static <T extends Comparable<T>> boolean greater(T t1, T t2) {
        return t1.compareTo(t2) > 0;
    }
    public static <T extends Comparable<T>> boolean less(T t1, T t2) {
        return t1.compareTo(t2) < 0;
    }
}

List.8 では,Genericメソッドの型パラメータに制約を加えている.greater メソッ ドは与えられた2つの引数t1,t2を取り,t1 > t2 ならばtrue,そうでなければ false を返す.lessメソッドはこの逆だ.2つの引数が同じ型であり,その型は CompareTo(T)メソッドを持っている必要がある.この制約を,extends Comparable<T> で表現している(JSR14仕様にはimplementsを使うとあるが,参 照コンパイラではextendsを用いないとコンパイルエラーとなった).ここで, Comparableはjava.langパッケージの標準インターフェイスであり,Integer, Charactorなどはこのインターフェイスが持つcompareToメソッドを実装している.こ の制約がないと,コンパイラはcompareToメソッドをTから見つけることができず,コ ンパイルエラーとなる.ちなみに,制約がない型パラメータはextends Objectとみな され,Objectが持つメソッドしか呼び出せない.コンテナなどの場合には入れるオブ ジェクトに制約がないため,これで十分なのである.


List.9 ComparerTest.java
public class ComparerTest {
    public static void main(String[] args) {
        Integer one = new Integer(1);
        Integer two = new Integer(2);

        if (Comparer.greater(one, two))
            System.out.println("1 > 2");
        if (Comparer.less(one, two))
            System.out.println("1 < 2");

        Character three = new Character('3');
        Character four = new Character('4');

        if (Comparer.greater(three, four))
            System.out.println("3 > 4");
        if (Comparer.less(three, four))
            System.out.println("3 < 4");

        // Comparer.greater(one, three);      // コンパイルエラー!
        
    }
}

List.9が,Comparerのテストプログラムだ.Integer型のオブジェクト作って 比較し,次にCharacter型のオブジェクトを作って比較している.型は違う が,共にComparer.greater, Comparer.lessが利用でき, 前述のように型は与える実引数からコンパイラが推測する.

最後に,Comparer.greaterを違う型(IntegerとCharactor)で呼び出してみる. これは,greator<T>() がふたつの引数が同じ型であることを要求するため, 対応するgreaterメソッドが見つからずにコンパイルエラーとなる. List.10が実行結果だ.

List.10 ComparerTest.javaの出力
C:\HOME>java ComparerTest
1 < 2
3 < 4

   Collectionライブラリ

Java Genericsがもっとも活躍するのはコレクションライブラリである.ここ では,java.util.Collection系に加わった変更を見ていこう.Collectionに関する仕様は,別記事である 「JDK1.2のコレクションを理解する」を参照してほしい.

前述したように,実行時には既存のクラスライブラリがそのまま利用される.ただし, コンパイル時には新しいライブラリ(のスタブ)が利用される.このスタブには, 型パラメータの型情報が含まれており,これを用いてコンパイラはイレイジャを実行 しながらコンパイルを行う.

この新しいクラスライブラリのクラス図を,既存のクラスライブラリと対比する形で図1に示す.

図1コレクションクラスライブラリ(java.util.Collection)
J2SE1.3(Java2 SDK 1.3)の Collection
Java Generics のCollection

すぐに分かるとおり,クラス名や継承構造は全く同じである.新しいクラスラ イブラリには,型パラメータがついている点だけが違う.Collectionを頂点と するクラス群には,<E>という要素(Element)の型を示す型パラメータが,Map を頂点とするマップ群には,<K,V>というキー(Key)の型と値(Value)の型を示 す型パラメータが新たに導入されている.

実際に利用するには,型パラメータを与える.例えばStringのVectorであれば, Vector<String>という具合だ.型パラメータを与えないと,Objectがデフォルト になり,Vector<Object>と認識される.これは,既存のVectorと全く同じである.


   Javaイベントモデルのサポート例

これまで述べた通り,Java Genericsはコレクションクラスに対するサポート という意味合いが強い.今までの例では,コレクションライブラリの新しい利 用や,Genericsのコレクション的な利用に関する例を示してきた.ここではコ レクションから離れて,少し実用的なコードにトライしてみたい.

Javaのイベント伝達のしくみとして,Java SDK 1.2からJavaイベントモデルが 提供されている.これは,Observerパターンを型安全に拡張した, MultiCastパターンの一種だと言える.MultiCastパターンについては,別記事, 「デザインパターンを使った進化的設計 - TemplateMethod/Observer/MultiCastパターン」 を参照してほしい.

このイベントモデルの1つの問題は,イベント毎にリスナーインターフェイスと イベントソースとなるクラスをハンドコードしなければならない点だ.例えば, キーボードイベントに関するイベントモデルをコーディングするには,

  • イベントクラス: class Key
  • リスナーインターフェイス: interface KeyListener
  • イベントソース: class Keyboard
  • 実際のリスナークラス: class EventReceiver

のそれぞれを準備する.それぞれには規約がある.

  • イベントクラスはjava.util.EventObjectを extends する.
  • リスナーインターフェイスにはjava.util.EventListenerをextendsし, イベントのコールバックメソッドのシグニチャ(メソッド宣言)を定義する.
  • イベントソースには,リスナーの追加,削除メソッドを用意する.
  • 実際のリスナークラスは,リスナーインターフェイスをimplementsし,コールバックメソッド 本体を実装する.

などである(List.11).

List.11 イベントモデルの規約
class Key extends java.util.EventObject {
    char code;
    // ...
}

interface KeyListener extends java.util.EventListener {
    void keyPressed(Key key);
}

class Keyboard {
    void addKeyListener(KeyListener listener) {
       // ...
    }
    void removeKeyListener(KeyListener listener) {
       // ...
    }
    // ...
}

class EventReceiver implements KeyListener {
    void keyPressed(Key key) {
        // コールバック本体
    }
}

List.11はKeyというイベントに関するクラス群であるが,ここでは一歩進んで, さまざまなイベントに対してこれらのクラスの作成を支援する,汎用的な Genericクラスを考えてみたい.イベントさえ定義すれば,イベントリスナー インターフェイスやイベントソースのクラスを自動的に作れるようにしたい.

List.12 EventListner.java
public interface EventListener<Event> extends java.util.EventListener {
    public void eventHappened(Event event);
}

List.12はイベント型を型パラメータとした,リスナーインターフェイスの Generic表現だ.コールバックメソッド名は,eventHappened で統一し, 引数のイベント型で識別できるようにしている.


List.13 EventSource.java
import java.util.Vector;

public class EventSource<Event> {
    protected Vector<EventListener<Event>> listeners = new Vector<EventListener<Event>>();

    public void addListener(EventListener<Event> l) {
        listeners.add(l);
    }
    public void removeListener(EventListener<Event> l) {
        listeners.remove(l);
    }
    protected void fireEvent(Event e) {
        for (int i = 0; i < listeners.size(); i++)
            listeners.get(i).eventHappened(e);       // イベント配送
    }
}

List.13はイベント型を型パラメータとした,イベントソースのGeneric表現だ. リスナーの追加(addListener)・削除(removeListener),およびイベント発火 (fireEvent)の各メソッドを汎用的に定義している.イベントソースのコード は,対応するイベントリスナの列をVectorに保持している.発火メソッドでは, 現在保持しているすべてのリスナーに,順にイベントを配送している(*). fireEventがprotected になっていることに注意してほしい.このGenericクラ スは,継承して使うことを前提としており,fireEventは継承先のクラスから 利用される. ここでは簡単のためにsynchronziedの問題を考えていない.詳しくは 別記事「Observerパターンとマルチスレッド」 を参照されたい.

さて,これらを使って,キーボードイベントとマウスイベントを扱うクラスを 作ってみよう.まずは,イベントとなるクラス,KeyとClickだ.この2つのク ラスは,通常のイベントクラスのお作法通りに作る(List.14,15).

List.14 Key.java
public class Key extends java.util.EventObject {
    private char code;
    public Key(Object source, char code) {
        super(source);
        this.code = code;
    }
    public char getCode() {
        return code;
    }
    public String toString() {
        return new String(new char[] {code} );
    }
}

List.15 Click.java
public class Click extends java.util.EventObject {
    private int button;
    public Click(Object source, int button) {
        super(source);
        this.button = button;
    }
    public int getButton() {
        return button;
    }
    public String toString() {
        return String.valueOf(button);
    }
}

では次に,イベント発生源であるKeyboardクラスとMouseクラスを定義する. ここで,先ほど用意したEventSource<Event>を継承して利用する(List.16,17).

List.16 Keyboard.java
public class Keyboard extends EventSource<Key> {
    public void pressKey(char key) {
        fireEvent(new Key(this, key));
    }
}
List.17 Mouse.java
public class Mouse extends EventSource<Click> {
    public void pressButton(int button) {
        fireEvent(new Click(this, button));
    }
}

この2つのクラスは,イベントを発生させるメソッドを用意しているだけだ. リスナーの管理を行うaddListener/removeListenerメソッドおよびイベントの 配送を行うfireEventメソッドは,EventSource<Event>クラスのものをそのま ま利用できる.通常ならば,イベントソースにはリスナーの管理コードがクラ ス毎に混入してしまう.Genericsを使うことによって,この管理コードが EventSource<Event> に分離できることが大きな利点だ.

では,この2つのイベントを受けるEventReceiverクラスを定義しよう. KeyboardとMouseから発生するイベントを一手に受信する(List.18).

List.18 EventReceiver.java
import java.util.Vector;
import java.io.PrintStream;

public class EventReceiver {
    Vector<Key> keyLog = new Vector<Key>();
    Vector<Click> clickLog = new Vector<Click>();

    private void log(Key key) {
        keyLog.add(key);
    }

    private void log(Click click) {
        clickLog.add(click);
    }

    public EventReceiver(EventSource<Key> keyboard, EventSource<Click> mouse) {
        keyboard.addListener(
            new EventListener<Key>() {
                public void eventHappened(Key key) {
                    System.out.println("key = " + key.getCode());
                    log(key);
                }
            }
        );
        mouse.addListener(
            new EventListener<Click>() {
                public void eventHappened(Click button) {
                    System.out.println("button = " + button.getButton());
                    log(button);
                }
            }
        );
    }

    public void reportLog(PrintStream out) {
        out.println("---- Event Report ----");
        out.print("KEY: ");
        for (int i = 0; i < keyLog.size(); i++) {
            out.print(keyLog.get(i));
        }
        out.println("");

        out.print("CLICK: ");
        for (int i = 0; i < clickLog.size(); i++) {
            out.print(clickLog.get(i));
        }
        out.println("");
        out.println("----------------------");
    }
}

受信のリスナークラスであるEventReceiverは,Javaらしく匿名クラスを利用 して作成している.コンストラクタの中でリスナーオブジェクトを2つ作成し, その場でイベントソースであるkeyboardとmouseに登録している. イベント受信のコールバックの中では,受信したキーの文字やマウスボタンの 番号をプリントする.さらに,イベントの受信履歴を保持するようにする. そして,現在までに受信したすべてのイベントをプリントするサービスとして reportLogメソッドを作成しよう. これで準備は整った.後はMainクラスを作って全体を組み立て,動作させてみる(List.19).

List.19 Main.java
public class Main {
    public static void main(String[] args) {

        // キーボードとマウスの生成
        Keyboard keyboard = new Keyboard();
        Mouse mouse = new Mouse();

        // 受信オブジェクトの生成
        EventReceiver receiver = new EventReceiver(keyboard, mouse);

        // イベント発生
        keyboard.pressKey('H');
        keyboard.pressKey('E');
        keyboard.pressKey('L');
        keyboard.pressKey('L');
        keyboard.pressKey('O');

        mouse.pressButton(1);
        mouse.pressButton(2);

        // イベントログのレポート
        receiver.reportLog(System.out);
    }
}

Mainクラスでは,プログラムのエントリーポイントであるmainメソッドのみを定義する.

  • キーボード(Keyboard)とマウス(Mouse)オブジェクトの生成
  • 受信オブジェクト(EventReceier)の生成
  • イベント(Key, Click)の発生
  • イベントログのレポート(reportLog)

を順に行う.イベントソースとイベントリスナの結合は,EventReceiver のコンストラクタの中で行われている.出力は List.20のようになる.

List.20 Main.java の出力
C:\HOME>java Main
key = H
key = E
key = L
key = L
key = O
button = 1
button = 2
---- Event Report ----
KEY: HELLO
CLICK: 12
----------------------

この例で注目したいのは,以下の点である.

  • キャストが全く使われていない(型安全である).
  • イベントリスナのインターフェイスは,EventListener<Event>から自動的に生成される.
  • イベントソースのリスナー管理コードは,EventSource<Event>に括り出される.

この例は,java.util.Observer/Observableを使っても作れただろう. しかし,その場合はキャストを多用することになり型安全性を損なう. 図2に,イベントクラス,図3に全体の構造をクラス図で示す. また,図4にはMainの動作をシーケンス図で示した.

図2 イベントクラス

この図では,匿名クラスを$1,$2という名前で示している.また,UML1.4で 導入された入れ子クラスの表記を用いて,これらのクラスと 外側のクラスの関係を表示した.


図3 全体の構造


図4 イベントの流れ

   Java Genericsの欠点

最後に,Java Genericsを使ってみて気付いたいくつかの問題点を指摘してみたい. 前述のように,JSR14ではイレイジャ(erasure)という方式でGenericsをJavaに導入し, 既存のコードやJVMなどと最大限の互換性を持たせることに成功している.しかし 次のような問題点もある.

  • Genericクラスの中で,型パラメータ(<T>)をnewできない. -- new T はだめ
  • Genericクラスの中で,型パラメータ(<T>)にキャストができない. -- (T)object はだめ
  • Genericクラスの中で,型パラメータに対する instanceof ができない.-- object instanceof T はだめ
  • Genericクラスを型パラメータ(<T>)から継承できない.
  • intなどの基本型を統一的に扱えない.
  • 例外クラス(Throwableのサブクラス)としてGenericクラスを定義できない.

これらは,実行時にはTに関する型情報が得られないこと,T型が実際にはすべ てObject型(もしくはTを制約する型)に置き換わっていること, 既存のJavaVMに変更が加えられないことに起因する.


まとめ

第1部では,JSR14で提案されたJava Genericsを概観し,それを使ったプログ ラミング例を見た.この仕様は「コレクションライブラリを型安全に利用する」 という課題を主目的としているようだ.しかし,イベントモデルの例でみたよ うに,ほかにもおもしろい利用方法があるし,これからも発見される可能性が高い.

イレイジャ方式を使っているために,C++のtemplateのような柔軟なプログラ ミングはできないのが欠点だが,既存のJavaVMやクラスライブラリとの互換性 が最大限考慮された仕様となっている点は大きく評価できる.

以降,おまけとして,付録を付けた.


   付録1:Java Generics拡張コンパイラのインストール

  • 現在SunのJDC(Java Developer Connection)から,Genericsを扱うことのでき るコンパイラ(javac)がプロトタイプ実装として提供されている(ソースコー ド付).このコンパイラ自身もGenericsを使って記述されている.ブートスト ラップのためにクラスファイルも提供されている. ここでは,ダウンロードからインストールまでの簡単な流れを紹介しておこう.

    まず,ダウンロードするには,JDC(Java Developer Connection)に登録(無料) する必要がある.登録が済んだら, http://developer.java.sun.com/developer/earlyAccess/adding_generics/の,

  • JSR 014: Adding Generics to the JavaTM Programming Language

    がダウンロードリンクだ.ここから jsr14_adding_generics-1_0-ea.zip(608Kbyte)というファイルが取得できる.

    • ここではWindowsを例に,ローカルでの設定方法を紹介する. 今回はこのファイルを解凍し,フォルダーをC:\直下に置いた.

      C:\jsr14_adding_generics-1_0-ea
         examples/   ... サンプルのソースコード
         javac/      ... 拡張 javac のソースコード
         scripts/    ... 拡張 javac を起動するスクリプト
         collect.jar ... Generic Collection のスタブ
         javac.jar   ... 拡張 javac のクラスファイル
      

      今回は,拡張javacの仕組みを見るのが目的ではなく,とにかく使ってみるこ とが主眼であるので,コンパイラのソースコードが格納されているjavac以下 のフォルダは無視しよう.

      javac.jarはGenericsを理解できるコンパイラであり,collect.jarはGenerics に拡張されたjava.util.Collectionライブラリである.ただし,collect.jar はスタブ(宣言だけで中身は空)であり,コンパイル時にのみ利用する.Java Generics 拡張は,既存のJava VMには影響を与えないし,既存のクラスファイ ルとも互換性を保っていることに注意しよう.よって,実行時は, J2SE1.3(Java2 SDK 1.3, Standard Edition) のJVM(javaコマンド)および java.utilライブラリがそのまま使える.

      拡張 javac の起動にも,J2SE1.3のjavac コンパイラを利用する.1.3のjavac コンパイラに,javac.jarとcollect.jarをブートのクラスパスとして与えて起 動する必要がある.

      scriptsフォルダには,UNIX用の起動スクリプトがある.ここでは,これを参 考にして,Window用の起動バッチファイル(gjavac.bat)を記述してみた(List A).


List A gjavac.bat
set J2SE13=C:\jdk1.3
set JSR14DISTR=C:\jsr14_adding_generics-1_0-ea

%J2SE13%\bin\javac -J-Xbootclasspath/p:%JSR14DISTR%/javac.jar \
   -bootclasspath %JSR14DISTR%/collect.jar;%J2SE13%/jre/lib/rt.jar \
   %1 %2 %3 %4 %5 %6 %7 %8 %9

このバッチファイルをscriptsディレクトリに置き,PATH環境変数に, C:\jsr14_adding_generics-1_0-ea\scriptsを追加すれば準備完了だ (Windows 98では,再起動の必要がある). 任意のフォルダに簡単なソースコードList B(Test.java)を置き,コンパイル してみよう.ここでは,C:\HOME というフォルダにTest.javaを置いた.

List.B Test.java
 1: import java.util.*;
 2: 
 3: public class Test {
 4:   public static void main(String[] args) {
 5: 
 6:     Vector<Integer> iv = new Vector<Integer>();
 7:     iv.add(new Integer(0));
 8:     iv.add(new Integer(1));
 9:
10:     Iterator<Integer> i = iv.iterator();
11:     Integer zero = i.next();
12:     Integer one = i.next();
13:
14:     System.out.println("zero = " + zero);
15:     System.out.println("one  = " + one);
16:   }
17: }

コンパイルには,List.Aのgjava.batというバッチファイルを利用する.

gjavac.bat を使ったコンパイル
C:\HOME>gjavac Test.java

コンパイルは正常に終了する.ちなみに,通常のjavacを使ってこれをコンパ イルしようとすると次のようなエラーが出るはずだ.

List.C 通常のjavac.bat を使ったコンパイル
C:\HOME>javac Test.java
Test.java:6: '(' または '[' がありません。
        Vector<Integer> iv = new Vector<Integer>();

これは通常のjavacがVector<型>という構文を理解できないから当然である. さて,では実際にコンパイルしたクラスを動かしてみよう.動かすには,通常 のJava VM (javaコマンド)およびクラスライブラリを利用する.

List.D Test.java の実行
C:\HOME>java Test
zero = 0
one = 1

これで正常に動作することが確かめられた.まとめてみよう.

  • 拡張javacコンパイラは,java.util.Collectionのスタブライブラリを用いてコンパイルする.
  • 実行時には,Java2 SDK 1.3のJVM(javaコマンド)およびライブラリ(rt.jar)をそのまま利用する.

   付録2: 既存のCollectionコードから
             新しいCollectionのスタブを生成する

Java Genericsでは,実行時には既存のコレクションライブラリを,コンパイ ル時には新しいコレクションライブラリ(のスタブ)を利用する.では,この スタブはどうやって作成するのだろうか.このスタブは,コンパイル時にしか 利用されないことに注意して欲しい.コンパイラにイレイジャのための型情報 を与える必要があるのだ.

幸運なことに,JavaVMのクラスファイルには付加情報を保存する"Signature"という フィールドがある.Genericコンパイラでは,このフィールドを利用して必要な 情報を保存する.ただし,実行時には,JavaVMはこの情報を無視してしまう.

Java Genericsの前身であるGJコンパイラには,レトロフィッティング (Retrofitting)というオプションがあり,このオプションを使ってクラスファ イルに型情報を格納することが可能である.-retrofit がこのオプションである. このオプションを使って,例えば,

List.F Vectorのレトロフィット
class Vector<E> implements Collection<E> {
    public Vector();
    public void add(E element);
    public E get(int index);
    public Iterator<E> iterator();
}

というシグニチャのみを含むソースコードをコンパイルすると,コンパイラは このコードとCLASSPATHにあるVectorクラスから,型情報を含むクラスファイル をVector.classとして作成する. 付録1で書いたように,jsr14の参照実装には,コレクションのスタブが付属している. このスタブは,レトロフィッティングにより生成されたものだろう.


Kenji Hiranabe <hiranabe@esm.co.jp>
Last modified: Wed Feb 20 18:21:33 2002



この記事への評価にご協力をお願いします。

良かった 普通 イマイチ