12 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Enum class | Перечисления со свойствами и методами, entries, when по enum и его границы |
|
enum class - класс с фиксированным набором экземпляров.
enum class Status { ACTIVE, BLOCKED, DELETED }
fun main() {
val s = Status.ACTIVE
println(s) // ACTIVE
println(s.name) // ACTIVE — строка
println(s.ordinal) // 0 — позиция в объявлении
}
Свойства и конструктор
Каждая константа - вызов конструктора:
enum class Planet(val radiusKm: Int, val hasRings: Boolean) {
EARTH(6371, false),
SATURN(58232, true),
MARS(3390, false);
val radiusM: Int get() = radiusKm * 1000
fun describe() = "$name: радиус $radiusKm км, кольца=$hasRings"
}
fun main() {
println(Planet.SATURN.describe()) // SATURN: радиус 58232 км, кольца=true
println(Planet.EARTH.radiusM) // 6371000
}
Точка с запятой после последней константы обязательна, если дальше идёт тело класса.
Методы в константах
Константа может переопределить метод - это заменяет when по самой себе:
enum class Operation {
PLUS { override fun apply(a: Int, b: Int) = a + b },
MINUS { override fun apply(a: Int, b: Int) = a - b },
TIMES { override fun apply(a: Int, b: Int) = a * b };
abstract fun apply(a: Int, b: Int): Int
}
fun main() {
println(Operation.PLUS.apply(2, 3)) // 5
println(Operation.TIMES.apply(2, 3)) // 6
println(Operation.entries.joinToString { "${it.name}=${it.apply(6, 3)}" })
// PLUS=9, MINUS=3, TIMES=18
}
Каждая такая константа компилируется в анонимный подкласс.
Реализация интерфейса
interface Describable { fun describe(): String }
enum class Level(val weight: Int) : Describable {
LOW(1), HIGH(10);
override fun describe() = "$name (вес $weight)"
}
fun main() {
val items: List<Describable> = Level.entries
println(items.joinToString { it.describe() }) // LOW (вес 1), HIGH (вес 10)
}
entries, values, valueOf
enum class Status { ACTIVE, BLOCKED }
fun main() {
println(Status.entries) // [ACTIVE, BLOCKED] — список
println(Status.entries.size) // 2
println(Status.valueOf("ACTIVE")) // ACTIVE
println(Status.entries.first().name) // ACTIVE
// println(Status.valueOf("НЕТ")) // IllegalArgumentException
}
entries (Kotlin 1.9+) заменяет values(). Разница практическая:
values() |
entries |
|
|---|---|---|
| тип | Array<Status> |
EnumEntries<Status> (это List) |
| аллокация | новый массив при каждом вызове | один неизменяемый список |
| изменяемость | элементы можно подменить | нельзя |
enum class Status { ACTIVE, BLOCKED }
fun main() {
@Suppress("DEPRECATION")
println(Status.values().contentToString()) // [ACTIVE, BLOCKED]
println(Status.entries) // [ACTIVE, BLOCKED]
}
В новом коде всегда entries.
Безопасный разбор строки:
enum class Status { ACTIVE, BLOCKED }
fun parseStatus(s: String): Status? =
Status.entries.find { it.name == s }
fun main() {
println(parseStatus("ACTIVE")) // ACTIVE
println(parseStatus("нет")) // null — без исключения
}
Через enumValueOf то же самое, но с исключением. Для внешних данных (JSON, БД, параметры запроса) нужна версия с null.
when по enum
Главное применение. Компилятор проверяет полноту, и else не нужен:
enum class Status { ACTIVE, BLOCKED, DELETED }
fun message(s: Status): String = when (s) {
Status.ACTIVE -> "работает"
Status.BLOCKED -> "заблокирован"
Status.DELETED -> "удалён"
}
fun main() {
println(message(Status.BLOCKED)) // заблокирован
}
Если добавить константу ARCHIVED, этот when перестанет компилироваться - компилятор найдёт все места, которые нужно обновить. Поэтому else в when по enum - вредная привычка: он скрывает именно те ошибки, от которых enum защищает.
Импорт констант сокращает запись:
enum class Status { ACTIVE, BLOCKED }
fun message(s: Status): String {
return when (s) {
Status.ACTIVE -> "работает"
Status.BLOCKED -> "заблокирован"
}
}
fun main() = println(message(Status.ACTIVE))
Сравнение и порядок
enum class Level { LOW, MEDIUM, HIGH }
fun main() {
println(Level.LOW == Level.LOW) // true
println(Level.LOW < Level.HIGH) // true — по ordinal
println(Level.entries.sorted()) // [LOW, MEDIUM, HIGH]
println(Level.HIGH.ordinal) // 2
println(Level.entries.maxOf { it.ordinal }) // 2
}
enum реализует Comparable по ordinal. Сравнивать через == безопасно: экземпляр единственный, поэтому == и === дают одно и то же.
EnumMap и EnumSet
Специализированные коллекции для enum - массив вместо хеш-таблицы:
import java.util.EnumMap
import java.util.EnumSet
enum class Day { MON, TUE, WED }
fun main() {
val hours = EnumMap<Day, Int>(Day::class.java)
hours[Day.MON] = 8
hours[Day.TUE] = 6
println(hours) // {MON=8, TUE=6}
val workdays = EnumSet.of(Day.MON, Day.TUE)
println(workdays.contains(Day.WED)) // false
println(EnumSet.complementOf(workdays)) // [WED]
}
Это классы JDK; в stdlib Kotlin аналогов нет. Для небольших enum разница с HashMap несущественна.
companion object
enum class Status(val code: Int) {
ACTIVE(1), BLOCKED(2);
companion object {
private val byCode = entries.associateBy { it.code }
fun fromCode(code: Int): Status? = byCode[code]
}
}
fun main() {
println(Status.fromCode(2)) // BLOCKED
println(Status.fromCode(99)) // null
}
Поиск по коду через заранее построенную Map - стандартный приём. Подробнее: Object и companion object
Когда enum, а когда sealed
enum class Status { OK, ERROR } // варианты без данных
sealed interface Result2
data class Ok(val value: Int) : Result2 // варианты с разными данными
data class Error(val message: String) : Result2
enum - фиксированный набор значений, у всех одинаковая структура. sealed - фиксированный набор типов, у каждого свои свойства. Подробнее: Sealed class и interface
[!info] Отличия от Java
Java Kotlin enum Status { ACTIVE }enum class Status { ACTIVE }Status.values()Status.entriesStatus.valueOf(s)Status.valueOf(s)илиentries.findswitchбез проверки полноты до Java 21whenпроверяет всегдаEnumMap/EnumSetто же (из JDK) Устройство одинаковое - на JVM это тот же
java.lang.Enum. Два практических различия:
entriesвместоvalues(). Java-методvalues()возвращает новый массив при каждом вызове, потому что массив изменяем и вернуть общий нельзя.entriesвозвращает неизменяемый список, поэтому экземпляр один. В цикле это заметная разница.Проверка полноты. В Java
switchпо enum безdefaultдолго не проверялся: добавление константы молча менялo поведение. Kotlin проверяетwhen-выражение с момента появления языка, а с Kotlin 2.0 - иwhen-инструкцию.Сравнить: Enum в Java
[!warning] Подводные камни
ordinalнельзя сохранять в БД или в файл. Он меняется при вставке константы в середину:enum class Status { ACTIVE, BLOCKED } // BLOCKED.ordinal == 1 // добавили NEW в середину: // enum class Status { ACTIVE, NEW, BLOCKED } // BLOCKED.ordinal == 2Все сохранённые единицы станут означать
NEW. Для сериализации используйтеnameили свой стабильныйcode.
nameтоже хрупок при переименовании - но его хотя бы нельзя изменить случайно. Самый надёжный вариант - явное свойство:enum class Status(val code: String) { ACTIVE("active"), BLOCKED("blocked") } fun main() { println(Status.ACTIVE.code) } // active
valueOfбросает исключение на незнакомой строке, а не возвращаетnull. Для внешних данных нужна своя обёртка черезentries.find.
values()аллоцирует массив на каждый вызов:enum class Status { ACTIVE } fun main() { @Suppress("DEPRECATION") repeat(3) { Status.values() } // три разных массива repeat(3) { Status.entries } // один и тот же список println("ok") }
elseвwhenпо enum отключает проверку полноты. Самая частая ошибка: добавили константу, компилятор промолчал, в рантайме сработала веткаelse.Enum нельзя наследовать и расширять. Если набор вариантов должен быть расширяемым (например, плагинами), enum не подходит - нужен
sealed interfaceили обычная иерархия.Enum с изменяемым состоянием - глобальная переменная:
enum class Counter { INSTANCE; var count = 0 // общее состояние на всю программу } fun main() { Counter.INSTANCE.count++ println(Counter.INSTANCE.count) // 1 — и так во всех потоках }Константы enum - синглтоны, поэтому
varв них небезопасен при многопоточности.
whenпо nullable enum всё равно требуетelseили веткиnull:enum class Status { ACTIVE, BLOCKED } fun message(s: Status?): String = when (s) { Status.ACTIVE -> "работает" Status.BLOCKED -> "заблокирован" null -> "не задан" } fun main() { println(message(null)) } // не задан
Дополнительно: Sealed class и interface Далее: Sealed class и interface