Effective Enums in Kotlin with reverse lookup?
EnumsKotlinEnums Problem Overview
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?
Enums Solutions
Solution 1 - Enums
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 - Enums
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 - Enums
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 - Enums
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 - Enums
I found myself doing the reverse lookup by custom, hand coded, value couple of times and came of up with following approach.
Make enum
s 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)
Solution 6 - Enums
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
Solution 7 - Enums
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
}
}
Solution 8 - Enums
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])
}
Solution 9 - Enums
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)
}
}
}
Solution 10 - Enums
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")
}
}
Solution 11 - Enums
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)
Solution 12 - Enums
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.
Solution 13 - Enums
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
Solution 14 - Enums
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()
Solution 15 - Enums
val t = Type.values()[ordinal]
:)