Javaコードの診断: 痛みを伴わないJava総称クラス 第2回

JSR-14プロトタイプ・コンパイラーにおける総称クラスといくつかの制限

Comments

2003年の終わりにリリースされるJ2SE 1.5 (コードネームTiger) には、総称型 (JSR-14プロトタイプ・コンパイラーでも紹介されており、今すぐダウンロードできます) が組み込まれる予定です。このシリーズの第1回では、総称型の基礎と、それらをJava言語に追加する重要性および必要性について説明しました。またTigerに組み込まれる予定の総称型を実装する際、総称型を使用できるコンテキストが制限されるいくつかの問題点についても触れました。

プログラミング初心者が総称クラスを有効に活用できるよう、TigerおよびJSR-14で禁止されている総称型の使用法について詳述していきます。また、JVMでの互換性のある総称型の実装を実現するため、JSR-14 (つまりTiger) で使用される実装ストラテジーによって必然的に制限が発生する理由についても説明します。

総称型における制限

TigerおよびJSR-14における総称型の使用には以下の制限があります。

  • 静的メンバー内では、囲んでいる型パラメーターを参照してはならない
  • プリミティブ型を使って総称型のパラメーターをインスタンス化できない
  • 「ネイキッドな」型パラメーターをキャストまたはinstanceof 演算子で使用できない
  • 「ネイキッドな」型パラメーターをnew 演算子で使用できない
  • 「ネイキッドな」型パラメーターをクラス定義のimplements 節またはextends 節で使用できない

なぜこのような制限が存在するのでしょうか。それはJVMに総称型を実装するためにTigerおよびJSR-14で使用するメカニズムに理由があります。JVMでは総称型をサポートしないため、これらのコンパイラーはある「仕掛け」を使ってJVMが総称型をサポートするかのように見せています。つまり、コンパイラーが総称型情報を持つすべてのコードについて型チェックを実行し、その後、すべての総称型を「消去」して標準的な型のみを含むクラス・ファイルを作成します。

たとえばList<T> のような総称型は消去され、単なるList になります。「ネイキッドな」型パラメーター (List<T> クラスの型パラメーターT のように、括弧で囲んで指定する型パラメーターではなく、単独で使用される型パラメーター) は単に消去され、上位の型パラメーターに関連付けられます (T の場合はObject)。

これはとても有力な手法です。これにより、総称型によって実現される型付けの明確さを確保しながら、JVMでの互換性も保持できます。実際、List などの非総称型の従来のクラスを対応する総称クラス (List<T>) と同等に使用できます。実行時にはどちらのクラスも同様に動作します。

残念ながら、この機能には上記のような制限が伴います。総称型を消去することにより、型システムに欠陥ができ、総称型の安全な使用が制限されてしまいます。

それぞれの制限を明確にするため、これらの事例を見ていきましょう。今回の記事では、最初の3つの制限について説明します。最後の2つの問題は複雑であり、詳細に説明する必要があるため、次回の記事で取り上げることにします。

静的メンバー内の囲んでいる型パラメーター

静的メソッドや静的内部クラス内の囲んでいる型パラメーターへの参照は、コンパイラーによって完全に禁止されています。そのため、たとえば以下のコードはTigerではエラーとなります。

リスト1. 静的コンテキスト内の囲んでいる型パラメーターへの不適切な参照
class C<T> {
  static void m() {
    T t;
  }
  static class D {
    C<T> t;
  }
}

このコードをコンパイルすると、次の2つのエラーが生成されます。

  • 静的メソッドm 内のT への不適切な参照エラー
  • 静的クラスD 内のT への不適切な参照エラー

静的フィールドを定義する場合は、さらに複雑になります。JSR-14およびTigerでは、総称クラスの静的フィールドは、そのクラスのすべてのインスタンス化で共有されます。現在JSR-14コンパイラー1.0および1.2では、静的フィールド宣言で型パラメーターを参照すると、コンパイラーはエラーを生成しませんが、実際はエラーを生成する必要があります。フィールドが共有されるという事実により、キャストを行わないコードでClassCastException が発生するなど、実行時に奇妙なエラーが起きやすくなります。

たとえば、次のプログラムはJSR-14のこれらのバージョンでは警告が生成されずにコンパイルされます。

リスト2. 静的フィールド内の囲んでいる型パラメーターへの問題のある参照
class C<T> {
  static T member;
  C(T t) { member = t; }
  T getMember() { return member; }
  public static void main(String[] args) {
    C<String> c = new C<String>("test");
    System.out.println(c.getMember().toString());
    new C<Integer>(new Integer(1));
    System.out.println(c.getMember().toString());
  }
}

クラスC のインスタンスが割り振られるたびに、静的フィールドmember がリセットされることに注意してください。さらに、設定されるオブジェクトの型は、C のインスタンス化の型に依存します。main メソッドの最初のインスタンスcC<String> 型です。しかし、2番目のインスタンスはC<Integer> 型になります。共有される静的フィールドmemberc からアクセスされる場合、member の型は常にString であると想定されます。ただしC<Integer> 型の2番目のインスタンスが割り振られた後、memberInteger 型になります。

Cmain メソッドの実行結果には驚かされるもしれません。ここでClassCastException が発生します。ソース・コードにはキャストが含まれていないのに、なぜでしょうか。総称型を消去することで特定の式の型付けが弱くなることを考慮して、コンパイル中にコンパイラーによってキャストがコードに挿入されたのです。これらのキャストは正常に実行されると想定されていますが、この場合はそうではありません。

JSR-14 1.0および1.2のこの特別な「機能」はバグとみなす必要があります。これは、型システムの安定性、つまり型システムとプログラマーとの基本的な約束事を損ねます。静的メソッドや静的クラスの場合と同様に、静的フィールドの総称型への参照を禁止してしまうほうがずっと簡単です。

そのように危険なコードの使用を許可することで問題となるのは、プログラマーが意図的に自身のコードで型システムをオーバーライドできることではありません。むしろ、プログラマーが意図せずにそのようなコードを作成してしまう可能性があることに問題があります。たとえば、コピー・アンド・ペーストにより、誤ってフィールド宣言に静的修飾子を含めてしまう場合が考えられます。

型チェッカーを使えばその種の誤りを修正することはできますが、静的フィールドの場合、型システムによってプログラマーが混乱する可能性があります。キャストが使用されていないコードでClassCastException エラーだけが通知されたとしたら、このようなバグをどのように診断すればよいのでしょうか。プログラマーがTigerの総称型に使用されている実装スキームを認識しておらず、型システムが当然適切に機能していると思い込んでいる場合、事態は悪化します。この例の場合、型システムは適切に機能していません。

幸運なことに、JSR-14の最新バージョン (1.3) では、静的フィールドでの型パラメーターの使用が禁止されています。したがって、Tigerの静的フィールドでもそれらが禁止されると考えることができます。

総称型パラメーターとプリミティブ型

この制限には上記のような落とし穴はありませんが、コードが冗長になる可能性があります。たとえば、java.util.Hashtable の総称型バージョンには、Key およびValue それぞれの2種類の型パラメーターがあります。そのためHashtableStringString にマッピングする場合、new Hashtable<String, String>() によって新しいインスタンスを指定できます。ただし、Stringint にマッピングするHashtable が必要な場合は、Hashtable<String, Integer> のインスタンスを作成し、すべてのint 値をInteger でラップするしかありません。

繰り返しますが、Tigerのこの性質は、使用する実装スキームに起因するものです。型パラメーターが消去され、プリミティブ型でないその上位の型にバインドされるため、型が消去されてしまえばプリミティブ型を使ったインスタンス化は意味がありません。

キャストまたはinstanceof演算子における「ネイキッド」パラメーター

「ネイキッドな」型パラメーターとは、より大きな型の構文サブコンポーネントではなく、文字通り単独で使用される型パラメーターのことです。たとえばC<T> はネイキッドな型パラメーターではありませんが、C の本文内のT はネイキッドな型パラメーターです。

コード内でネイキッドな型パラメーターに対してキャストまたはinstanceof 演算を実行すると、コンパイラーはいわゆる「未チェック (unchecked)」警告を発します。たとえば、次のコードではWarning: unchecked cast to type T という警告が生成されます。

リスト3. 「未チェック」警告が生成される総称コード
import java.util.Hashtable;
interface Registry {
  public void register(Object o);
}
class C<T> implements Registry {
  int counter = 0;
  Hashtable<Integer, T> values;
  public C() {
    values = new Hashtable<Integer, T>();
  }
  public void register(Object o) {
    values.put(new Integer(counter), (T)o);
    counter++;
  }
}

このような警告は、実行時にコードが異常な動作をする可能性を示しているので、慎重に対処する必要があります。実際、それによってバグの診断が非常に困難になることがあります。前のコード例でregister("test")C<JFrame> のインスタンスで呼び出された場合は、ClassCastException が通知されるであろうと推測されます。しかし実際にはそうなりません。キャストが正常に実行されたかのように計算は続行され、計算がさらに進んだ後にエラーが通知されるか、最悪の場合、壊れたデータで計算されて問題が通知されないこともあります。同様に、ネイキッドな型パラメーターに対するinstanceof チェックではコンパイル時に「未チェック」警告が通知され、実行時に予期されたチェックは行われません。

両刃の剣

それではここで何が実行されるのでしょうか。Tigerは型消去に依存しているため、キャストおよびinstanceof テストでネイキッド型パラメーターは「消去」され、その上位の型へバインドされます (前の事例でのObject 型)。そのため、型パラメーターへのキャストは、その上位の型パラメーターへのキャストに変わります。

同様に、instanceof によって、そのオペランドが、上位の型パラメーターへのバインドのinstanceof であることが確認されます。これは、意図したことと全く異なります。仮にそうするのであれば、単に明示的に上位の型パラメーターにキャストするでしょう。そのため、通常は、型パラメーターに対するキャストおよびinstanceof チェックは避けます。

それでも、コードをコンパイルするために型パラメーターへのキャストに頼らなければならないことがあります。その場合は、コードのその部分は型チェックが安全に行われないので、自分で型チェックを実行しなければならないことを覚えておいてください。

総称型が堅固なコードの作成に強い威力を持っていても、それを誤って使用すると、堅固なコードにならないだけでなく、診断と修正が非常に困難なコードになる可能性があるということを説明してきました。次回は、Tigerにおける総称型の最後の2つの制限を取り上げ、それを総称Java型システムに組み込もうとしたときに必然的に発生する問題について説明します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219317
ArticleTitle=Javaコードの診断: 痛みを伴わないJava総称クラス 第2回
publish-date=03112003