Effective Javaを読むチャレンジ-項目28その1-

項目28 APIの柔軟性向上のために境界ワイルドカードを使用する

ジェネリックスを使用したAPIを公開する場合、境界ワイルドカードを使えばその柔軟性を高めることができるという話。その基本原則としてPECS規則(別にGet&Put原則というものもあります)というものを忘れないように、とのことです。

ジェネリックスの章もクライマックスですね・・・何とか読んでいきたいと思います。

すっかり聞き慣れたパラメータ化された型は不変であり、List<Type1>とList<Type2>はお互いスーパータイプの関係でもサブタイプの関係でもないという話から始まります。不変というのは型変換ができないということですね。これはすごく道理にかなっていると。

ところがこの不変である型パラメータでは柔軟性に欠けることがあるということです。

本書に何度も出てきたStackクラス。このクラスにたくさんの要素を受け取りそれらをすべてスタックに積むというメソッドを追加することを考えます。

特に問題があるように見えないパラメータ化された型を使ったこのAPIですが、次のようなコードを書くとエラーになってしまいます。

このコードは結局、pushAll(Iterable<Number> src)に対してIterable<Integer>を渡すことになります。パラメータ化された型が不変であるため、この型変換は失敗します。

この問題の解決のために境界ワイルドカード型という特殊な型がJavaには用意されています。

境界ワイルドカード型

先ほどのエラーが起きないようなAPIにするにはどうすればいいのか考えます。先ほどの失敗は「NumberのIterable」に「IntegerのIterable」を渡したから起きたものでした。それなら「NumberのサブタイプであるIntegerのIterable」も渡せれば話は簡単なのに・・・と思うことでしょう。感覚的にはそう思うと思います。ですがJavaではパラメータ化された型は不変で、そこは変わりません。

そこで用意されているのが境界ワイルドカード型です。それを使うとpushAllメソッドはこう書き換えることができます。

「<? extends E>」という部分が境界ワイルドカード型です。この記述の意味は「Eのサブタイプである何らかの型」となります。サブタイプという言葉には自分自身のタイプも含まれているので、E自身が含まれるということは自明の理ということになります。Javaはポリモーフィズムなどあるので型名を見ると自然とそのサブタイプからも変換できると思いがちですが、ジェネリックスでは不変であり、サブタイプを許容する場合にはわざわざこういう形しないといけないのですね・・・ただこれは次の話を見れば納得できると思います。とにかくこれで上記のエラーは解決されるようになりました。

さて今度はスタックの要素をすべて取り出して引数のコレクションに追加するメソッドを考えます。本書ではメソッドの名前はpushAllに対してpopAllとなっています。

この場合も問題が起こるコードがありうるということですね。それは次のようなコードです。

このコードは先ほどと同じエラーが発生します。理由もだいたい同じ。Collection<Number>に対してCollection<Object>を挿入しようとしています。それではこのエラーが起きないようなAPIにするにはどうすればいいのか考えることにします。

先ほどの失敗は、引数である「NumberのCollection」に「ObjectのCollection」を渡そうとしたから起きたものでした。それなら引数である「NumberのCollection」に「NumberのスーパータイプであるObjectのCollection」を渡せれば話は簡単・・・とは思えないですね。感覚的にはいい方法は浮かびません。クライアントの方でどうにかして、と言いたくなりそうです。

ところがJavaではこういった場合のためにも特殊な型が用意されています。

この「<? super E>」がそうです。この記述の意味は「Eのスーパータイプである何らかの型」となります。サブタイプという言葉同様、E自身もスーパータイプに含まれます。そして実はこの書き方も境界ワイルドカード型と呼びます。それでは区別がつかないので「<? extends E>」を上限境界ワイルドカード、「<? super E>」を下限境界ワイルドカードと呼んだりします。

これで先ほどのエラーも解消されるようになりました。

本書ではこの後、いきなりプロデューサーだコンシューマーだ言い出してきて、さっぱり意味が分からなくなります。その話は境界ワイルドカード型を適切に使うための原則というか、今までこの項目で説明されてきた内容を簡単に言い表した語呂合わせみたいなものです。それはPECSというものです。

PECS

PECSは「プロデューサー(Producer)extends、コンシューマー(Consumer)super」 のイニシャルを取った省略語です。

だからプロデューサーとかコンシューマーって何?ということなんですが、この項目の例をとって考えてみます。pushAllメソッドでは、引数で渡されたIterableの型パラメータEは、Stackクラスの型パラメータEに格納するために使用されていました。つまり引数のEはStackのEを増やす(生産する)ために使用されています。一方、popAllメソッドでは、引数で渡されたCollectionの型パラメータEは、StackクラスのEを取り出すために使用されていました。つまり、引数のEはStackのEを減らす(消費する)ために使用されています。

そして最終的に、生産するために使用される引数のEには「<? extends E>」、消費するために使用される引数のEには「<? super E>」を適用しましたね。これを簡単に書いたのが「プロデューサー(生産者)extends、コンシューマー(消費者)super」です。

まとめると、PECSとは、パラメータ化された型Tが生産されるために使用される場合(生産者)には「<? extends T>」、消費されるために使用される場合(消費者)には「<? super T>」を使って型パラメータを宣言すると柔軟性が向上するよ、というお約束のことなのです。

Get&Put原則

本書ではPECSを紹介した後、NaftalinとWadlerはGet&Put原則と呼んでいた、と言っています。誰のことかというと「Java Generics and Collections」という本がO’reilly出版から発売されていて、その本の著者の名前です。その本の2.4節に境界ワイルドカードの適切な使用方法について書かれていて、そのなかでGet and Put Principle(Get&Put原則)という言葉が使われている、ということです。

Get&Put原則は簡単にいうと、メソッドの中で引数Eから値を取得(get)するなら「<? extends E>」、値を格納する(put)なら「<? super E>」を使うというものです。

この本、和訳ないですかね・・・。

この項目は、ここからPECSの活用例などの詳細な話となります。ちょっと長いし難しいので今回はここまでにします。

広告
  • LINEで送る