| |||
はじめに | |||
最初にJava Genericsの導入動機,必要性などを解説した後, その仕様について見ていこう.また,Genericsを使ったサンプルも解説する. なお,JSR14について詳細は,以下から参照できる. | |||
Hello Generics | |||
まずは,簡単なコードを見てみよう.List 1 にJava Generics使用前と使用後 のコードを比較した.プログラムは,Vectorに文字列を詰めそれを取りだす,という簡単なものだ. |
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をはじめとするjava.utilパッケージ内のCollection系クラス/インター フェイス群は,Object型を要素として扱うようになっている.これによって, どんなクラスのオブジェクトでもコレクションに追加することができる.しか し,Javaプログラミングに慣れている読者の方は「Vectorに詰め込んだ 要素はキャストして取り出さなくてはならない」という制約に苛立ちを感じ続 けていたはずだ.また,どんなクラスのオブジェクトでも追加できてしまうた め,追加される要素と取得する要素の型の整合性をプログラマが管理しなけれ ばならないという問題もある. java.util.Collection系クラスの問題点
実はもう一つ,「intなどの基本型を直接扱うことができず,Integerなどのラッパー オブジェクトに入れなおす必要がある」という問題もある.残念ながらこの問 題点に対する回答は示されていない.他の言語(C++, C#)では,この問題に対する 解決策が示されているが,ここではそれには触れない.C# の統一型システムについては,別記事, 「C#とJavaの言語仕様の比較」を参照してほしい. Java Genericsは,この2つの問題点に対する解決である.これまで,Vector に入れた要素の型は「プログラマ」が管理していた.すなわち,プログラマの 意識の中に「このVectorにはStringを保持する」という約束があり,要素の追 加時には意図しない型を追加しないように,要素の取得時にはキャストを行っ て具体的なString型の参照に戻して代入していた.特にキャスト操作は,面倒 であるのと同時にバグを生みやすい.List. 1 のようにソースコード上の比較 的近くにadd()/get()の組があればまだよいが,コード上で遠く離れてしまう と,その約束事を人がチェックすることは難しくなる.Genericsを使えば, この型の管理は「プログラマ」でなく「コンパイラ」の仕事となるわけだ. 例えば,Vectorを使う時に要素の実際の型をコメントで書いた経験はないだろうか. |
|
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の方針は次のようなものだ.
このような比較的強い制約のため,JSR14で提案されている仕様はイレイジャ (erasure)というOberon言語で知られる手法である.基本的には「GJ」と呼ばれる Javaに型総称性を導入するプロジェクトがこの提案の元になっている. イレイジャでは,コンパイラが前処理に近い形で型総称性を扱う.すなわち, コンパイラがプログラマに代わって,
| ||||||||
Java Genericsを使ってみよう | ||||||||
では,簡単にJava Genericsを使ってコーディングしてみよう. 要素のペアを表す Pair というクラスを自作する. |
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のようになる. |
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のようになる. |
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といった標準コンテナと組み合わせてみる. |
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がこのプログラムの出力だ. |
C:\HOME>java PairTest2 [zero,0] [one,1] [two,2] |
型パラメータの制約 | |||
Java Genericsでは,C++のtemplateなどと違って型パラメータ<T>に対する制約を明示的に 表現することができる.すなわち,型パラメータが実装すべきインターフェイスや継承すべき クラスを型パラメータに指定できるのだ.これを型パラメータの制約(bound)といい, このような方式の型総称性を,閉じた多態(bounded polymorphism)という.言語では Eiffelがこの方式を採用している.
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が,Comparerのテストプログラムだ.Integer型のオブジェクト作って 比較し,次にCharacter型のオブジェクトを作って比較している.型は違う が,共にComparer.greater, Comparer.lessが利用でき, 前述のように型は与える実引数からコンパイラが推測する. 最後に,Comparer.greaterを違う型(IntegerとCharactor)で呼び出してみる. これは,greator<T>() がふたつの引数が同じ型であることを要求するため, 対応するgreaterメソッドが見つからずにコンパイルエラーとなる. List.10が実行結果だ.
|
Collectionライブラリ |
Java Genericsがもっとも活躍するのはコレクションライブラリである.ここ では,java.util.Collection系に加わった変更を見ていこう.Collectionに関する仕様は,別記事である 「JDK1.2のコレクションを理解する」を参照してほしい. 前述したように,実行時には既存のクラスライブラリがそのまま利用される.ただし, コンパイル時には新しいライブラリ(のスタブ)が利用される.このスタブには, 型パラメータの型情報が含まれており,これを用いてコンパイラはイレイジャを実行 しながらコンパイルを行う. この新しいクラスライブラリのクラス図を,既存のクラスライブラリと対比する形で図1に示す. |
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つの問題は,イベント毎にリスナーインターフェイスと イベントソースとなるクラスをハンドコードしなければならない点だ.例えば, キーボードイベントに関するイベントモデルをコーディングするには,
のそれぞれを準備する.それぞれには規約がある.
などである(List.11).
List.11はKeyというイベントに関するクラス群であるが,ここでは一歩進んで, さまざまなイベントに対してこれらのクラスの作成を支援する,汎用的な Genericクラスを考えてみたい.イベントさえ定義すれば,イベントリスナー インターフェイスやイベントソースのクラスを自動的に作れるようにしたい.
List.12はイベント型を型パラメータとした,リスナーインターフェイスの Generic表現だ.コールバックメソッド名は,eventHappened で統一し, 引数のイベント型で識別できるようにしている. |
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). |
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} ); } } |
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).
この2つのクラスは,イベントを発生させるメソッドを用意しているだけだ. リスナーの管理を行うaddListener/removeListenerメソッドおよびイベントの 配送を行うfireEventメソッドは,EventSource<Event>クラスのものをそのま ま利用できる.通常ならば,イベントソースにはリスナーの管理コードがクラ ス毎に混入してしまう.Genericsを使うことによって,この管理コードが EventSource<Event> に分離できることが大きな利点だ. では,この2つのイベントを受けるEventReceiverクラスを定義しよう. KeyboardとMouseから発生するイベントを一手に受信する(List.18).
受信のリスナークラスであるEventReceiverは,Javaらしく匿名クラスを利用 して作成している.コンストラクタの中でリスナーオブジェクトを2つ作成し, その場でイベントソースであるkeyboardとmouseに登録している. イベント受信のコールバックの中では,受信したキーの文字やマウスボタンの 番号をプリントする.さらに,イベントの受信履歴を保持するようにする. そして,現在までに受信したすべてのイベントをプリントするサービスとして reportLogメソッドを作成しよう. これで準備は整った.後はMainクラスを作って全体を組み立て,動作させてみる(List.19).
Mainクラスでは,プログラムのエントリーポイントであるmainメソッドのみを定義する.
を順に行う.イベントソースとイベントリスナの結合は,EventReceiver のコンストラクタの中で行われている.出力は List.20のようになる.
この例で注目したいのは,以下の点である.
この例は,java.util.Observer/Observableを使っても作れただろう. しかし,その場合はキャストを多用することになり型安全性を損なう. 図2に,イベントクラス,図3に全体の構造をクラス図で示す. また,図4にはMainの動作をシーケンス図で示した. この図では,匿名クラスを$1,$2という名前で示している.また,UML1.4で 導入された入れ子クラスの表記を用いて,これらのクラスと 外側のクラスの関係を表示した. |
Java Genericsの欠点 |
最後に,Java Genericsを使ってみて気付いたいくつかの問題点を指摘してみたい. 前述のように,JSR14ではイレイジャ(erasure)という方式でGenericsをJavaに導入し, 既存のコードやJVMなどと最大限の互換性を持たせることに成功している.しかし 次のような問題点もある.
これらは,実行時にはTに関する型情報が得られないこと,T型が実際にはすべ てObject型(もしくはTを制約する型)に置き換わっていること, 既存のJavaVMに変更が加えられないことに起因する. |
第1部では,JSR14で提案されたJava Genericsを概観し,それを使ったプログ ラミング例を見た.この仕様は「コレクションライブラリを型安全に利用する」 という課題を主目的としているようだ.しかし,イベントモデルの例でみたよ うに,ほかにもおもしろい利用方法があるし,これからも発見される可能性が高い. イレイジャ方式を使っているために,C++のtemplateのような柔軟なプログラ ミングはできないのが欠点だが,既存のJavaVMやクラスライブラリとの互換性 が最大限考慮された仕様となっている点は大きく評価できる. 以降,おまけとして,付録を付けた. |
付録1:Java Generics拡張コンパイラのインストール |
ここでは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). |
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.Aのgjava.batというバッチファイルを利用する.
コンパイルは正常に終了する.ちなみに,通常のjavacを使ってこれをコンパ イルしようとすると次のようなエラーが出るはずだ.
これは通常のjavacがVector<型>という構文を理解できないから当然である. さて,では実際にコンパイルしたクラスを動かしてみよう.動かすには,通常 のJava VM (javaコマンド)およびクラスライブラリを利用する.
これで正常に動作することが確かめられた.まとめてみよう.
|
付録2: 既存のCollectionコードから 新しいCollectionのスタブを生成する | |
Java Genericsでは,実行時には既存のコレクションライブラリを,コンパイ ル時には新しいコレクションライブラリ(のスタブ)を利用する.では,この スタブはどうやって作成するのだろうか.このスタブは,コンパイル時にしか 利用されないことに注意して欲しい.コンパイラにイレイジャのための型情報 を与える必要があるのだ. 幸運なことに,JavaVMのクラスファイルには付加情報を保存する"Signature"という フィールドがある.Genericコンパイラでは,このフィールドを利用して必要な 情報を保存する.ただし,実行時には,JavaVMはこの情報を無視してしまう. Java Genericsの前身であるGJコンパイラには,レトロフィッティング (Retrofitting)というオプションがあり,このオプションを使ってクラスファ イルに型情報を格納することが可能である.-retrofit がこのオプションである. このオプションを使って,例えば,
というシグニチャのみを含むソースコードをコンパイルすると,コンパイラは このコードとCLASSPATHにあるVectorクラスから,型情報を含むクラスファイル をVector.classとして作成する. 付録1で書いたように,jsr14の参照実装には,コレクションのスタブが付属している. このスタブは,レトロフィッティングにより生成されたものだろう. Kenji Hiranabe <hiranabe@esm.co.jp> Last modified: Wed Feb 20 18:21:33 2002 |