2

Effective Enums in Kotlin with reverse lookup?

 10 months ago
source link: https://stackoverflow.com/questions/37794850/effective-enums-in-kotlin-with-reverse-lookup/62758659#62758659
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Effective Enums in Kotlin with reverse lookup?

Asked 7 years, 2 months ago
Modified 11 months ago
Viewed 72k times

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?

asked Jun 13, 2016 at 16:30

15 Answers

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.

answered Jun 13, 2016 at 17:29

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

companion object {
   fun find(value: Int): Type? = Type.values().find { it.value == value }
}
answered Nov 5, 2017 at 20:14

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].

answered Aug 20, 2019 at 11:24

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)

answered Jun 14, 2016 at 3:00

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

answered Feb 5, 2021 at 21:46

I found myself doing the reverse lookup by custom, hand coded, value couple of times and came of up with following approach.

Make enums implement a shared interface:

interface Codified<out T : Serializable> {
    val code: T
}

enum class Alphabet(val value: Int) : Codified<Int> {
    A(1),
    B(2),
    C(3);

    override val code = value
}

This interface (however strange the name is :)) marks a certain value as the explicit code. The goal is to be able to write:

val a = Alphabet::class.decode(1) //Alphabet.A
val d = Alphabet::class.tryDecode(4) //null

Which can easily be achieved with the following code:

interface Codified<out T : Serializable> {
    val code: T

    object Enums {
        private val enumCodesByClass = ConcurrentHashMap<Class<*>, Map<Serializable, Enum<*>>>()

        inline fun <reified T, TCode : Serializable> decode(code: TCode): T where T : Codified<TCode>, T : Enum<*> {
            return decode(T::class.java, code)
        }

        fun <T, TCode : Serializable> decode(enumClass: Class<T>, code: TCode): T where T : Codified<TCode> {
            return tryDecode(enumClass, code) ?: throw IllegalArgumentException("No $enumClass value with code == $code")
        }

        inline fun <reified T, TCode : Serializable> tryDecode(code: TCode): T? where T : Codified<TCode> {
            return tryDecode(T::class.java, code)
        }

        @Suppress("UNCHECKED_CAST")
        fun <T, TCode : Serializable> tryDecode(enumClass: Class<T>, code: TCode): T? where T : Codified<TCode> {
            val valuesForEnumClass = enumCodesByClass.getOrPut(enumClass as Class<Enum<*>>, {
                enumClass.enumConstants.associateBy { (it as T).code }
            })

            return valuesForEnumClass[code] as T?
        }
    }
}

fun <T, TCode> KClass<T>.decode(code: TCode): T
        where T : Codified<TCode>, T : Enum<T>, TCode : Serializable 
        = Codified.Enums.decode(java, code)

fun <T, TCode> KClass<T>.tryDecode(code: TCode): T?
        where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
        = Codified.Enums.tryDecode(java, code)
answered Jun 13, 2016 at 18:02

Another example implementation. This also sets the default value (here to OPEN) if no the input matches no enum option:

enum class Status(val status: Int) {
OPEN(1),
CLOSED(2);

companion object {
    @JvmStatic
    fun fromInt(status: Int): Status =
        values().find { value -> value.status == status } ?: OPEN
}
answered May 3, 2019 at 13:46

True Idiomatic Kotlin Way. Without bloated reflection code:

interface Identifiable<T : Number> {

    val id: T
}

abstract class GettableById<T, R>(values: Array<R>) where T : Number, R : Enum<R>, R : Identifiable<T> {

    private val idToValue: Map<T, R> = values.associateBy { it.id }

    operator fun get(id: T): R = getById(id)

    fun getById(id: T): R = idToValue.getValue(id)
}

enum class DataType(override val id: Short): Identifiable<Short> {

    INT(1), FLOAT(2), STRING(3);

    companion object: GettableById<Short, DataType>(values())
}

fun main() {
    println(DataType.getById(1))
    // or
    println(DataType[2])
}
answered Jul 6, 2020 at 14:51

A variant of some previous proposals might be the following, using ordinal field and getValue :

enum class Type {
A, B, C;

companion object {
    private val map = values().associateBy(Type::ordinal)

    fun fromInt(number: Int): Type {
        require(number in 0 until map.size) { "number out of bounds (must be positive or zero & inferior to map.size)." }
        return map.getValue(number)
    }
}
answered Apr 19, 2019 at 19:34

A slightly extended approach of the accepted solution with null check and invoke function

fun main(args: Array<String>) {
    val a = Type.A // find by name
    val anotherA = Type.valueOf("A") // find by name with Enums default valueOf
    val aLikeAClass = Type(3) // find by value using invoke - looks like object creation

    val againA = Type.of(3) // find by value
    val notPossible = Type.of(6) // can result in null
    val notPossibleButThrowsError = Type.ofNullSave(6) // can result in IllegalArgumentException

    // prints: A, A, 0, 3
    println("$a, ${a.name}, ${a.ordinal}, ${a.value}")
    // prints: A, A, A null, java.lang.IllegalArgumentException: No enum constant Type with value 6
    println("$anotherA, $againA, $aLikeAClass $notPossible, $notPossibleButThrowsError")
}

enum class Type(val value: Int) {
    A(3),
    B(4),
    C(5);

    companion object {
        private val map = values().associateBy(Type::value)
        operator fun invoke(type: Int) = ofNullSave(type)
        fun of(type: Int) = map[type]
        fun ofNullSave(type: Int) = map[type] ?: IllegalArgumentException("No enum constant Type with value $type")
    }
}
answered Aug 25, 2020 at 18:56

Based on your example, i might suggest removing the associated value and just use the ordinal which is similar to an index.

ordinal - Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial constant is assigned an ordinal of zero).

enum class NavInfoType {
    GreenBuoy,
    RedBuoy,
    OtherBeacon,
    Bridge,
    Unknown;

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

In my case i wanted to return Unknown if the map returned null. You could also throw an illegal argument exception by replacing the get with the following:

operator fun get(value: Int) = map[value] ?: throw IllegalArgumentException()
answered Nov 21, 2020 at 18:17

An approach that reuses code:

interface IndexedEnum {
    val value: Int

    companion object {
        inline fun <reified T : IndexedEnum> valueOf(value: Int) =
            T::class.java.takeIf { it.isEnum }?.enumConstants?.find { it.value == value }
    }
}

Then the enums can be made indexable:

enum class Type(override val value: Int): IndexedEnum {
    A(1),
    B(2),
    C(3)
}

and reverse searched like so:

IndexedEnum.valueOf<Type>(3)
answered Dec 30, 2020 at 1:20

There is a completely generic solution that

  • Does not use reflection, Java or Kotlin
  • Is cross-platform, does not need any java
  • Has minimum hassle

First, let's define our interfaces as value field is not inherent to all enums:

interface WithValue {
    val value: Int
}

interface EnumCompanion<E> where E: Enum<E> {
    val map: Map<Int, E>
    fun fromInt(type: Int): E = map[type] ?: throw IllegalArgumentException()
}

Then, you can do the following trick

inline fun <reified E> EnumCompanion() : EnumCompanion<E>
where E : Enum<E>, E: WithValue = object : EnumCompanion<E> {
    override val map: Map<Int, E> = enumValues<E>().associateBy { it.value }
}

Then, for every enum you have the following just works

enum class RGB(override val value: Int): WithValue {
    RED(1), GREEN(2), BLUE(3);
    companion object: EnumCompanion<RGB> by EnumCompanion()
}

val ccc = RGB.fromInt(1)

enum class Shapes(override val value: Int): WithValue {
    SQUARE(22), CIRCLE(33), RECTANGLE(300);
    companion object: EnumCompanion<Shapes> by EnumCompanion()
}

val zzz = Shapes.fromInt(33)

As already mentioned, this is not worth it unless you have a lot of enums and you really need to get this generic.

answered Mar 1, 2022 at 11:46

Came up with a more generic solution

inline fun <reified T : Enum<*>> findEnumConstantFromProperty(predicate: (T) -> Boolean): T? =
T::class.java.enumConstants?.find(predicate)

Example usage:

findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A
answered Jun 3, 2020 at 16:51

val t = Type.values()[ordinal]

answered Nov 4, 2017 at 17:12

Your Answer

Sign up or log in

Sign up using Google
Sign up using Facebook
Sign up using Email and Password

Post as a guest

Name
Email

Required, but never shown

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK