Skip to content

ジェネリクス: in, out, where

KotlinのクラスはJavaと同様に型パラメータを持つことができます。

kotlin
class Box<T>(t: T) {
    var value = t
}

このようなクラスのインスタンスを作成するには、型引数を提供するだけです。

kotlin
val box: Box<Int> = Box<Int>(1)

しかし、パラメータがコンストラクタの引数などから推論できる場合は、型引数を省略できます。

kotlin
val box = Box(1) // 1はInt型なので、コンパイラはそれがBox<Int>であると判断します

バリアンス

Javaの型システムで最も扱いにくい側面の1つが、ワイルドカード型です(Java Generics FAQを参照)。 Kotlinにはこれらがありません。代わりに、Kotlinには宣言箇所でのバリアンス (declaration-site variance) と型プロジェクション (type projections) があります。

Javaにおけるバリアンスとワイルドカード

なぜJavaにこれらの謎めいたワイルドカードが必要なのか考えてみましょう。まず、Javaのジェネリック型は不変 (invariant) です。 これは、List<String>List<Object>のサブタイプではないことを意味します。もしListが不変でなければ、Javaの配列と大差なく、 次のコードがコンパイルはされるものの、実行時に例外を引き起こすことになります。

java
// Java
List<String> strs = new ArrayList<String>();

// Javaはここでコンパイル時に型不一致を報告します。
List<Object> objs = strs;

// もし、これが許されたらどうなるでしょうか?
// StringのリストにIntegerを入れられることになります。
objs.add(1);

// そして実行時に、Javaは
// ClassCastException: Integer cannot be cast to String をスローします。
String s = strs.get(0);

Javaは実行時の安全性を保証するためにこのようなことを禁止しています。しかし、これには影響があります。例えば、CollectionインターフェースのaddAll()メソッドを考えてみましょう。 このメソッドのシグネチャは何でしょうか?直感的には、次のように書きたくなります。

java
// Java
interface Collection<E> ... {
    void addAll(Collection<E> items);
}

しかし、それでは次のこと(完全に安全な操作)を実行できなくなります。

java
// Java

// addAllの素朴な宣言では、以下はコンパイルできません:
// Collection<String> は Collection<Object> のサブタイプではないため
void copyAll(Collection<Object> to, Collection<String> from) {
    to.addAll(from);
}

そのため、addAll()の実際のシグネチャは次のようになっています。

java
// Java
interface Collection<E> ... {
    void addAll(Collection<? extends E> items);
}

ワイルドカード型引数? extends Eは、このメソッドがEのオブジェクトまたはEのサブタイプのコレクションを受け入れることを示します。 これは、itemsからEを安全に読み出すことができるが(このコレクションの要素はEのサブクラスのインスタンスであるため)、 それがEのどの未知のサブタイプに従うかがわからないため、それに書き込むことはできない、ということを意味します。 この制限と引き換えに、望ましい動作が得られます: Collection<String>Collection<? extends Object>のサブタイプです。 言い換えれば、extends境界(上限境界)を持つワイルドカードは、型を共変 (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は、彼の著書「Effective Java, 3rd Edition」でこの問題をうまく説明しています (項目31: 「APIの柔軟性を高めるために、境界付きワイルドカードを使用する」)。彼は、読み出すだけのオブジェクトを「プロデューサー」、書き込むだけのオブジェクトを「コンシューマー」と呼んでいます。彼は次のように推奨しています。

NOTE

「最大限の柔軟性を得るには、プロデューサーまたはコンシューマーを表す入力パラメータにワイルドカード型を使用してください。」

そして、彼は次のニーモニックを提案しています: PECSProducer-Extends, Consumer-Super の略です。

プロデューサーオブジェクト、例えばList<? extends Foo>を使用する場合、このオブジェクトに対してadd()set()を呼び出すことはできませんが、

これはそれが不変 (immutable) であることを意味するものではありません。例えば、clear()を呼び出してリストからすべてのアイテムを削除することを妨げるものは何もありません。

clear()はパラメータを一切取らないためです。

ワイルドカード(または他の型のバリアンス)によって保証される唯一のことは型安全性 (type safety) です。不変性は全く別の話です。

宣言箇所でのバリアンス

ジェネリックインターフェースSource<T>があり、Tをパラメータとして取るメソッドは持たず、Tを返すメソッドのみを持つとします。

java
// Java
interface Source<T> {
    T nextT();
}

この場合、Source<String>のインスタンスへの参照をSource<Object>型の変数に格納することは完全に安全です。 呼び出すべきコンシューマーメソッドが存在しないためです。しかし、Javaはこれを認識せず、依然として禁止しています。

java
// Java
void demo(Source<String> strs) {
    Source<Object> objects = strs; // !!! Javaでは許可されていません
    // ...
}

これを解決するには、Source<? extends Object>型のオブジェクトを宣言する必要があります。 これを行っても意味がありません。なぜなら、以前と同じメソッドをその変数に対してすべて呼び出すことができるため、より複雑な型によって付加価値は何もありません。 しかし、コンパイラはそれを知りません。

Kotlinには、この種のことをコンパイラに説明する方法があります。これは宣言箇所でのバリアンス (declaration-site variance) と呼ばれます。 Source型パラメータTにアノテーションを付けて、Source<T>のメンバーからT返される(生成される)だけであり、決して消費されないことを保証できます。 これを行うには、out修飾子を使用します。

kotlin
interface Source<out T> {
    fun nextT(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs // これはOKです。Tがoutパラメータであるため
    // ...
}

一般的なルールは次のとおりです: クラスCの型パラメータToutと宣言されている場合、それはCのメンバーの出力 (out) 位置にのみ現れることができますが、 その見返りとしてC<Base>C<Derived>のスーパータイプに安全にすることができます。

言い換えれば、クラスCはパラメータTに対して共変 (covariant) である、またはT共変型パラメータであると言うことができます。 CTプロデューサーであり、Tコンシューマーではないと考えることができます。

out修飾子はバリアンスアノテーション (variance annotation) と呼ばれ、型パラメータの宣言箇所で提供されるため、宣言箇所でのバリアンス (declaration-site variance) を提供します。 これは、型使用箇所でのワイルドカードが型を共変にするJavaの利用箇所でのバリアンス (use-site variance) とは対照的です。

outに加えて、Kotlinには補完的なバリアンスアノテーションinがあります。これは型パラメータを反変 (contravariant) にし、消費されるだけで生産されることはないことを意味します。 反変型の良い例はComparableです。

kotlin
interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    x.compareTo(1.0) // 1.0 は Double 型であり、Number のサブタイプです
    // したがって、x を Comparable<Double> 型の変数に代入できます
    val y: Comparable<Double> = x // OK!
}

inoutという言葉は自己説明的であるように見えます(C#で長らくうまく使用されてきたように)。 したがって、上記のニーモニックは実際には必要ありません。実際、より高い抽象度で言い換えることができます。

実存的変換: 消費者はin、生産者はout 😃

型プロジェクション

利用箇所でのバリアンス: 型プロジェクション

型パラメータToutとして宣言し、利用箇所でのサブタイピングの問題を回避するのは非常に簡単ですが、 中には実際にTを返すことしかできないように制限できないクラスもあります! その良い例がArrayです。

kotlin
class Array<T>(val size: Int) {
    operator fun get(index: Int): T { ... }
    operator fun set(index: Int, value: T) { ... }
}

このクラスはTに関して共変でも反変でもありません。そして、これはある程度の柔軟性の欠如を引き起こします。次の関数を考えてみましょう。

kotlin
fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

この関数は、ある配列から別の配列にアイテムをコピーすることを想定しています。実際に適用してみましょう。

kotlin
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any)
//   ^ 型は Array<Int> ですが、Array<Any> が期待されました

ここで、同じおなじみの問題に直面します: Array<T>Tにおいて不変であり、Array<Int>Array<Any>も互いのサブタイプではありません。 なぜでしょうか?これもまた、copyが予期せぬ動作をする可能性があるためです。例えば、fromStringを書き込もうとするかもしれず、 もし実際にIntの配列を渡した場合、後でClassCastExceptionがスローされます。

copy関数がfrom書き込むのを禁止するために、次のようにすることができます。

kotlin
fun copy(from: Array<out Any>, to: Array<Any>) { ... }

これは型プロジェクション (type projection) であり、fromが単純な配列ではなく、制限された(プロジェクションされた)配列であることを意味します。 この場合、型パラメータTを返すメソッドのみを呼び出すことができ、つまりget()のみを呼び出すことができます。 これが利用箇所でのバリアンス (use-site variance) への私たちのアプローチであり、JavaのArray<? extends Object>に相当しますが、少し単純です。

inを使って型をプロジェクションすることもできます。

kotlin
fun fill(dest: Array<in String>, value: String) { ... }

Array<in String>はJavaのArray<? super String>に相当します。これは、StringCharSequence、またはObjectの配列をfill()関数に渡すことができることを意味します。

スタープロジェクション

型引数について何も知らないが、安全な方法でそれを使いたい場合があります。 ここで安全な方法とは、そのジェネリック型のプロジェクションを定義することです。 それにより、そのジェネリック型のすべての具体的なインスタンス化がそのプロジェクションのサブタイプになります。

Kotlinは、このためにスタープロジェクション (star-projection) 構文を提供しています。

  • Foo<out T : TUpper>の場合、Tが上限TUpperを持つ共変型パラメータであるとき、Foo<*>Foo<out TUpper>と同じです。 これは、Tが不明な場合でも、Foo<*>からTUpperの値を安全に読み出すことができることを意味します。
  • Foo<in T>の場合、Tが反変型パラメータであるとき、Foo<*>Foo<in Nothing>と同じです。 これは、Tが不明な場合、Foo<*>に安全に書き込むことは何もできないことを意味します。
  • Foo<T : TUpper>の場合、Tが上限TUpperを持つ不変型パラメータであるとき、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?>を意味します。

NOTE

スタープロジェクションはJavaの生の型 (raw types) と非常によく似ていますが、より安全です。

ジェネリック関数

クラスだけでなく、関数も型パラメータを持つことができます。型パラメータは関数の名前のに置かれます。

kotlin
fun <T> singletonList(item: T): List<T> {
    // ...
}

fun <T> T.basicToString(): String { // 拡張関数
    // ...
}

ジェネリック関数を呼び出すには、呼び出し元で関数の名前のに型引数を指定します。

kotlin
val l = singletonList<Int>(1)

型引数はコンテキストから推論できる場合は省略できるため、次の例も機能します。

kotlin
val l = singletonList(1)

ジェネリック制約

特定の型パラメータに代入できるすべての可能な型のセットは、ジェネリック制約 (generic constraints) によって制限される場合があります。

上限境界

最も一般的な制約のタイプは上限境界 (upper bound) で、Javaのextendsキーワードに相当します。

kotlin
fun <T : Comparable<T>> sort(list: List<T>) {  ... }

コロンの後に指定された型は上限境界であり、Comparable<T>のサブタイプのみがTに代入できることを示します。例:

kotlin
sort(listOf(1, 2, 3)) // OK。IntはComparable<Int>のサブタイプです
sort(listOf(HashMap<Int, String>())) // エラー: HashMap<Int, String>はComparable<HashMap<Int, String>>のサブタイプではありません

デフォルトの上限境界(指定がない場合)はAny?です。山かっこ内に指定できる上限境界は1つだけです。 同じ型パラメータに複数の上限境界が必要な場合は、個別のwhere句が必要です。

kotlin
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

渡される型は、where句のすべての条件を同時に満たす必要があります。上記の例では、T型はCharSequenceComparable両方を実装する必要があります。

確実な非NULL型

ジェネリックなJavaクラスやインターフェースとの相互運用を容易にするため、Kotlinはジェネリック型パラメータを確実な非NULL (definitely non-nullable) として宣言することをサポートしています。

ジェネリック型Tを確実な非NULLとして宣言するには、T & Anyと型を宣言します。例えば: T & Any

確実な非NULL型は、NULL許容な上限境界を持つ必要があります。

確実な非NULL型を宣言する最も一般的な使用ケースは、@NotNullを引数に含むJavaメソッドをオーバーライドしたい場合です。 例えば、load()メソッドを考えてみましょう。

java
import org.jetbrains.annotations.*;

public interface Game<T> {
    public T save(T x) {}
    @NotNull
    public T load(@NotNull T x) {}
}

Kotlinでload()メソッドを正常にオーバーライドするには、T1を確実な非NULLとして宣言する必要があります。

kotlin
interface ArcadeGame<T1> : Game<T1> {
    override fun save(x: T1): T1
    // T1 は確実な非NULL型です
    override fun load(x: T1 & Any): T1 & Any
}

Kotlinのみで作業している場合、Kotlinの型推論がこれを処理してくれるため、明示的に確実な非NULL型を宣言する必要はほとんどありません。

型消去

Kotlinがジェネリック宣言の使用に対して行う型安全性のチェックは、コンパイル時に行われます。 実行時には、ジェネリック型のインスタンスは実際の型引数に関する情報を保持しません。 型情報は消去 (erased) されると言われます。例えば、Foo<Bar>Foo<Baz?>のインスタンスは、単にFoo<*>に消去されます。

ジェネリクス型チェックとキャスト

型消去のため、実行時にジェネリック型のインスタンスが特定の型引数で作成されたかどうかを一般的にチェックする方法はありません。 そしてコンパイラはints is List<Int>list is T(型パラメータ)のようなisチェックを禁止します。 ただし、スタープロジェクションされた型に対してインスタンスをチェックすることはできます。

kotlin
if (something is List<*>) {
    something.forEach { println(it) } // アイテムは `Any?` 型として扱われます
}

同様に、インスタンスの型引数が静的に(コンパイル時に)チェックされている場合、 型の非ジェネリック部分を含むisチェックまたはキャストを行うことができます。この場合、山かっこが省略されることに注意してください。

kotlin
fun handleStrings(list: MutableList<String>) {
    if (list is ArrayList) {
        // `list` は `ArrayList<String>` にスマートキャストされます
    }
}

型引数を省略した同じ構文は、型引数を考慮しないキャスト(list as ArrayListなど)にも使用できます。

ジェネリック関数呼び出しの型引数も、コンパイル時にのみチェックされます。 関数本体内では、型パラメータを型チェックに使用することはできず、型パラメータへの型キャスト(foo as T)は未検査です。 唯一の例外は、再具体化された型パラメータを持つインライン関数です。 これらは各呼び出し箇所で実際の型引数がインライン化されます。これにより、型パラメータの型チェックとキャストが可能になります。 ただし、チェックまたはキャスト内で使用されるジェネリック型のインスタンスには、上記で説明した制限が引き続き適用されます。 例えば、型チェックarg is Tにおいて、arg自体がジェネリック型のインスタンスである場合、その型引数は依然として消去されます。

kotlin
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)

val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // コンパイルは通るが型安全性を損なう!
// さらに詳細なサンプルを展開


fun main() {
    println("stringToSomething = " + stringToSomething)
    println("stringToInt = " + stringToInt)
    println("stringToList = " + stringToList)
    println("stringToStringList = " + stringToStringList)
    //println(stringToStringList?.second?.forEach() {it.length}) // リストのアイテムがStringではないため、ClassCastExceptionをスローします
}

未検査キャスト

foo as List<String>のような、具体的な型引数を持つジェネリック型への型キャストは、実行時にチェックできません。 これらの未検査キャスト (unchecked casts) は、型安全性が高レベルのプログラムロジックによって暗示されているが、コンパイラによって直接推論できない場合に使用できます。以下の例を参照してください。

kotlin
fun readDictionary(file: File): Map<String, *> = file.inputStream().use { 
    TODO("Read a mapping of strings to arbitrary elements.")
}

// このファイルにInt型のマップを保存しました
val intsFile = File("ints.dictionary")

// 警告: 未検査キャスト: `Map<String, *>` から `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

最後の行のキャストに対して警告が表示されます。コンパイラは実行時に完全にチェックできず、マップ内の値がIntであるという保証は提供しません。

未検査キャストを避けるためには、プログラムの構造を再設計することができます。上記の例では、DictionaryReader<T>およびDictionaryWriter<T>インターフェースを、異なる型に対して型安全な実装で使用することができます。 未検査キャストを呼び出し元から実装の詳細に移動するために、適切な抽象化を導入できます。 ジェネリックバリアンスを適切に使用することも役立ちます。

ジェネリック関数では、再具体化された型パラメータを使用すると、arg as Tのようなキャストがチェックされますが、argの型が独自の型引数を持っており、それが消去される場合は例外です。

未検査キャストの警告は、警告が発生する文または宣言に@Suppress("UNCHECKED_CAST")アノテーションで付加することで抑制できます。

kotlin
inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List<T> else
        null

NOTE

JVM上で: 配列型Array<Foo>)は要素の消去された型に関する情報を保持しており、

配列型への型キャストは部分的にチェックされます。要素型のnull可能性と実際の型引数は依然として消去されます。

例えば、キャストfoo as Array<List<String>?>は、fooが任意のList<*>を保持する配列である場合、それがnull許容であるかどうかにかかわらず成功します。

型引数のアンダースコア演算子

アンダースコア演算子_は型引数に使用できます。これは、他の型が明示的に指定されている場合に、引数の型を自動的に推論するために使用します。

kotlin
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() {
    // SomeImplementationがSomeClass<String>から派生しているため、TはStringと推論されます
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")

    // OtherImplementationがSomeClass<Int>から派生しているため、TはIntと推論されます
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}