Kotlin の enum
クラスは、ほぼ Java の enum
と同じように扱うことができます。
次の Kotlin コードは FOO
と BAR
という Hoge
型の定数を定義しています。
enum class Hoge { FOO, BAR }
enum
クラスで定義した定数は String
への変換 (name
メソッド) と、 String
からの変換 (valueOf
メソッド) をサポートしているので、簡単にシリアライズ・デシリアライズができます。またenum
クラスに振る舞いを記述できるので、フィールドをもたせたり、メソッドを呼び出したりするコードも動きます。
enum class Hoge(val flag: Boolean) { FOO(true), BAR(false); fun getSomething(): String { return "Hello, world!" } }
メソッドは abstract で定義したり、インタフェースを実装したりもできます。この場合、個々の定数の定義で具体的な処理を記述します。
enum class Hoge { FOO { override fun getSomething(): String { return "Hello, world!" } }, BAR { override fun getSomething(): String { return "Hi, world!" } }; abstract fun getSomething(): String }
ここまでがおさらいです。
ここで次のように String?
から任意の型のオブジェクトへ変換する Generic なメソッドを考えます。ストレージに書き込んだ文字列から期待する型への変換を行うようなメソッドです。
@Suppress("UNCHECKED_CAST") fun <T : Any> String?.convert(fallback: T): T { return when (fallback) { is String -> { this ?: fallback } is Int -> { this?.let { Integer.valueOf(this) } ?: 0 } is Enum<*> -> { this?.let { val method = fallback.javaClass.getDeclaredMethod("valueOf", String::class.java) method.invoke(null, this) } ?: fallback } else -> { throw IllegalArgumentException("") } } as T }
このメソッドを、次のように標準入力で与えられた文字列からデシリアライズする目的で使ってみましょう。標準入力で FOO と入力すると、正しく定数に変換されます。
fun main(args: Array<String>) { println(readLine().convert<Hoge>(Hoge.FOO)) } enum class Hoge { FOO, BAR }
では次の場合はどうなるでしょうか。Hoge
には abstract メソッドが定義されています。
fun main(args: Array<String>) { println(readLine().convert<Hoge>(Hoge.FOO)) } enum class Hoge { FOO { override fun getSomething(): String { return "Hello, world!" } }, BAR { override fun getSomething(): String { return "Hi, world!" } }; abstract fun getSomething(): String }
上記のコードの場合、FOO を標準入力に入れると次のようなエラーで異常終了します。
Exception in thread "main" java.lang.NoSuchMethodException: dev.keithyokoma.Hoge$FOO.valueOf(java.lang.String) at java.lang.Class.getDeclaredMethod(Class.java:2130) at dev.keithyokoma.MainKt.convert(Main.kt:22) at dev.keithyokoma.MainKt.main(Main.kt:6)
この挙動の差は生成されるバイトコードの違いを見ると分かります。
メソッドの定義がなかったり、実装を enum
クラスの宣言に書く場合は次のようなバイトコードが生成されます。
public final enum dev/keithyokoma/Hoge extends java/lang/Enum { // access flags 0x4019 public final static enum Ldev/keithyokoma/Hoge; FOO // access flags 0x4019 public final static enum Ldev/keithyokoma/Hoge; BAR }
一方で、abstract メソッドやインタフェースのメソッドの実装を個別の定数で行う場合は次ようなバイトコードが生成されます。
public abstract enum dev/keithyokoma/Hoge extends java/lang/Enum { // access flags 0x18 final static INNERCLASS dev/keithyokoma/Hoge$FOO dev/keithyokoma/Hoge FOO // access flags 0x18 final static INNERCLASS dev/keithyokoma/Hoge$BAR dev/keithyokoma/Hoge BAR // access flags 0x4019 public final static enum Ldev/keithyokoma/Hoge; FOO // access flags 0x4019 public final static enum Ldev/keithyokoma/Hoge; BAR }
なにやら増えていますね。内部クラスとして Hoge$FOO
と Hoge$BAR
があるように見えます。そして実行時のエラーでは、Hoge$FOO
に対して valueOf
メソッドを探したが見つからなかった、というメッセージが出ています。
enum
クラス内の個別の定数で実装を定義する場合 Hoge.FOO
は Hoge$FOO
型で、Hoge.BAR
は Hoge$BAR
型として解釈されます。これらはすべて単なるクラスなので、Generic な型変換コードの val method = fallback.javaClass.getDeclaredMethod("valueOf", String::class.java)
は valueOf
メソッドを見つけられません (Hoge$FOO
型の Hoge.FOO
が格納された fallback
変数に対し valueOf
を探そうとするが、valueOf
メソッドは Hoge
のクラスメソッドとして定義されるので見つからない)。
Hoge$FOO
型と Hoge$BAR
型はどちらも Hoge
型のサブクラスになっているので、次のように修正すると期待通りの動作をします。
fallback
の型が enum
かどうかを判別し、enum
でなければその親クラスから valueOf
メソッドを探します。
@Suppress("UNCHECKED_CAST") fun <T : Any> String?.convert(fallback: T): T { return when (fallback) { // ... is Enum<*> -> { this?.let { val clazz = fallback.javaClass val method = if (clazz.isEnum) { clazz.getDeclaredMethod("valueOf", String::class.java) } else { clazz.superclass.getDeclaredMethod("valueOf", String::class.java) } method.invoke(null, this) } ?: fallback } // ... } as T }
コンパイラの吐き出すバイトコードまでを考慮しないといけないコードになってしまいました (enum
クラスは文法上継承できないはずなので、isEnum
が false
のときに superclass
を参照しにいくのは一見とても奇妙)。
ちなみに、Java で同様のコードを書いて検証してみたところ、Java でも同じ挙動をしました。
もし、型パラメータの T
から直接 Class
クラスを参照できれば、このような回りくどい記述は必要ありません。
次のコードでは正しく valueOf
メソッドが探せます。
inline fun <reified T : Any> String?.convert(fallback: T): T { return when (fallback) { // ... is Enum<*> -> { this?.let { val method = T::class.java.getDeclaredMethod("valueOf", String::class.java) method.invoke(null, this) } ?: fallback } // ... } as T }
2019/09/03 16:00 追記
型パラメータ T
が Enum<T>
に限定できる場合は、リフレクションは不要になり、代わりに enumValueOf
を使います (thank you @red_fat_daruma !)。
inline fun <reified T : Enum<T>> String?.convert(fallback: T): T { return this?.let { value -> enumValueOf<T>(value) } ?: fallback }