Effective Enums in Kotlin with reverse lookup?
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
Baron
Updated on March 02, 2022Comments
-
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 almost 4 yearsYour 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 almost 8 yearsI was about to recommend the same. In addition, I would make
fromInt
return non-null likeEnum.valueOf(String)
:map[type] ?: throw IllegalArgumentException()
-
JB Nizet almost 8 yearsGiven 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 almost 8 yearsThanks, 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 over 7 yearsThat's a lot of work for such a simple operation, the accepted answer is much cleaner IMO
-
miensol over 7 yearsFully agree for simple use it's definitely better. I had the above code already to handle explicit names for given enumerated member.
-
Raphael about 7 years@JBNizet I agree; so why does the standard
valueOf(String)
not return an optional? *sigh* -
JB Nizet about 7 years@Raphael because enums were introduced in Java 5 and Optional in Java 8.
-
Raphael about 7 years@JBNizet Last I looked, Kotlin was a lot newer.
-
Hoang Tran about 6 yearsmy version of this code use
by lazy{}
for themap
andgetOrDefault()
for safer access byvalue
-
creativecreatorormaybenot about 6 yearsAn obvious enhancement is using
first { ... }
instead because there is no use for multiple results. -
Arto Bendiken almost 6 yearsThis 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 almost 6 yearsNo, using
first
is not an enhancement as it changes the behavior and throwsNoSuchElementException
if the item is not found wherefind
which is equal tofirstOrNull
returnsnull
. so if you want to throw instead of returning null usefirst
-
CoolMind over 5 yearsThis works for constants 0, 1, ..., N. If you have them like 100, 50, 35, then it won't give a right result.
-
Alex V. about 5 yearsYou could even replace
fromInt
withoperator fun invoke(type: Int) = map[type]!!
, and then use constructor syntaxval t = Type(x)
. -
ecth almost 5 yearsThis 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 over 4 yearsDefinitely more idiomatic! Cheers.
-
Eldar Agalarov almost 4 yearsYour method have linear complexity O(n). Better to use lookup in predefined HashMap with O(1) complexity.
-
Eldar Agalarov almost 4 yearsWhy you use open class? Just make it abstract.
-
Eldar Agalarov almost 4 yearsYour code using reflection (bad) and is bloated (bad too).
-
humazed almost 4 yearsyes, 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 over 3 yearsThis solution has the downside of (some) memory consumption, but does not better perform than other solutions that do not use a Map.
-
IPP Nerd over 3 yearsThis solution performs well and gives the option to provide a default value or
?: throw IllegalArgumentException(status.toString())