17日目:ジェネリクス
アドベントカレンダー17日目の今日は、Kotlinのジェネリクスについてお話しします。Javaプログラマにとっては当たり前のことかも知れませんが、ジェネリクスというのは型を柔軟でかつ安全に扱おうという仕組みです。
ジェネリクスは既に親友さ、という方はKotlinの文法にのみフォーカスしていただければと思います。
ジェネリッククラス
KotlinのクラスはJavaと同様に、型引数を持つことができます。クラス内で扱う何かしらの変数の型に型引数を指定することで、その型を仮に決めておくことができます。クラスがインスタンス化されるなどのタイミングで型パラメータを与えます*1。そうすることで仮に決めていた型が実際に使用する型に置き換わります。具体例を見ましょう。次のクラスはコンストラクタから型が T であるオブジェクトを受け取ります。型 T を型引数として使用することを宣言しています。
class Container<T>(val component : T)
このクラスをインスタンス化します。
val container = Container<Int>(123)
コンストラクタを呼び出す際に、型パラメータとして Int を指定しているのがわかります。そして、コンストラクタの引数として Int型のリテラル 123 を指定しています。このタイミングで型引数 T が Int として見なされたクラス Container のインスタンスが生成されるわけです。
ちなみに上記のインスタンス生成で、型パラメータとコンストラクタのパラメータの型は同じですので、同じ情報を2ヶ所に記述していることになり冗長です。この場合Kotlinコンパイラは型推論をしてくれるので、次のように無駄なく記述できます。
val container = Container(123) val component : Int = container.component println(component) // 123
さらに生成したオブジェクトからプロパティ component の値を取得しています。component はクラス定義の段階では型が確定していませんでしたが、インスタンス化のときに型が確定したので、上記の例では Int型の値として受け取っているのがわかると思います。
で、何が嬉しいの?
例で作成したクラス Container は型パラメータに任意の型を指定することで、あらゆる型のオブジェクトのコンテナとして機能するようになりました。まぁそれはいい。それのどこが便利なのか。次のようなコードでも同じことができるのでは?
class Container2(val component : Any) fun main(args : Array<String>) { val container = Container2(123) val component : Int = container.component as Int println(component) // 123 }
クラス Any はKotlinのすべてのクラスのスーパクラスとなるクラスです。Java の java.lang.Object のような存在です。なるほど。クラス Container2 のコンストラクタで受け取るオブジェクトの型を Any にすれば、あらゆる型に対応できますね。事実、このコードはコンパイルも成功しますし、実行結果も期待通りです。
しかし待ってください!見慣れないキーワードが登場しました。キーワード as です。これはキャスト(型変換)のためのキーワードです。component の型が Any なので Int として扱うにはキャストが必要です。これは型安全ではありません*2!次のようなコードを簡単に書けてしまいます。キャストの失敗によりプログラムはクラッシュします。
//注意!!! 例外投げます val container = Container(123) val component = container.component as String
このように、ジェネリクスは型を柔軟に扱うだけでなく、型安全性も提供してくれる強力な機能なのです。
こんなジェネリクスもあるよ
2つ以上の型引数を持ったクラス
型引数を複数個持つことも可能です。型引数をカンマで区切って宣言するだけです。
class Pair<T, U>(val component1 : T, val component2 : U)
コレクションはジェネリッククラス
標準ライブラリはコレクションクラスをたくさん提供しています。コレクションクラスは、複数のオブジェクトをまとめて管理することに責任があり、管理しているオブジェクトの型には関心を持ちません。それらの型に自由度を持たせるためジェネリッククラスとして定義されています。
一番簡単な例は、配列であるクラス Arrayです。関数 main の唯一の引数のクラスです。
ジェネリック関数
クラスと同様に、関数も型引数を持つことができます。
fun <T> head(array : Array<T>) = array[0] fun last<T>(array : Array<T>) = array[array.size - 1]
関数 head と last、書き方が若干異なりますが、どちらも文法的にはOKらしいです。
今日はここまで
ジェネリクスの基本的なことは以上です。ここからは使用頻度があまり高くないジェネリクスの機能について見て行きます。
ジェネリクスはもうお腹いっぱいかな、という方は「まとめと次回予告」に飛んでいただいてOKです。
上限制約
型引数に対して制約を設けることができます。型引数に対して任意の型を指定できますが、制約によって指定できる型を絞ることができます。
上限制約によって、型階層の上限による型引数の制限ができます。Javaでは <型引数 extends 上限型> という風に書くアレです。Kotlinでは extends の代わりにコロンを使用するだけです。ひとつの型引数が複数の上限を持つ場合は where 節で区切ります。
trait TraitA trait TraitB trait TraitC class NiceClass() where T : TraitB, T : TraitC
分散
まず次の問題のあるJavaコードを見て、どこが悪いのか考えてみましょう。
// Java final Integer ints = {1, 2, 3}; final Number nums = ints; nums[0] = Double.valueOf(0.1);
このコードはコンパイルに成功します。しかし実行すると3行目で実行時例外を投げます。3行目だけを見ると、悪くはなさそうです。DoubleをNumberと見なすことが可能だからです。2行目を見ると、微妙ですね。2行目をコンパイル可能にしたばっかりに、3行目のような記述を許し、その結果例外を投げてしまいます。これを禁止すれば万事解決、というわけにもいきません。IntegerをNumberと見なしたい場合も少なくないからです。
では、上記のコードをもう一度Javaで、今度はジェネリクスを使って表現してみます。
//Java final List<Integer> ints = Arrays.asList(1, 2, 3); final List<Number> nums = ints;
これは2行目でコンパイルエラーとなります。配列のときと違い、List<Integer>をList<Number>と見なしてくれないようです。しかし、2行目を次のように書き直すとコンパイルを通ります。
// Java final List<? extends Number> nums = ints;
このように、型パラメータの継承関係を受け入れたり、拒否したりするような操作や機能のことを分散(variance)と言います*3。
用語を整理しておきましょう。
不変(invariance) | 型パラメータ間の継承関係を考慮しません。つまりList<Integer>とList<Number>は別物であると見なします。 |
共変(covariant) | 型パラメータ間の継承関係を考慮します。Javaの配列は共変です。また、List<Integer>はList<? extends Number>となることができます。 |
反変(contravariant) | 型パラメータ間の継承関係を考慮します。List<Object>はList<? super Number>となることができます。 |
分散の型安全性
Javaの配列が共変で、型安全でないことがわかりました。ジェネリクスはデフォルトで不変であることによって型を安全に保ちますが、共変や反変になっても型安全を貫きます。そのための工夫は、言葉で説明するよりもコードを交えて説明した方がわかりやすいと思いますので、次のセクションで使用箇所分散と共にお話しします。
使用箇所分散(型プロジェクション)
上記のJavaの例のように、Kotlinでも型パラメータに共変性または反変性を指定することができます。その指定をジェネリッククラスを使用する場所(コード上)で行うため、これを使用箇所分散と呼びます*4。
例を示すために次のようなジェネリッククラスを定義しました。
class Container<T>(var component : T?)
このクラスを使った例を示します。特に何も指定していないので、不変です。つまり次のコードはコンパイルを通りません。
val int : Container<Int> = Container(777) val num : Container<Number> = int // compile ERROR!!!
では、変数 num の型を共変にします。共変性を指定するにはアノテーション out を使用します。
val num : Container<out Number> = int
これはコンパイルを通ります。通りますが、Javaの配列のような悲劇を思い出しますね。つまり、num はContainer<Number> ですが、その実体は Container<Int> なので、 component に例えば Double型の値をセットしようとすると実行時例外が起こります。安心してください。前述しましたが、ジェネリクスは安全です。この場合、num を介してcomponent の値はセットできません。仮に num.component = 5 のようなコードを書いてもコンパイルに文句を言われるだけです。一般的に言うと、out 指定された型パラメータを持つ変数に対する代入ができなくなります*5。値を取得するだけなら安全なので、可能です。つまり out 指定すると、その型を持った変数に対しては読み取り専用となります。読み取り専用、という意味の「out」らしいです。
次に反変の例を見てみましょう。反変性はアノテーション in によって指定します。お気づきかも知れませんが、in 指定された型の変数に対しては書き込み専用となります。書き込み専用という意味の「in」ですねー。
val num : Container<Number> = Container(5.0) val int : Container<in Int> = num
正確には、値を取得することもできます。が、取得する際に、その型は Any? です。というのは、<in Int>は<Number>でも<Any>でも受け付けるため、上限がありません。そのため、取得する際の型はすべてのルートである(しかもNULL許容型) Any? です。
スタープロジェクション
使用箇所分散においてスタープロジェクションと呼ばれる、Javaの非境界ワイルドカードに相当する記法があります。型パラメータ指定部分に * を記述するだけです。これは <out Any?>の構文糖衣に過ぎません。
宣言箇所分散
使用箇所分散は、Javaでも可能でした。次に紹介するのは宣言箇所分散です。これはJavaにはない機能です。使用箇所分散に対して型引数の宣言箇所で不変、共変、反変を指定するのでこのような呼び方をします
今まで使用してきたクラス Container のプロパティ component をイミュータブル(変更不可)にします。
class Container<T>(val component : T)
val 宣言したのでインスタンス生成後、component を変更することはできません。ここで共変の性質を思い出してください。指定された型パラメータに対する変数は読み取り専用でした。つまり変更不可(読み取り専用)となった component に対して宣言箇所分散による共変の指定は特に不都合を引き起こすものではありません。むしろ Container を柔軟に扱えるようになります。
class Container<out T>(val component : T) fun main(args : Array<String>) { val int : Container<Int> = Container(123) val num : Container<Number> = int val t = num.component num.component = t // compile ERROR!!! }
変数 num を介してプロパティ component に値をセットしようとしていますが、この部分はコンパイルエラーとなります。out 指定されていることもありますが、それ以前に val 宣言されているからです。
まとめと次回予告
今日はジェネリクスについて、その性質と特長、Kotlinにおける文法を学びました。クラスや関数は型引数を取ることでジェネリッククラス(あるいはジェネリック関数)になることができます。ジェネリッククラスにより型を柔軟かつ安全に扱うことができます。
明日はKotlinの標準ライブラリが提供する便利なクラスや関数をいくつか紹介します。
日記
昨日は暖かかったです。午後は散歩しました。