今更聞けないKotlin Generics入門

公開:2020/09/24
更新:2020/09/24
11 min読了の目安(約6700字TECH技術記事

対象

なんとなくGenericsについて触れないまま来てしまっている方向け(そう。つまり私)

Genericsとは

総称型とも呼ばれ、型をパラメータとすることを可能としたものになります。
一般的に<T>のように<>の部分がGenericsとなります。
例としてある型の変数を保存するBoxクラスを見てみましょう。この時、Boxクラスは以下のような実装になります。

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

これはインスタンス生成時に保存する型を定義できるよ、という記述になります。例えばInt型の箱としたい場合は以下のような実装します。

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

また、String型の箱としたい場合は以下のような実装です。

val box: Box<String> = Box<String>("test")

このGenericsの変数名はEやKなどいろいろ見かけるかと思いますが、変数名によってGenericsの意味が変わったりはしません。ただ、一般的に以下のような使い分けがされています。

  • T ・・・ Type(タイプ。型をパラメータとするときに使われる)
  • E ・・・ Element(要素。Listなどで使われる)
  • K ・・・ Key(キー。Mapなどで使われる)
  • V ・・・ Value(値。Mapなどで使われる)
  • R ・・・ Result(戻り値)

なぜGenericsが必要なのか(Genericsがないとこうなる)

どんな型でも格納できる、という意味であればAny(JavaならObject)とすれば大丈夫でした。
ただこうした場合どんな物でも格納できてしまうため、予測できない型のデータが含まれてしまう可能性があります。これは実行時にclassCastExceptionが判明してしまう原因になりかねず、またそれを防ぐために本質的でないコードを記述することが増えてしまいます。

class Box(a: Any) {
   var value = a
}
	
val box = Box(1111)
val box2 = Box("test")
arrayListOf<Box>(box,box2) //数値と思い込むとあとでclassCastExceptionになる

これは型安全とは呼べません。そのため、このBoxはIntしか入らないということが明示的に宣言されているという状態を目指すためにGenericsを利用します。

class Box<T>(t: T) {
  var value = t
}
	
val box = Box(1111)
val box2 = Box("test")
arrayListOf<Box<Int>>(box,box2) //box2がType miss matchでコンパイルエラー

この機能の使い所

この機能を利用することでより抽象化された実装が可能となります。いくつか利用例を見てみましょう。

Collectionなどのコンテナ

データの操作方法は提供するが、そのデータの中身自体は統一さえされていればなんでも良いよ、という場合です。例えばKotlinのCollection型は以下のように定義されています。

public interface List<out E> : Collection<E> 

指定クラスでの戻り値指定

DDDの文脈で、Application層ではアプリケーションロジックのみ記載しステートレスにしたいという場合に、Streamをsubscribeされてしまうとdisposeされているかどうかのステートを持つことになってしまいます。そこで、このようなabstract classを用意して、戻り値を制限します。

abstract class FlowUseCase<in P, out R> {
    abstract fun execute(parameters: P): Flow<R>
}
class FlowUseCaseImpl: FlowUseCase<Command, Result>(){
    override fun execute(command: Command): Flow<Result> {
        return flow {
            emit(Result.Failed)
        }
    }

    data class Command(
        val value: String
    )

    sealed class Result {
        object Failed : Result()
	object Success : Result()
    }
}

kotlinのGenerics

KotlinのGenericsはJavaのGenericsといくつか異なる点があります。
Kotlin Reference[1]を例に取りながら解説していきます。

宣言箇所の分散

Javaで以下の処理を考えてみます。

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // type mismatch

一見すると通りそうですが、Type mismatchが発生してしまいます。これはList<String>はList<Object>のサブタイプではないということを示しています。なぜこのようになっているのかというと、以下のような処理を考慮する必要があるためです。

objs.add(1); // Integer を Strings(Object) のリストへ入れる
String s = strs.get(0); // !!! ClassCastException: Integer を String へキャストできない

List<String>をList<Object>のサブタイプとしてしまうと、一度objectクラスに抽象化してからIntegerを追加することが可能となってしまいます。これでは実行時の安全性が保証されません。そのためJavaではこれらが禁止されています。

では以下のようなクラスではどうでしょうか。

    abstract class Box<T> {
        abstract fun get(): T
    }

    fun test(box: Box<String>) {
        val anyBox : Box<Any> = box // type mismatch
    }

これらはBoxに新しく追加するメソッドがないため、Box<Any>に代入しても型的に安全です。しかし、コンパイラはそれを知りません。

Kotlinではコンパイラにこれを教えてあげることができます。(宣言箇所分散)
この型Tは値を返す時のみ使われることをout 修飾子を用いて表します。

    abstract class Box<out T> {
        abstract fun get(): T
    }

    fun test(box: Box<String>) {
        val anyBox : Box<Any> = box // OK!
    }

また、in 修飾子もあります。これは値を返すときには利用されず、パラメータとして利用されることを示します。例えば以下のような処理を考えます。

    abstract class Comparable<T> {
        abstract fun compareTo(other: T): Int
    }

    fun demo(x: Comparable<Number>) {
        x.compareTo(1.0)
        val y: Comparable<Double> = x // type mismatch
    }

この時、値返却としてTが利用されないため型安全です。これをコンパイラに示すためにin 修飾子を利用します。

    abstract class Comparable<in T> {
        abstract fun compareTo(other: T): Int
    }

    fun demo(x: Comparable<Number>) {
        x.compareTo(1.0)
        val y: Comparable<Double> = x // OK!
    }

型投影

outパラメータを利用することは非常に便利ですが、利用できないパターンもあります。例えばArrayListです。こちらはadd / getのメソッドがあるため、Tを値を返すのみに限定できません。

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

こうなってしまうと、例えばcopyメソッドを利用するときにエラーが発生してしまいます。

@SuppressLint("Assert")
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>) が期待されている

ただこのcopyは新しい違った型の値を追加するわけではないので、型的に安全です。しかしコンパイラはそれを判別できません。copy内で書き込みが行われる可能性があるからです。

この時、formは投影されるだけであり、何らか書き込みが行われないことを示す方法としてout 演算子を利用することができます。これを型投影といいます。

@SuppressLint("Assert")
fun copy(from: Array<out Any>, to: Array<Any>) {
  assert(from.size == to.size)
  for (i in from.indices)
    to[i] = from[i]
	
  from.set(1, "test") // エラー。書き込みはできない
}

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3){}
copy(ints, any) // OK

これも同様にin 修飾子を利用して型投影を行えます。例えば先ほどのComparatorをパラメータに渡したいときは以下のように記載します。

public fun <T> Array<out T>.sortWith(comparator: Comparator<in T>, fromIndex: Int = 0, toIndex: Int = size): Unit {
    java.util.Arrays.sort(this, fromIndex, toIndex, comparator)
}

スタープロジェクション

型に関して特に考慮しなくても良い場面があります。例えばLog配列に全ての値を投入して出力する場合です。これらはout 修飾子を利用することで以下のようにかけます。

    fun printLog(array: Array<out Any?>) {
        array.forEach { Timber.d(it.toString()) }
    }

これらは以下のように省略することができます。 (star-projections)

    fun printLog(array: Array<*>) {
        array.forEach { Timber.d(it.toString()) }
    }

Generics関数

関数にもGenericsを利用することができます。拡張関数も同様です。
Kotlin referenceの例を用いると以下のようになります。

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

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

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

val l = singletonList<Int>(1)

Genericsの制約

このTに対して、なんでもではなく、ある一定の制約を設けたい場合があります。例えばsortをするので比較可能な物だけ扱いたいという場合です。この時は以下のように記載します。

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

このように記載するとComparableを継承しているクラスのみTとして扱えます。
この時IntはComparableを継承していますが、HashMapは継承してないのでエラーとなります。

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> cloneWhenGreater(list: List<T>, threshold: T): List<T>
    where T : Comparable,
          T : Cloneable {
  return list.filter { it > threshold }.map { it.clone() }
}

まとめ

汎用的なクラスや、実装上での制約をかけたい時に活用できる。
が、そんな機会はプロジェクト開始して一度か二度くらいしかないので忘れていく運命にあるんだ。。。

参考文献

[1] ジェネリクス - kotlin reference
https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/generics.html