課題
Kotlin でコレクションなどを Map
型に変換することはよくあるでしょう。
そのときにキーが enum 型であることもよくあるでしょう。
enum class MyEnum { A, B }
val map = // val map: Map<MyEnum, String>
listOf(MyEnum.A, MyEnum.B)
.associateWith { myEnum ->
myEnum.toString()
}
キーが enum 型の場合、Map
の 実装として Java 標準ライブラリーの EnumMap
クラスを使用するとパフォーマンスがよくなります。
val map = // val map: EnumMap<MyEnum!, String!>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(
EnumMap(MyEnum::class.java)
) { myEnum ->
myEnum.toString()
}
このとき問題が3つあります。
-
EnumMap
クラスのコンストラクター引数にキーの型のClass
インスタンスを渡す必要がある - 変換後の型の実型パラメーターがプラットフォーム型になる
- 変換後の型がミュータブルである
この記事ではこれらの問題を解決します。
EnumMap
クラスのコンストラクター引数にキーの型の Class
インスタンスを渡す必要がある
EnumMap
クラスのコンストラクター引数にキーの型の Class
インスタンス(今回の例では MyEnum::class.java
)を渡す必要があるのが面倒です。
インライン関数の reified
を使用すれば実型パラメーターの Class
インスタンスを取得することができます。
これを用いて次のような関数を作って使うようにしましょう。
/**
* 空の新しい [EnumMap] を返す。
*/
inline fun <reified K : Enum<K>, V> enumMapOf(): EnumMap<K, V> =
EnumMap(K::class.java)
val map = // val map: EnumMap<MyEnum!, String!>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(enumMapOf()) { myEnum ->
myEnum.toString()
}
これでキーの型を明示することなく EnumMap
インスタンスを生成できるようになりました。
変換後の型の実型パラメーターがプラットフォーム型になる
プラットフォーム型は、nullable か non-null かが分からない型です。
そして、分からないにもかかわらず、null を受け付けてしまいます(つまり null 安全ではありません)。
nullable は MyEnum
、non-null は MyEnum?
のように表されるのに対して、
プラットフォーム型は MyEnum!
のように表されます。
先ほどの例では変換後の型が、キーも値もプラットフォーム型である EnumMap<MyEnum!, String!>
と推論されています。
この例ではキーにも値にも null が入ることはないので、いずれもが non-null である EnumMap<MyEnum, String>
となってほしいのですが、そうなっていません。
val map = // val map: EnumMap<MyEnum!, String!>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(enumMapOf()) { myEnum ->
myEnum.toString()
}
プラットフォーム型は、Kotlin コード上で型を明示してやることで nullable 型もしくは non-null 型にすることができます。
例えば次のようにします。
val map = // val map: EnumMap<MyEnum, String>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(enumMapOf<MyEnum, String>()) { myEnum ->
myEnum.toString()
}
しかしこれはコードを書くのが面倒です。
EnumMap
クラスは MutableMap
インターフェイスを実装しています。
ほとんどの場合、必要なのは MutableMap
型のオブジェクトであって、EnumMap
型である必要はありません。
そこで、実装が EnumMap
である MutableMap
オブジェクトを生成する関数を作ってそれを使うようにしましょう。
MutableMap
は Kotlin で定義された型なので、型パラメータがプラットフォーム型にはなることはありません。
/**
* [Enum] をキーとする、空の新しい [MutableMap] を返す。
*/
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(): MutableMap<K, V> =
EnumMap(K::class.java)
val map = // val map: MutableMap<MyEnum, String>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
myEnum.toString()
}
これで、型を明示することなく、プラットフォーム型でない型として推論されるようになりました。
変換後の型がミュータブルである
コレクションを別のコレクションに変換する関数で変換後のオブジェクトを指定できる関数(〜To 関数)一般に言えることですが、
変換後の型は、引数で指定したオブジェクトの型そのままとなるので、ミュータブルになります。
このままだと意図せず変更してしまうリスクがあるので、
できるだけ早い段階でリードオンリーの型にしておきたいです。
かといって型を明示するのは面倒です。
そこでミュータブルである MutableMap
型をリードオンリーである Map
型に変換する関数を作って使うようにしましょう。
/**
* [MutableMap] を [Map] として返す。
*/
fun <K, V> MutableMap<K, V>.asMap(): Map<K, V> = this
val map = // val map: Map<MyEnum, String>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
myEnum.toString()
}
.asMap()
これで、早い段階で簡潔にリードオンリーの型にできました。
付録: MutableMap
を生成する関数のオーバーロード
Kotlin 標準ライブラリーには MutableMap
を生成する関数として mutableMapOf
が用意されています。
この関数には、引数なしのものだけでなく、引数が vararg pairs: Pair<K, V>
のものもあります。
今回作った関数にも、それを用意しておきましょう。
/**
* 第一構成要素がキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [EnumMap] を返す。
*
* 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
*/
inline fun <reified K : Enum<K>, V> enumMapOf(vararg pairs: Pair<K, V>): EnumMap<K, V> =
pairs.toMap(enumMapOf())
/**
* 第一構成要素が [Enum] 型のキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [MutableMap] を返す。
*
* 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
*/
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(vararg pairs: Pair<K, V>): MutableMap<K, V> =
pairs.toMap(mutableMapWithEnumKeyOf())
まとめ
次のような関数を作ることで、
/**
* 空の新しい [EnumMap] を返す。
*/
inline fun <reified K : Enum<K>, V> enumMapOf(): EnumMap<K, V> =
EnumMap(K::class.java)
/**
* 第一構成要素がキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [EnumMap] を返す。
*
* 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
*/
inline fun <reified K : Enum<K>, V> enumMapOf(vararg pairs: Pair<K, V>): EnumMap<K, V> =
pairs.toMap(enumMapOf())
/**
* [Enum] をキーとする、空の新しい [MutableMap] を返す。
*/
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(): MutableMap<K, V> =
EnumMap(K::class.java)
/**
* 第一構成要素が [Enum] 型のキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [MutableMap] を返す。
*
* 複数のペアが同じキーを持つ場合、結果として得られるマップはそれらのペアの最後の値を含む。
*/
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(vararg pairs: Pair<K, V>): MutableMap<K, V> =
pairs.toMap(mutableMapWithEnumKeyOf())
/**
* [MutableMap] を [Map] として返す。
*/
fun <K, V> MutableMap<K, V>.asMap(): Map<K, V> = this
EnumMap
を使っていない次のようなコードを、
val map = // val map: Map<MyEnum, String>
listOf(MyEnum.A, MyEnum.B)
.associateWith { myEnum ->
myEnum.toString()
}
次のように簡潔に、EnumMap
を使ったパフォーマンスがよいコードに書き換えられます。
val map = // val map: Map<MyEnum, String>
listOf(MyEnum.A, MyEnum.B)
.associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
myEnum.toString()
}
.asMap()
/以上