Initialization order of companion object and initializers in an enum

I was trying to write an enum, where in the enum initializer some values are stored in a map in a companion object.
However, the in the initializer function, the map is still NULL, thus I get a NPE while the enum instances are created.

Sample code is:

enum class Test(vararg mappedValues : String) {
    T1("val1.1", "val1.2"),
    T2("val2")
    ;

    companion object {
        private val valueMapping = mutableMapOf<String,Test>()

        fun fromMappedValue(mappedValue : String) : Test {
            return valueMapping[mappedValue] ?: throw IllegalArgumentException("Illegal mapped value: $mappedValue")
        }
    }

    init {
        mappedValues.forEach { valueMapping[it] = this }
    }
}

fun main(args: Array<String>) {
    val v2 = "val2"
    println("'$v2' is mapped to ${Test.fromMappedValue(v2)}")
}

And the exception which I get is:

Exception in thread "main" java.lang.ExceptionInInitializerError
	at EnumInitOptionKt.main(enumInitOption.kt:21)
Caused by: java.lang.NullPointerException
	at Test$Companion.access$getValueMapping$p(enumInitOption.kt:6)
	at Test.<init>(enumInitOption.kt:15)
	at Test.<clinit>(enumInitOption.kt:2)
	... 1 more

Process finished with exit code 1

To me this exception is not logical, and I don’t see how to get rid of it, other than by moving the private valueMapping field out of the enum definition (but I’d rather keep it together).
So this looks like a bug to me?

Since it’s an enum, I cannot move the companion object to above the first constructor call.

Kotlin version: 1.1.1

How about making valueMapping lazily initialized?

private val valueMapping: MutableMap <String, Test> by lazy {mutableMapOf<String,Test>()}

Yeah that might work, it does still sound like a workaround though.

I ran into the same issue, with Java code converted by Idea.

enum TestEnum {
    SOME_VALUE("world");

    String value;

    private static final USEFUL_CONSTANT = "Hello ";

   TestEnum(String value) {
       this.value = USEFUL_CONSTANT + value; // NPE in translated code
   }
}

If you need a simple primitive constant you can use const:

enum class TestEnum {
    SOME_VALUE("world");

    val value: String

    companion object {
        const val USEFUL_CONSTANT = "Hello "
    }

    constructor(value: String) {
        this.value = USEFUL_CONSTANT + value
    }
}
1 Like

Did we find a solution to this problem? by lazy did not work either. Still the same error.

I found a workaround, probably I moved the map outside the enum, and I moved on. There’s no direct solution as far as I know but since I don’t have access to the source code for that anymore I can’t easily try it out.

I did the same (i.e. moved out my object outside the enum) and moved on. Although this works but I would like to understand more why it does not work when defined inside the enum.

The cause is the following
“it is illegal to access static member from enum constructor”
See also : java - Why can't enum's constructor access static fields? - Stack Overflow

The lazy approach is the best:

enum class Test(val value : String) {
    T1("val1.1"),
    T2("val2")
    ;

    companion object {
        private val valueMapping by lazy   values().associateBy { it.value }

        fun fromMappedValue(mappedValue : String) : Test {
            return valueMapping[mappedValue] ?: throw IllegalArgumentException("Illegal mapped value: $mappedValue")
        }
    }
}

fun main(args: Array<String>) {
    val v2 = "val2"
    println("'$v2' is mapped to ${Test.fromMappedValue(v2)}")
}

For multiple value constructor it will be a little more complex:

enum class Test(vararg  val values : String) {
    T1("val1.1", "val1.2"),
    T2("val2")
    ;

    companion object {
        private val valueMapping by lazy {
            values().flatMap { entry ->
               entry.values.map { value -> value to entry }
            }.toMap()
        }

        fun fromMappedValue(mappedValue : String) : Test {
            return valueMapping[mappedValue] ?: throw IllegalArgumentException("Illegal mapped value: $mappedValue")
        }
    }
}

fun main(args: Array<String>) {
    val v2 = "val1.2"
    println("'$v2' is mapped to ${Test.fromMappedValue(v2)}")
}