Effective Javaを読むチャレンジ-項目23-

項目23 新たなコードで原型を使用しない

第5章ジェネリックスの最初ということと、初版のときにはなかったJ2SE 5.0からの言語仕様ということで、まずはジェネリックス関連の用語を定義しています。

ジェネリックスは、以前のバージョンではコレクションから要素を取り出すたびにオブジェクトをキャストする必要があり、コレクションに何のクラスが入っているか実行時まで分からないことによる失敗が多く、その失敗を減らすために、コレクションに許されるオブジェクトの型が何であるかというのをコンパイラに伝えられるようにしたものです。

文法的にジェネリックスというのはクラスやインタフェースが<>で囲まれた型パラメータを持つような記述をします。たとえばjava.util.Listのインタフェース宣言を見てみると、

となっています。こういったクラスやインタフェースをジェネリック型(総称型)といいます。上記のListインタフェースは単一の型パラメータEを持っています。このEは、実際にあるクラスではなく、型パラメータに指定するクラスを必要になった段階で指定しますよ、というための仮の名前です。こういった型パラメータを仮型パラメータといいます。

慣例的に仮型パラメータの名前はElementのEやTypeのTなど、そのジェネリック型の使用目的などを表す単語の頭文字1文字を取っていますが、仮型パラメータはクラス名として書ける文字や記号を使っていれば別にどんな名前でもコンパイルは通ります。ただ、通常のクラス名と見分けがつかなくなるので止めた方がいいと思います。

ジェネリック型の型パラメータが決まる段階は代入の左辺やインスタンス生成の式、メソッド宣言の戻り値や引数などにジェネリック型を使用するときなどです。その場合は、以下のように実際のクラスを記述します。

このStringのことを仮型パラメータEに対する実型パラメータといいます。ここまでの例では型パラメータは1つしかありませんが、実際は型パラメータはそれらのリストで定義されます。例えばjava.util.Mapインタフェースは「Map<K,V>」といった複数の型パラメータを持っています。KはKey、VはValueですね。

まとめると、ジェネリック型は実型パラメータのリストからなるパラメータ化された型の集合を定義する、ということになります。言い方が難しいですが、パラメータ化された型というのは、実型パラメータを持ったクラスやインタフェースです。

また、ジェネリック型には実型パラメータを伴わない、単なるクラスやインタフェース名のみの形で使用することができます。これを原型といいます。上記のListインタフェースの原型は以下のようになります。

やっと原型という言葉が出てきました。

これをやってはいけない

ここで本項目の主題、今のバージョンのJavaでやってはいけない原型を使ったコードの例です。

以前のバージョンからあったキャスト例外の問題です。これが発生すると、厖大なコードの中から間違った型をコレクションに入れている箇所を見つけなくてはいけなくなります。

これをパラメータ化された型を使うとどうなるか、というとこうなります。

パラメータ化された型を使うと、誤った型の挿入をコンパイラが見つけ、「型 Collection<Stamp> のメソッド add(Stamp) は引数 (Coin) に適用できません」というようなエラーメッセージを出します。パラメータ化されたコレクションは型安全である、ということです。

パラメータ化された型に対してはイテレータではなく「for-eachループ」が便利です。また、イテレータでキャストが不要になります。

原型を使用すると、ジェネリックスの安全性と表現力のすべてを失うことになる

原型がいまだにサポートされているのは、互換性のためです。実型パラメータの使用を強制すると、JavaシステムがJ2SE 5.0以降の新しいバージョンに移行しようとするとき、それまで普通のクラスと同様に扱ってきたジェネリック型が登場してくるコードをすべて書き換えないといけなくなります。

ということで、現在のJavaは原型と実型パラメータを持ったパラメータ化された型とが相互に扱えるような仕様になっています。例えばパラメータ化された型に対して原型で生成したインスタンスを代入できて、その逆もできたりします。本書ではこのことを移行互換性と呼んでいます。

そうなると上記のような問題が残ってしまうということでもあるんですが・・・仕方ないです。そういうわけなので「新しいコード」では原型を使用しない、なんですね。

型List<Object>のパラメータにList<String>を渡すことはできない

これ個人的に一度引っかかったことがありまして・・・こういうことです。

こういったList<Object>のパラメータに対してList<String>を渡すとコンパイルエラーになります。

「型 ○○ のメソッド print(List<Object>) は引数 (List<String>) に適用できません」というエラーメッセージが出ます。

ジェネリック型に対するサブタイプ化の規則によると、List<String>はList<Object>のサブタイプではないということになるようです。それはListインタフェースの仮型パラメータが<E>だからなんですが・・・この辺りは後の項目まで読んでまとめないとぐちゃぐちゃになりそうですね。

非境界ワイルドカード型

先ほどの例でList<String>をパラメータとして渡せるようにするにはどうすればいいでしょうか。Javaでは非境界ワイルドカード型というジェネリック型を使用したいけどどんな型パラメータが何であるか分からない、気にしたくない場合に型パラメータに対するワイルドカードを使うことができます。それは「<?>」と書きます。

この非境界ワイルドカード型を使うと先ほどの例はこうなります。

こうするとコンパイルがとおります。非境界ワイルドカード型を使うとき、そのコレクションの要素はObject型として取り出せます。しかしこれはたまたま要素の取り出ししか
行っていないからコンパイルが通っただけです。非境界ワイルドカード型を持つコレクションには、null以外の要素を挿入することはできません。それが次の話。

Collection<?>には、(null以外の)いかなる要素も入れることはできない

次のような場合はコンパイルエラーになります。

「型 List<capture#1-of ?> のメソッド add(capture#1-of ?) は引数 (String) に適用できません」というエラーメッセージが出ます。しかし、次のようにnullを挿入する場合にはコンパイルエラーになりません。

これはObject型でもエラーになります。本書ではこのことをコンパイラがコレクションの型の不変式を破壊することを防いでいる、と言っています。実型パラメータの型は何でもいいですが、その型と別の型が挿入されないようにしているということですね。つまりワイルドカード型は型安全です。

型安全とはいえ、何も要素を入れられず、要素を取り出す時はObject型で結局キャストの問題が残る非境界ワイルドカード型なんか使えない、という場合にはジェネリックメソッド境界ワイルドカードが使えるとのことです。詳細は後の項目で出てきます。

原型を使用しないことの例外

Javaではジェネリック情報が実行時に消されるという事実から、原型を使用しないということには例外が生じます。ジェネリック情報が実行時に消されるということについては後で出てきますが、簡単に言うと、ジェネリック型を使った実装はコンパイル時に前のバージョンのキャストを使ったコードのようなものに書き換えられるのです。

これにより、クラスリテラルでは原型を使用しなければなりません。クラスリテラルとは「List.class」などです。これを「List<String>.class」という風には書けないのです。

もう1つの例外は非境界ワイルドカード型以外のパラメータ化された型に対するinstanceof演算子の使用が許されていないことです。「list instanceof List<String>」といったことはできません。

一気にいろいろな用語が出てきますが、ジェネリクスをある程度知っていれば単なるおさらいのような項目ですね。長かったですが、今回は以上です。

広告
  • LINEで送る