Эффективные перечисления в Kotlin: как сделать обратный поиск по значению?
Оптимальное решение: ленивая инициализация маппинга через companion object
В Kotlin лучший способ реализовать обратный поиск — создать статическую мапу внутри companion object с ленивой инициализацией. Это даёт O(1) время доступа и не требует перебора всех значений при каждом запросе.
enum class OrderStatus(val code: Int) {
PENDING(1),
PROCESSING(2),
SHIPPED(3),
DELIVERED(4),
CANCELLED(5);
companion object {
private val map: Map<Int, OrderStatus> by lazy {
values().associateBy { it.code }
}
fun fromCode(code: Int): OrderStatus? = map[code]
}
}
Как это работает:
by lazy— мапа создаётся только при первом обращении кfromCode(), а не при загрузке класса.associateBy { it.code }— строит HashMap, где ключ — этоcode, а значение — сам enum.- Доступ по ключу в HashMap — O(1), что идеально для больших enum.
Почему это лучше вашего варианта?
Ваша функция values().find { ... } каждый раз проходит по всем элементам (O(n)). Для 5 значений разница незаметна, но для 50+ она может быть существенной, особенно если вызывать fromCode тысячи раз в секунду. Мапа решает эту проблему раз и навсегда.
Дополнительные улучшения
Если вы хотите избежать null и выбрасывать исключение при неизвестном коде:
fun fromCode(code: Int): OrderStatus =
map[code] ?: throw IllegalArgumentException("Unknown code: $code")
Или если хотите более безопасный API с возможностью задать значение по умолчанию:
fun fromCode(code: Int, default: OrderStatus = PENDING): OrderStatus =
map[code] ?: default
Альтернативный синтаксис (без lazy)
Если enum небольшой или загружается редко, можно без lazy:
companion object {
private val map = values().associateBy { it.code }
fun fromCode(code: Int) = map[code]
}
Разница: мапа создаётся сразу при инициализации класса (при первом обращении к любому методу enum). lazy откладывает создание до первого вызова fromCode.
Для enum с разными типами ключей
Шаблон универсален для любых типов (String, Long, UUID и т.д.):
enum class Priority(val level: String) {
LOW("low"),
MEDIUM("medium"),
HIGH("high");
companion object {
private val map = values().associateBy { it.level }
fun fromLevel(level: String) = map[level]
}
}
Этот подход считается идиоматичным для Kotlin и рекомендуется в официальных гайдлайнах. Он компактный, эффективный и легко читается.
<<Альтернатива: использование sealed class вместо enum
Если ваш случай сложнее, чем просто числовые коды (например, нужно хранить дополнительные данные или иметь разную логику для каждого значения), можно рассмотреть sealed class с объектами:
sealed class OrderStatus(val code: Int) {
object PENDING : OrderStatus(1)
object PROCESSING : OrderStatus(2)
object SHIPPED : OrderStatus(3)
object DELIVERED : OrderStatus(4)
object CANCELLED : OrderStatus(5)
companion object {
private val map = listOf(
PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED
).associateBy { it.code }
fun fromCode(code: Int): OrderStatus? = map[code]
}
}
Плюсы:
- Можно наследовать разные классы с разной реализацией методов.
- Больше гибкости для сложной логики.
Минусы:
- Нет встроенного
values()— нужно вручную перечислять объекты. - Нельзя использовать в
whenбезelse(хотя можно добавить абстрактный метод).
Для простого случая с кодами лучше оставить enum — он лаконичнее и имеет встроенную поддержку values() и valueOf().