ジェネリクス: in, out, where
Javaと同じように、Kotlinのクラスは型パラメータを持つ事が出来ます:
class Box<T>(t: T) {
var value = t
}
このようなクラスのインスタンスを作成するためには、単に型引数を提供すればよろしい:
val box: Box<Int> = Box<Int>(1)
しかし、パラメータを推測することができる場合には、(例えば、コンストラクタの引数からとか)、型引数を省略することができます:
val box = Box(1) // 1 は Int型のため、コンパイラはBox<Int>だとわかる
分散
(訳注: varianceの事。アンカのidになるのでサブタイトルには括弧書きでは足していない)
Javaの型システムの最もトリッキーな部分の一つは、ワイルドカード型(JavaのジェネリックのFAQを参照してください)です。 そして、Kotlinにはありません。 その代わり、2つの別のものがあります:宣言箇所の分散(declaration-site variance)と型プロジェクション(type projections)です。
まずは、Javaがこれらの神秘的なワイルドカードを必要とする理由について考えてみましょう。
この問題はEffective Java, 3rd Editionの項目31「APIの柔軟性を高めるためのバインドされたワイルドカードの使用」で説明されています。
まず、Javaでジェネリック型は 不変(invariant) です。
これは、 List<String>
は List<Object>
のサブタイプ ではない ことを意味します。
もしリストが 不変 でなかった場合は、次のコードはコンパイルされ、実行時に例外を発生させていたので、それは、Javaの配列より良いものではなかったでしょう。
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! ここでのコンパイルエラーが、後のruntime exceptionを防いでくれます。
objs.add(1); // Integer を Strings のリストへ入れる
String s = strs.get(0); // !!! ClassCastException: Integer を String へキャストできない
実行時の安全性を保証するために、Javaはそのようなことを禁止しているのです。
しかし、これはいくつかの含意があります。
例えば、 Collection
インタフェースの addAll()
メソッドを考えます。
このメソッドのシグネチャは何でしょうか?
直感的には、こう書けそうなものだと思うでしょう:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
しかしこれだと、次のような簡単なこと(完全に安全である)を行うことができなくなります。
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !!! addAllのネイティブの宣言ではコンパイルできません:
// Collection<String> は Collection <Object> のサブタイプではありません
}
(あたながJavaを学んでいるなら、この事を苦労して学んだ事だろう。Effective Java, 3rd Editionの項目28「 配列よりリストを使え 」を参照してください。)
これが、 addAll()
の実際のシグネチャが以下の通りになる理由です:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
ワイルドカード型引数 ? extends E
は、このメソッドが受け入れるコレクションは E
またはそのサブタイプ のオブジェクトであって、 E
だけではないことを示します。
この事は、私たちが安全に E
をitemsから 読み取る ことができる(このコレクションの要素は E
のサブクラスのインスタンスです)けれど、
未知の E
のサブタイプに対して、どのオブジェクトなら準拠しているかが分からないため、 書き込みができない という事を意味します。
この制限と引き換えに、私たちは望んだ動作を得ます: Collection<String>
は Collection<? extends Object>
のサブタイプ である ということ。
言い換えると、extends-bound (上限(upper bound))付きのワイルドカードは型を_共変(covariant)_にします。
このトリックがなぜ働くのかを理解するための鍵は、かなりシンプルです:
コレクションからアイテムを 取り出す ことだけ出来れば、String
のコレクションを使用して、 Object
で読み出すのは何も問題ありません。
反対に、コレクションにアイテムを 入れる ことだけできるのならば、 Object
のコレクションを使用し、 String
を入れても良いのです。
Javaには List<? super String>
があって、これはString
の他に、その基底型ならなんでも受け付けます。
後者は 反変性(contravariance) と呼ばれ、
あなたはList<? super String>
のString
を引数としたメソッドを呼ぶことのみができます(例えば、 add(String)
や set(int, String)
を呼ぶことができます)。
もしList<T>
の中にある T
を返す何かを呼んだとき、得るのは String
ではなく Object
となります。
ジョシュア・ブロック (Joshua Bloch) はこれらのオブジェクトを 「 プロデューサ(生産者) からのみ 読み込み 、コンシューマ(消費者) にのみ 書き込む 」と呼びました。彼の勧めによると:
「最大の柔軟性を得るために、プロデューサやコンシューマを表す入力パラメータにワイルドカードタイプを使用する」 次の記憶術 (mnemonic) も提案しています。
PECS, Producer-Extends, Consumer-Super を意味します。
プロデューサオブジェクトを使用する場合(たとえば、
List<? extends Foo>
)、このオブジェクト上のadd()
やset()
を呼び出すことができません。 しかし、このオブジェクトは イミュータブル(不変) であるというわけでもありません。例えば、clear()
は全くパラメータを取らないため、リストからすべての項目を削除するためにclear()
を呼び出しても構いません。ワイルドカード(または分散の他の型)によって唯一保証されるのは 型の安全性 です。不変性(immutability)は全く別の話です。
宣言箇所分散(declaration-site variance)
ジェネリックインターフェイスの Source<T>
があると仮定します。また、パラメータとして T
をとるメソッドを持たず、 T
を返すメソッドのみを持つとします。
// Java
interface Source<T> {
T nextT();
}
この場合、 Source<Object>
型の変数で Source<String>
のインスタンスへの参照を保持するのに完全に安全です - 呼び出せるコンシューマメソッドがないからです。しかし、Javaはその事を知らず、そういうコードを禁止します:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Java では許可されていない
// ...
}
これを修正するために、Source<? extends Object>
型のオブジェクトを宣言する必要があります。
そうする事は実際の所、意味はありません。
なぜならそのように定義した変数でも、全てのメソッドを以前と同様呼び出せてしまうからです。
より複雑な型にした所で、なんのご利益も得られていません。
しかし、コンパイラはそれを知りません。
Kotlinでは、コンパイラにこの種の事柄を説明する方法があります。これは、 宣言箇所分散(declaration-site variance) と呼ばれています:
Source
の 型パラメータ T
を Source<T>
のメンバから 返す (プロデュースする)のみで、消費されることがない(consumeされることが無い)ということを保証するために、
アノテーションを付けることができます。そのためには、out 修飾子を使います:
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // これは OK 、なぜなら T はoutパラメータのため
// ...
}
一般的なルールは次のとおりです。クラス C
の型パラメータ T
が、 out として宣言されているとき、
C
のメンバの中で out の位置でのみTが現れ得る。
しかしその制約のおかげで、 C<Base>
は 安全にC<Derived>
のスーパータイプになる事が出来ます。
言い換えると、クラス C
は、パラメータ T
に 共変(covariant) である、または T
が 共変 の型パラメータであるとなります。
C
は T
の プロデューサ であり、 T
の コンシューマ ではない、と考えることができます。
out 修飾子は、 分散アノテーション と呼ばれ、それは型パラメータの宣言箇所で提供されているので、宣言箇所分散(declaration-site variance) を提供します。 これは、型を使用する側にワイルドカードをつけて共変にする、Javaの 使用箇所分散(use-site variance) とは対照的です。
out に加えて、Kotlinは in という補完的な分散(variance)アノテーションを提供します。
これは、型パラメータを 反変(contravariant) にします。
消費される(consumeされる)のみであり、決してプロデュース(生産)されない、という意味です。
反変クラスの良い例は Comparable
です:
abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 は Number のサブタイプである Double 型をもつ
// それゆえ、 x を Comparable<Double> 型の変数へ代入できる
val y: Comparable<Double> = x // OK!
}
(C#で随分前から使われて成功しているように)in と out は自己説明的であるゆえに、以前述べたような記憶術(ニーモニック)は不要となります。 より高次の目的のために言い換えることすらできます:
実存的言い換え:コンシューマ(消費者)は in、プロデューサ(生産者)は out ! :-)
タイププロジェクション(型投影)
利用箇所の分散(Use-site variance):タイププロジェクション
型パラメータTをoutとして宣言し、使用箇所でサブタイプする問題を避ける事はとても簡単ですが、
ある種のクラスではT
は返すだけ、と制限するのが不可能な場合もあります。
その良い例は、Array
です。
class Array<T>(val size: Int) {
fun get(index: Int): T { /* ... */ }
fun set(index: Int, value: T) { /* ... */ }
}
このクラスは T
の共変または反変のいずれかにもなることはできません。
そして、これはある種の柔軟性に欠けます。
次の関数を考えてみます:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
この関数は、ある配列から別の配列へ、要素をコピーしようとしています。 それでは、実際にそれを使ってみましょう:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3)
copy(ints, any) // エラー: (Array<Any>, Array<Any>) が期待されている
ここで以前と同様のおなじみの問題に遭遇します: Array<T>
は T
において 不変(invariant) であり、ゆえに Array<Int>
も Array<Any>
も、どちらも他方のサブタイプではありません。
どうして違うのか?
コピーが何か予想外の事、たとえばfrom
へのString
の 書き込み などをやっている 可能性がある ためです。
もしそんなケースの場合にそこに Int
の配列を実際に渡したら、ClassCastException
が後になって投げられるでしょう。
copy()
が from
に書き込むことを禁止する為には、以下のようにします:
fun copy(from: Array<out Any>, to: Array<Any>) { ... }
これが タイププロジェクション(型投影) です。
この意味する所は、from
は単純に配列なのではなく、制限された( 投影された )ものであるということです。
型パラメータ T
を返すメソッドだけを呼ぶ事が出来ます。
つまりこの場合は get()
を呼ぶことのみができるということです。
これが、 使用箇所分散(use-site variance) のための我々のアプローチであり、Javaの Array<? extends Object>
に対応しますが、少しだけシンプルなものになっています。
in
も同様にタイププロジェクション(型投影)で使用できます:
fun fill(dest: Array<in String>, value: String) { ... }
Array<in String>
は Javaの Array<? super String>
に対応します。
すなわち、 CharSequence
の配列や Object
の配列を fill()
関数へ渡すことができます。
スタープロジェクション (star-projections)
型引数について何も知らないが、それでも安全な方法で使用したいと、時には言いたくなることもあるでしょう。 ここでの安全な方法とは、 個々のジェネリック型が実際にインスタンス化される時にそのプロジェクションのサブタイプになるような、 そんなジェネリック型のプロジェクションを定義する、という意味です。
Kotlinはこのために、いわゆる スタープロジェクション (star-projection) 構文を提供します:
Foo <out T : TUpper>
の場合、T
は上限TUpper
を持つ共変の型のパラメータであり、そのupper boundはTUpper
で、Foo <*>
はFoo<out TUpper>
と等価となります。これは、T
が不明でも、安全にFoo <*>
からTUpper
の値を読み取ることができることを意味します。T
が反変(contravariant)の型パラメータであるFoo<in T>
については、Foo<*>
はFoo <in Nothing>
と等価です。それはT
が不明な時はFoo <*>
には何も書き込めない事を意味します。Foo <T : TUpper>
でT
は上限TUpper
を持つ不変(invariant)の型パラメータの場合は、Foo<*>
は、値を読み込む時はFoo<out TUpper>
と等しく、値を書き込む時はFoo<in Nothing>
と等しくなります。
ジェネリック型がいくつかの型パラメータをもつ場合、それらは独立してプロジェクション(投影)することができます。
例えば、型が interface Function<in T, out U>
として宣言されている場合なら、次のようなスタープロジェクションを使用することができます:
Function<*, String>
はFunction<in Nothing, String>
を意味しますFunction<Int, *>
はFunction<Int, out Any?>
を意味しますFunction<*, *>
はFunction<in Nothing, out Any?>
を意味します
スタープロジェクションは非常にJavaの raw タイプににていますが、安全です。
ジェネリック関数
型パラメータを持つことができるのはクラスだけではありません。関数も同じです。 型パラメータは、関数名の前に置かれます。
fun <T> singletonList(item: T): List<T> {
// ...
}
fun <T> T.basicToString() : String { // 拡張関数
// ...
}
ジェネリック関数を呼び出すには、関数名の 後に 呼び出し箇所で型引数を指定します。
val l = singletonList<Int>(1)
型引数は文脈から推測出来る時には省略可能です。だから以下のようにも書く事が出来ます:
val l = singletonList(1)
ジェネリックの制約
ある型パラメータに置換することができるすべての許容される型の集合を、 ジェネリック制約(generic constraints) によって制限する事が出来ます。
上限
制約の最も一般的なタイプは、Javaの extends キーワードに対応する 上限(upper bound) です。
fun <T : Comparable<T>> sort(list: List<T>) { ... }
コロンの後に指定されたタイプが 上限 です。 Comparable<T>
のサブタイプだけを T
として置換することができます。例えば:
sort(listOf(1, 2, 3)) // OK. Int は Comparable<Int> のサブタイプです
sort(listOf(HashMap<Int, String>())) // エラー: HashMap<Int, String> は Comparable<HashMap<Int, String>> のサブタイプではない
デフォルトの上限(いずれも指定されていない場合)は Any?
です。角括弧内では上限は一つだけ指定することができます。
同じ型パラメータに複数の上限を必要とする場合、独立した where 句が必要になります:
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
Definitely non-nullable型
(訳注:確実にnullableじゃない型)
Javaのジェネリック型とインターオペラブルにする為に、 Kotlinはジェネリック型パラメータをdefinitely non-nullable(確実にnullableじゃない)として 宣言する事が出来ます。
ジェネリック型T
をdefinitely non-nullableにするには、型を& Any
をつけて宣言します。
例えば、T & Any
のように。
definitely non-nullalbe型は上限(upper bound)としてnullableを持たなくてはいけません。
definitely non-nullalbe型を使うもっとも一般的なユースケースとしては、
@NotNull
を引数に持つJavaのメソッドをオーバーライドしたい時です。
例えば、以下のload
メソッドを考えます:
import org.jetbrains.annotations.*;
public interface Game<T> {
public T save(T x) {}
@NotNull
public T load(@NotNull T x) {}
}
Kotlin側でこのload()
メソッドを正しくオーバーライドする為には、
以下のT1をdefinitely non-nullableとして宣言する必要があります:
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
// T1 は definitely non-nullable
override fun load(x: T1 & Any): T1 & Any
}
Kotlinだけで作業している分には、definitely non-nullable型を明示的に宣言する必要は無いでしょう。 というのは、Kotlinの型推論はこのケースを面倒見てくれるはずだからです。
型消去(Type erasure)
Kotlinが行うジェネリック宣言の使用の型安全性チェックはコンパイル時に行われます。
実行時には、ジェネリック型のインスタンスは実際の型引数について、何の情報も持ちません。
型情報は_消去された(erased)_と言います。
例えば、Foo<Bar>
やFoo<Baz?>
のインスタンスは消去されて単にFoo<*>
になります。
ジェネリクスの型チェックとキャスト
型消去の為に、実行時にジェネリック型のインスタンスがある引数で作られたかどうかをチェックする一般的な方法は存在しません。
そしてコンパイラはそのようなis
チェックを禁止しています。
つまりints is List<Int>
とかlist is T
(型パラメータ)とかは禁止です。
しかしながら、スタープロジェクトされた型のインスタンスかどうかについてはチェック出来ます:
if (something is List<*>) {
something.forEach { println(it) } // この要素は `Any?` と型づけされる
}
また、もしすでに(コンパイル時に)静的にチェックされた型引数を持っている場合、
ジェネリックと関係無い部分についてはis
チェックをしたりキャストしたり出来ます。
この場合角括弧は省略される事に注意:
fun handleStrings(list: MutableList<String>) {
if (list is ArrayList) {
// `list` は `ArrayList<String>` にスマートキャストされる
}
}
型引数を考慮しないキャストもキャストと同様だけれど型引数を省略したシンタックスで行えます: list as ArrayList
ジェネリックな関数呼び出しの型引数のチェックもコンパイル時にだけチェックされます。
関数の本体(body)では型パラメータは型チェックには使えません。
型パラメータへのキャスト(foo as T
)はチェックされません。
唯一の例外はインライン関数のreified型パラメータです。
そのケースでは、呼び出し箇所(call site)で実際の型引数を持っています。
この事が、型パラメータによる型チェックやキャストを可能にします。
しかしながら、上に述べた制限は、
内部でチェックやキャストに使われるジェネリック型のインスタンスに依然として適用されます。
例えば、型チェックのarg is T
において、arg
がジェネリック型自身のインスタンスだったならば、
その型引数は依然として除去されます。
(訳注:うまく訳せなかったが、ようするにargがList<String>
などのように何らかのジェネリック型のインスタンスの場合、いくらTの方がreifiedであってもargの方の型のString
がtype erasureで除去されてしまう事は同様、という事が言いたいのだと思う)
チェック無しキャスト (Unchecked casts)
具体的な型引数を与えたジェネリック型への型キャスト、例えばfoo as List<String>
のようなものは、実行時にはチェック出来ない。
このようなチェック無しキャスト(Unchecked casts)は、高レベルのプログラムのロジックからは類推出来るけれどコンパイラが直接推測する事が出来ないようなケースで使われる事がある。
以下の例を見てください。
fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
TODO("文字列から任意の要素へのマップを読み出してください")
}
// `Int`のマップをこのファイルに保存してある
val intsFile = File("ints.dictionary")
// Warning: チェック無しキャスト!: `Map<String, *>` から `Map<String, Int>` へ
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>
最後の行ではワーニングが出ています。
コンパイラは実行時にMapの型引数のInt
の部分をちゃんとチェックする事は出来ず、
型引数がInt
である保証を与える事は出来ません。
チェック無しキャストを避ける為に、
プログラムの構造を再設計する事が出来ます。
上の例では、DictionaryReader<T>
と DictionaryWriter<T>
インターフェースに個々の型ごとに型安全な実装を提供する事で、
チェック無しキャストを呼び出し元から単なる実装の詳細へと移動できる、リーズナブルな抽象を導入出来ます。
適切なジェネリクスの分散(variance)の利用もまたこの助けとなります。
ジェネリック関数の場合、reified型パラメータを使えば、
arg as T
のようなキャストをチェック有りに出来ます。
けれどこの場合もarg
の型が型除去(type erased)されてしまう型引数を持っている場合はやはりチェック無しキャストになってしまう事には注意しましょう。
チェック無しキャストのワーニングはアノテーションで抑制する事が出来ます。
ワーニングの出ている式や宣言に@Suppress("UNCHECKED_CAST")
をつけると抑制出来ます:
inline fun <reified T> List<*>.asListOfType(): List<T>? =
if (all { it is T })
@Suppress("UNCHECKED_CAST")
this as List<T> else
null
JVMでは: 配列型 (
Array<Foo>
) はその要素に関する型除去(type erased)された情報を保持していて、 配列型へのキャストは部分的にはチェックされます: nullablityや要素がさらに型引数を持っている場合のその型引数などはやはり除去されます。 例えば、キャストfoo as Array<List<String>?>
は、foo
がList<*>
を保持する配列なら成功します。 nullableかどうかも関係ありません。
型引数に対するアンダースコア演算子
アンダースコア演算子 _
を型引数に使う事が出来ます。
他の明示的に指定した型引数から推論出来る場合に使います:
abstract class SomeClass<T> {
abstract fun execute() : T
}
class SomeImplementation : SomeClass<String>() {
override fun execute(): String = "Test"
}
class OtherImplementation : SomeClass<Int>() {
override fun execute(): Int = 42
}
object Runner {
inline fun <reified S: SomeClass<T>, T> run() : T {
return S::class.java.getDeclaredConstructor().newInstance().execute()
}
}
fun main() {
// TはStringと推論される。なぜならSomeImplementationはSomeClass<String>を継承しているから
val s = Runner.run<SomeImplementation, _>()
assert(s == "Test")
// TはIntと推論される。なぜならOtherImplementationはSomeClass<Int>を継承しているから
val n = Runner.run<OtherImplementation, _>()
assert(n == 42)
}