Genericsの共変と反変

 ここまでの説明で,ScalaのGenericsは単にJavaのGenericsの記法を変えただけと思われた方も居られるかもしれません。しかし,ScalaのGenericsにはJavaのGenericsと異なる特徴がいくつかあります。その内の一つが,ここで説明する共変と反変の概念です。

共変

 Genericsにおける共変とは,おおまかに言って次のようなものです。まず,あるGenericなクラス(あるいはインタフェース,trait)Gと型T1,T2があったとします。このとき,T1がT2のサブタイプならばG<T1>もG<T2>のサブタイプである場合に,Gは共変であるといいます。

 これだけではわかりにくいと思うので,具体例を挙げて説明します。まず,次のように,java.util.List<Object>型の変数s1とjava.util.List<String>型の変数s2があったとします。

java.util.List<Object> s1 = ...;
java.util.List<String> s2 = ...;

 StringはObjectのサブクラスですが,このとき,s1 = s2;という代入はJavaのGenericsでは許されず,コンパイルエラーになります。ここで,StringはObjectのサブタイプであるにもかかわらず,java.util.List<String>はjava.util.List<Object>のサブタイプではないので,JavaのGenericsによって定義されるクラス(やインタフェース)は共変では無いことになります。これは,別にJavaのGenericsの融通が利かないせいではなく型安全性を保つうえで,共変なGenericsには問題点があるためです。

 s1 = s2;という代入を許したと仮定してみましょう。s1はObjectを要素として持つListですから,次のようにjava.util.Date型のオブジェクトを追加しても大丈夫なはずです。

s1.add(new java.util.Date());

 しかし,s1 = s2;によって,s1とs2は同じListを指すようになっているため,Stringを要素として持つListであるs2に対しても,java.util.Dateが追加されてしまいます。これでは,せっかくGenericsによって保証された型安全性(java.util.List<String>にはStringしか含まれない)を破壊してしまいます。このような問題点があるため,JavaのGenericsは共変では無いのです。

 ちなみに,Java 5より前からある機能である配列については,配列の要素型Aが要素型Bのサブタイプであるならば,Aの配列型もBの配列型のサブタイプであるという関係が成り立ちます。つまり,Javaの配列は共変です。そのため,Javaの配列については,以下のように,(キャストがあるわけでも無いのに)型安全では無いコードがコンパイルを通ってしまいます。

String[] s2 = new String[1];
Object[] s1 = s2;
s1[0] = new java.util.Date(); //実行時にArrayStoreExceptionが発生

 このように,JavaのGenericsが共変ではないことにはちゃんとした理由があるわけですが,この制限が強過ぎる場合が存在します。例えば,上で定義したimmutableなLinkクラスを使う場合を考えてみます。この場合,いったんLinkクラスのインスタンスが作られた後は,java.util.Listの場合と違い,インスタンスに対する書き込みが行われないため,Link<Object>型の変数にLink<String>型の値を代入しても問題無いと考えられますが, JavaのGenericsではそのようなことは許可されません。

 ScalaのGenericsも,特に指定を行わない限り,基本的にはJavaの場合と同様に共変ではありません。例えば,先程のLinkクラスを使った,次のコード

val link: Link[Any] = new Link[String]("FOO", null)
...

は以下のようなコンパイルエラーになります。Link[Any]が要求されている個所にLink[String]が現れたのがエラーの理由ということで,Linkクラスが共変でないことがわかります。

(fragment of Link.scala):2: error: type mismatch;
 found   : this.Link[String]
 required: this.Link[Any]
val link: Link[Any] = new Link[String]("FOO", null)

 しかし,Scalaではクラス/trait定義で,型パラメータの前に+という記号を付加することで,共変なクラス/traitを作成することができます。試しに,Scalaで共変なクラスを定義してみることにします。題材は,先程のLinkクラスです。Linkクラスの定義の,型パラメータTの前に+を付加して,以下のようにします。

class Link[+T](val head: T, val tail: Link[T])

 Linkクラスをこのようにして定義すると,Linkクラスを使った先程のコードがコンパイルを通るようになります。ちなみに,共変にすると型安全性が保てないクラスを定義した場合,コンパイルエラーになります。しかし,実は,immutableなデータ構造を定義する場合などに,この制限が問題になる場合が存在します。

 例えば,先程のLinkクラスに対して,引数として渡された要素を先頭に追加して返す新しいメソッドprependを追加したとします。

class Link[+T](val head: T, val tail: Link[T]) {
  def prepend(newHead: T): Link[T] = new Link(newHead, this)
}

 prependは元のLinkクラスのインスタンス自体は変更していませんから問題は無いはずですが,このコードをコンパイルしようとすると,次のようなコンパイルエラーになります。

Link.scala:2: error: covariant type T occurs in contravariant position in type T
 of value newHead
  def prepend(newHead: T): Link[T] = new Link(newHead, this)

 実は,型パラメータを共変にした場合,その型パラメータはそのままでは引数の位置(ここではnewHead)などに書くことができなくなります。この制限は,prependをGenericなメソッドにして,次のように書くことで回避することができます(何故これで制限を回避できるかの説明は省きます)。

class Link[+T](val head: T, val tail: Link[T]) {
  def prepend[U >: T](newHead: U): Link[U] = new Link(newHead, this)
}

 GenericなメソッドはJavaにもある機能ですが,メソッドを型によってパラメータ化したメソッドで,Genericなクラスなどと同様に,型安全で汎用的なメソッドを定義するために使われます。例えば,連載第5回で登場したListクラスのmapメソッドはGenericなメソッドで,

override final def map[B](f : (A) => B) : List[B]

のように定義されています。mapメソッドは,引数として与えられた関数fをListの各要素に適用し,その結果を集めたListを返すメソッドです。しかし,fを適用した結果の型が何になるかはmapメソッドの定義時点では決めようが無いので,Genericなメソッドによって型をパラメータ化することで,mapメソッドを型に対して汎用的にすることができるわけです。

 Genericなメソッドは,メソッド名の直後に[]で囲って型パラメータを書くことで定義できます。[]内の型パラメータはそのメソッドの中では普通の型として使うことができます。なお,型パラメータの後に続けて<: 型名または>: 型名とすることで,ある型のサブタイプ/スーパータイプでなければならないという制約を付けることもできます。例えば,[U <: T]と書いた場合,UはTのサブタイプでなければならないという意味になり,[U >: T]と書いた場合,UはTのスーパータイプでなければならないという意味になります。

この先は会員の登録が必要です。今なら有料会員(月額プラン)登録で5月末まで無料!