Effective Javaを読むチャレンジ-項目26~27-

項目26 ジェネリック型を使用する

本項目の前半はサンプルのクラスをジェネリック型に変えていくジェネリック化の説明から、ジェネリック型について復習していく内容です。

なので、実際に本書の説明を読みながらサンプルコードを書いていくと、あっという間に後半に辿り着きます。説明自体はジェネリックの章の復習なので難しくないです。

ところが後半になってさらっと新しい内容が出てきます。

基本データ型のStackは生成できない

「Stack<int[]>」はできるけど「Stack<int>」はコンパイルされません。コンパイルしようとすると「エラー: 予期しない型」というコンパイルエラーが発生します。基本データ型を型パラメータに使用するときはボクシングされた型、例えばIntegerなどを使います。

境界型パラメータ

項目23の最後にジェネリックスに関する用語をまとめた表が載っていますが、そこに境界型パラメータという言葉だけ出てきています。

境界型パラメータというのは、ジェネリック型の型パラメータとして可能な値に制限をかけたものです。本書の例に倣いますが、例えばjava.util.concurrent.DelayQueueクラスのクラス宣言は次のようになっています。

この「<E extends Delayed>」が境界型パラメータです。これは実型パラメータがjava.util.concurrent.Delayedのサブタイプでなければならないことを意味しています。こうすることで、明示的なキャストやClassCastExceptionの危険なくDelayedのメソッドが使えるようになります。

この項目で結局何が言いたいのかというと、型をジェネリック型にすることでクライアントのコードでキャストが不要になり、安全で使いやすくなるからおすすめだよ、既存の型もできればジェネリック型にすればいいよ、ということですね。既存を直すとなると工数当ててコード修正してテストしてリリースしなきゃいけないので現実的にはなかなかできないでしょうけど・・・。

そこそこ長い項目26ですがほとんど苦も無く読み終わります。ジェネリックスの章の折り返し地点にしてオアシスですよ・・・この後はまたハイレベルな話題が待っています。

項目27 ジェネリックメソッドを使用する

ジェネリック化はクラスだけでなく、メソッド単位でもできます。メソッドをジェネリック化するとジェネリックメソッドになります。staticのユーティリティメソッドがジェネリック化のいい候補ということで、java.util.Collectionsクラスが例に出されています。そのCollectionクラスのsortメソッドは以下のようになっています。

このstatic(メソッドの修飾子)とvoid(戻り値の型)の間にある「<T>」が、型パラメータを宣言する型パラメータリストです。ジェネリック型ではクラス名の後ろに型パラメータリストを宣言しましたが、ジェネリックメソッドではこの位置です。この型パラメータはメソッドの宣言とメソッド本体でしか使えません。

引数で使われている「Comparator<? super T>」は境界ワイルドカード型といいますが、これについては次の項目の内容になります。

脱線したので本書の内容に戻ります。本書はジェネリックメソッドを作るのは型のジェネリック化と似ているという話から始まります。そして2つのセットの和集合を返すメソッドを例に出しています。

この原型を使ったユーティリティメソッドを型のジェネリック化と同様にジェネリックメソッドに変えようとすると、まずは型パラメータEを宣言してその型パラメータをメソッド内に適用するところから始めたくなると思います。

この状態でも型安全で使いやすいメソッドになりますが、3つのセットに対する型パラメータはすべて同じ型でなければならないので、少し使いづらいかもしれません。この状態からもっと柔軟なコードにするために上記の境界ワイルドカード型が有用です。その話が次の項目ということですね。

このメソッドを実際にテストするところから次の話題が始まります。そのコードをちょっと変形して抜粋したものが下記にあります。

上記のようにジェネリックメソッドを使うと、「Set<String> s = new HashSet<String>();」のようなジェネリックコンストラクタを呼び出すために型パラメータを明示するような必要もなく、キャストも不要になります。

これはコンパイル時にコンパイラがジェネリックメソッドの引数がどんな型であるかをチェックするからです。それにより、引数の型パラメータEがStringだと分かるので、宣言された型パラメータはStringであろうということも分かります。その結果、上記のように型パラメータが不要になっています。

このようにコードにわざわざ型の指定をしなくてもコンパイラが型を判断するような機構を型推論(type inference)と呼んでいます。

ジェネリックメソッドを使用すると型推論が使えます。ジェネリックコンストラクタを使うと同じ型パラメータを代入の左辺、右辺両方に書かないといけなかったので、クライアントコードを書くのがだいぶ楽になります。ということは、staticファクトリメソッドをジェネリックメソッドにすれば、上記のunionメソッドのように型推論を活用してもっとコードを楽に書けるようになりますね。という話が展開されています。

その締めくくりに型推論をジェネリックコンストラクタを使う際にできるようになればいいですね、と言っていますが、Java SE 7以降はダイヤモンド演算子というものが導入され、ジェネリックコンストラクタを使う際に右辺の型パラメータを省略できるようになりました。「Set<String> s = new HashSet<>();」というように書きます。この「<>」の部分がダイヤモンド演算子です。

ジェネリックシングルトンファクトリー

本書をにらみ続けてもどうにも理解できなかったので、実例を見ながら考えていくことにしました。本書によるとこのジェネリックシングルトンファクトリーはCollections.reverseOrderメソッドなどで使用されるとあったので、そのコードを見てみます。

引数のないreverseOrderメソッドはComparableインターフェースを実装したオブジェクトの自然順序付けの逆を義務付けるコンパレータを返します。コードを見てみると、そのコンパレータはstaticメンバクラスであるReverseComparatorクラスのことで、そのReverseComparatorクラスのシングルトンインスタンスをジェネリックメソッドであるreverseOrderメソッドが返しているのが分かります。こういったジェネリックメソッドがシングルトンインスタンスを返すようなパターンがジェネリックシングルトンファクトリーです。

時々、多くの異なった型に適用可能な不変オブジェクトを生成する必要がある。

上記のReverseComparatorは状態を持たないので不変クラスです。そのクラスは「implements
Comparator<Comparable<Object>>」を実装していることから分かるとおり、Comparableを実装したあらゆるオブジェクトに適用可能なコンパレータです。こういった不変オブジェクトを生成するためにジェネリックシングルトンファクトリーパターンを使用しているということです。

なんとなくジェネリックシングルトンファクトリーをイメージできたので本書に戻ります。

恒等関数の例

本書では何らかの型Tを受け取ってその型を返す関数を1つだけもつインタフェースを用意し、恒等関数を提供することを仮定してジェネリックシングルトンファクトリーパターンを説明しています。恒等関数というのは「f(x) → x」のように引数の値がそのまま出力されるような関数です。

上記のようなインタフェースを実装したクラスをあらゆる型に適用可能にした状態で提供するにはどうすればいいでしょうか。項目21では、具象戦略クラスのインスタンス生成は1度しか利用しないのであれば無名クラスで記述できるとありました。ですので、UnaryFunctionを実装するのに無名クラスを使います。同様に、項目21の話を踏まえ、UnaryFunctionは不変クラスなのでシングルトンでインスタンスを生成することにします。

また、インスタンス生成式には実型パラメータを使用しなければなりませんので、あらゆる型に適用可能ということでObjectを使用します。こうすると結果として本書のサンプルコードになります。

しかし、これだけではあらゆる型に適用可能になりません。<Object>は、<String>とは違います。というわけでここからがテクニックになります。

IDENTITI_FUNCTIONが非境界の型パラメータであるとき、上記のように「<Object>」から「<T>」へキャストできます。そのキャスト変換には無検査キャスト警告が出ますが、今回は恒等関数であるため、引数を何も変更を加えずそのまま返します。その場合は型安全とみなせるので「@SuppressWarnings(“unchecked”)」を使って警告を抑制できます。これであらゆる型に適用可能なインスタンス生成ができました。ジェネリックシングルトンファクトリーパターンは以上です。

※ここで注意して欲しいのは、「<Object>」から「<T>」へのキャストが型安全とみなせたのは型パラメータTに何も変更を加えないと分かっているからです。そうでない場合、Objectからジェネリックスの型パラメータへのキャストは型安全ではありません。非境界の型パラメータは結局Objectと同じなので、StringやDateなどの参照型はすべて非境界の型パラメータへキャストできます。例えばリストへの異なる型の混入など、安全性を壊すような実装ができちゃいます。気を付けたいところです。

広告
  • LINEで送る