Effective Enums in Kotlin with reverse lookup?

57,825

Solution 1

First of all, the argument of fromInt() should be an Int, not an Int?. Trying to get a Type using null will obviously lead to null, and a caller shouldn't even try doing that. The Map has also no reason to be mutable. The code can be reduced to:

companion object {
    private val map = Type.values().associateBy(Type::value)
    fun fromInt(type: Int) = map[type]
}

That code is so short that, frankly, I'm not sure it's worth trying to find a reusable solution.

Solution 2

we can use find which Returns the first element matching the given predicate, or null if no such element was found.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}

Solution 3

It makes not much sense in this case, but here is a "logic extraction" for @JBNized's solution:

open class EnumCompanion<T, V>(private val valueMap: Map<T, V>) {
    fun fromInt(type: T) = valueMap[type]
}

enum class TT(val x: Int) {
    A(10),
    B(20),
    C(30);

    companion object : EnumCompanion<Int, TT>(TT.values().associateBy(TT::x))
}

//sorry I had to rename things for sanity

In general that's the thing about companion objects that they can be reused (unlike static members in a Java class)

Solution 4

Another option, that could be considered more "idiomatic", would be the following:

companion object {
    private val map = Type.values().associateBy(Type::value)
    operator fun get(value: Int) = map[value]
}

Which can then be used like Type[type].

Solution 5

If you have a lot of enums, this might save a few keystrokes:

inline fun <reified T : Enum<T>, V> ((T) -> V).find(value: V): T? {
    return enumValues<T>().firstOrNull { this(it) == value }
}

Use it like this:

enum class Algorithms(val string: String) {
    Sha1("SHA-1"),
    Sha256("SHA-256"),
}

fun main() = println(
    Algorithms::string.find("SHA-256")
            ?: throw IllegalArgumentException("Bad algorithm string: SHA-256")
)

This will print Sha256

Share:
57,825
Baron
Author by

Baron

Updated on March 02, 2022

Comments

  • Baron
    Baron about 2 years

    I'm trying to find the best way to do a 'reverse lookup' on an enum in Kotlin. One of my takeaways from Effective Java was that you introduce a static map inside the enum to handle the reverse lookup. Porting this over to Kotlin with a simple enum leads me to code that looks like this:

    enum class Type(val value: Int) {
        A(1),
        B(2),
        C(3);
    
        companion object {
            val map: MutableMap<Int, Type> = HashMap()
    
            init {
                for (i in Type.values()) {
                    map[i.value] = i
                } 
            }
    
            fun fromInt(type: Int?): Type? {
                return map[type]
            }
        }
    }
    

    My question is, is this the best way to do this, or is there a better way? What if I have several enums that follow a similar pattern? Is there a way in Kotlin to make this code more re-usable across enums?

    • Eldar Agalarov
      Eldar Agalarov almost 4 years
      Your Enum should implement Identifiable interface with id property and companion object should extend abstract class GettableById which holds idToEnumValue map and returns enum value based on id. Details is below in my answer.
  • mfulton26
    mfulton26 almost 8 years
    I was about to recommend the same. In addition, I would make fromInt return non-null like Enum.valueOf(String): map[type] ?: throw IllegalArgumentException()
  • JB Nizet
    JB Nizet almost 8 years
    Given the kotlin support for null-safety, returning null from the method wouldn't bother me as it would in Java: the caller will be forced by the compiler to deal with a null returned value, and decide what to do (throw or do something else).
  • Baron
    Baron almost 8 years
    Thanks, that is pretty compact code. The reason I had fromInt(type: Int?) is that in my case I was mapping values from a database and using JDBI to do the mapping, which returns an Int? and not an Int when retrieving a column value. If for instance, that column is nullable and returns a null, it seems ok in my use case to pass the nullable along.
  • Connor Wyatt
    Connor Wyatt over 7 years
    That's a lot of work for such a simple operation, the accepted answer is much cleaner IMO
  • miensol
    miensol over 7 years
    Fully agree for simple use it's definitely better. I had the above code already to handle explicit names for given enumerated member.
  • Raphael
    Raphael about 7 years
    @JBNizet I agree; so why does the standard valueOf(String) not return an optional? *sigh*
  • JB Nizet
    JB Nizet about 7 years
    @Raphael because enums were introduced in Java 5 and Optional in Java 8.
  • Raphael
    Raphael about 7 years
    @JBNizet Last I looked, Kotlin was a lot newer.
  • Hoang Tran
    Hoang Tran about 6 years
    my version of this code use by lazy{} for the map and getOrDefault() for safer access by value
  • creativecreatorormaybenot
    creativecreatorormaybenot about 6 years
    An obvious enhancement is using first { ... } instead because there is no use for multiple results.
  • Arto Bendiken
    Arto Bendiken almost 6 years
    This solution works well. Note that to be able to call Type.fromInt() from Java code, you will need to annotate the method with @JvmStatic.
  • humazed
    humazed almost 6 years
    No, using first is not an enhancement as it changes the behavior and throws NoSuchElementException if the item is not found where find which is equal to firstOrNull returns null. so if you want to throw instead of returning null use first
  • CoolMind
    CoolMind over 5 years
    This works for constants 0, 1, ..., N. If you have them like 100, 50, 35, then it won't give a right result.
  • Alex V.
    Alex V. about 5 years
    You could even replace fromInt with operator fun invoke(type: Int) = map[type]!!, and then use constructor syntax val t = Type(x).
  • ecth
    ecth almost 5 years
    This method can be used with enums with multiple values: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Also you can throw an exception if the values are not in the enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") or you can use it when calling this method: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
  • AleksandrH
    AleksandrH over 4 years
    Definitely more idiomatic! Cheers.
  • Eldar Agalarov
    Eldar Agalarov almost 4 years
    Your method have linear complexity O(n). Better to use lookup in predefined HashMap with O(1) complexity.
  • Eldar Agalarov
    Eldar Agalarov almost 4 years
    Why you use open class? Just make it abstract.
  • Eldar Agalarov
    Eldar Agalarov almost 4 years
    Your code using reflection (bad) and is bloated (bad too).
  • humazed
    humazed almost 4 years
    yes, I know but in most cases, the enum will have very small number of states so it doesn't matter either way, what's more readable.
  • IPP Nerd
    IPP Nerd over 3 years
    This solution has the downside of (some) memory consumption, but does not better perform than other solutions that do not use a Map.
  • IPP Nerd
    IPP Nerd over 3 years
    This solution performs well and gives the option to provide a default value or ?: throw IllegalArgumentException(status.toString())