0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】EnumMap を使いやすくする

Last updated at Posted at 2024-10-20

課題

Kotlin でコレクションなどを Map 型に変換することはよくあるでしょう。
そのときにキーが enum 型であることもよくあるでしょう。

コレクションを Map に変換する
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 クラスを使用するとパフォーマンスがよくなります。

コレクションを 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 を生成する関数
/**
 * 空の新しい [EnumMap] を返す。
 */
inline fun <reified K : Enum<K>, V> enumMapOf(): EnumMap<K, V> =
    EnumMap(K::class.java)
EnumMap を生成する関数を使用する
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> となってほしいのですが、そうなっていません。

再掲:EnumMap を生成する関数を使用する
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 で定義された型なので、型パラメータがプラットフォーム型にはなることはありません。

実装が EnumMap である MutableMap を生成する関数
/**
 * [Enum] をキーとする、空の新しい [MutableMap] を返す。
 */
inline fun <reified K : Enum<K>, V> mutableMapWithEnumKeyOf(): MutableMap<K, V> =
    EnumMap(K::class.java)
実装が EnumMap である MutableMap を生成する関数を使用する
val map = // val map: MutableMap<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
            myEnum.toString()
        }

これで、型を明示することなく、プラットフォーム型でない型として推論されるようになりました。

変換後の型がミュータブルである

コレクションを別のコレクションに変換する関数で変換後のオブジェクトを指定できる関数(〜To 関数)一般に言えることですが、
変換後の型は、引数で指定したオブジェクトの型そのままとなるので、ミュータブルになります。

このままだと意図せず変更してしまうリスクがあるので、
できるだけ早い段階でリードオンリーの型にしておきたいです。
かといって型を明示するのは面倒です。

そこでミュータブルである MutableMap 型をリードオンリーである Map 型に変換する関数を作って使うようにしましょう。

MutableMap 型を Map 型に変換する関数
/**
 * [MutableMap] を [Map] として返す。
 */
fun <K, V> MutableMap<K, V>.asMap(): Map<K, V> = this
MutableMap 型を Map 型に変換する関数を使用する
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 を生成する関数
/**
 * 第一構成要素がキーであり第二構成要素が値であるペアのリストにより内容が指定された、新しい [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())

まとめ

次のような関数を作ることで、

Maps.kt
/**
 * 空の新しい [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 を使っていない次のようなコードを、

コレクションを Map に変換する(EnumMap 不使用)
val map = // val map: Map<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWith { myEnum ->
            myEnum.toString()
        }

次のように簡潔に、EnumMap を使ったパフォーマンスがよいコードに書き換えられます。

コレクションを Map に変換する(EnumMap 使用)
val map = // val map: Map<MyEnum, String>
    listOf(MyEnum.A, MyEnum.B)
        .associateWithTo(mutableMapWithEnumKeyOf()) { myEnum ->
            myEnum.toString()
        }
        .asMap()

/以上

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?