А вы знали, что HashMap для enum уступает по эффективности EnumMap? Или что EnumSet под капотом это обычный long? Под катом несколько рецептов удобного применения этих структур.


Классы над которыми будем ставить эксперименты
    enum class RoleType {
        ACTOR,
        COMMENTATOR,
        VOICE_ACTOR,
        DIRECTOR,
        PRODUCER,
        SINGER,
        COMPOSER,
    }

    data class Person(
        val name: String,
        val type: RoleType,
        val age: Int,
    )

Преимущества EnumSet

Типовые операций (add, remove, contains, next) - реализованы при помощи битовых операций, они очень быстрые и выполняются за константное время.

Для енумов с размером меньше 64, типовая структура данных - long

Для енумов с размером больше 64, типовая структура данных - массив long

По сравнению с HashSet занимает гораздо меньший объем памяти

Неудобства EnumSet

По умолчанию EnumSet предоставляет достаточно неудобные статические методы для создания экземпляра класса EnumSet. Например для EnumSet.copyOf(otherCollection), otherCollection обязательно должна быть не пустой, иначе бросится исключение. Чтобы создать пустой EnumSet, надо применить громоздкую конструкцию EnumSet.noneOf(RoleType::class.java).

Преобразование коллекции енумов в EnumSet

Функция помощник

inline fun <reified T : Enum<T>> Collection<T>.toEnumSet(): EnumSet<T> {
    return if (this.isEmpty())
        EnumSet.noneOf(T::class.java)
    else
        EnumSet.copyOf(this)
}

Пример использования

@Test
fun `map any collection to enumSet`() {
    val rolesList = createPersons().map { it.type }
    val allPersonRoles: Set<RoleType> = rolesList.toEnumSet()
}

Преобразование произвольной коллекции в EnumSet

В предыдущем примере персоны сначала преобразуются в List<PersonType>, попробуем убрать этот промежуточный шаг

Функция помощник

inline fun <T, reified R : Enum<R>> Iterable<T>.mapToEnumSet(
  crossinline transform: (T) -> R
): EnumSet<R> = mapTo(EnumSet.noneOf(R::class.java), transform)

Пример использования

@Test
fun `get all unique roles`() {
    val team = createPersons()
    val allPersonRoles: Set<RoleType> = team
        .filter { it.age > 40 }
        .mapToEnumSet { it.type }
}

Группировка по типу в EnumMap

Функция помощник

inline fun <T, reified R : Enum<R>> Iterable<T>.groupByToEnumMap(
    crossinline selector: (T) -> R
): EnumMap<R, MutableList<T>> {
    return groupByTo(EnumMap(R::class.java), selector)
}

Пример использования

@Test
fun `group persons by role`() {
    val team = createPersons()
    val personsByRole: Map<RoleType, List<Person>> = team
        .groupByToEnumMap { it.type }
}

Группировка по типу в EnumMap с преобразованием

Функция помощник

inline fun <T, reified R : Enum<R>, U> Iterable<T>.groupByToEnumMap(
    crossinline selector: (T) -> R,
    crossinline transform: (T) -> U,
): EnumMap<R, MutableList<U>> {
    return groupByTo(EnumMap(R::class.java), selector, transform)
}

Пример использования

@Test
fun `group person names by role`() {
  val team = createPersons()
  val namesByRole: Map<RoleType, List<String>> = team
    .groupByToEnumMap({ it.type }) { it.name }
}

Ассоциирование по типу в EnumMap

Функция помощник

inline fun <T, reified R : Enum<R>> Iterable<T>.associateByToEnumMap(
    crossinline selector: (T) -> R
): EnumMap<R, T> {
    return associateByTo(EnumMap(R::class.java), selector)
}

Пример использования

@Test
fun `associate by role`() {
  val team = createPersons()
  val roleToPersonMap: Map<RoleType, Person> = team
    .associateByToEnumMap { it.type }
}

Комментарии (3)


  1. GospodinKolhoznik
    20.12.2023 07:47

    А вы знали, что HashMap для enum уступает по эффективности EnumMap?

    Если это так, то почему компилятор автоматически не заменяет HashMap для enum на EnumMap?


    1. PqDn Автор
      20.12.2023 07:47

      Хороший поинт. Но в реальности надо самому управлять структурами данных.

      Я кстати знаю, что Jackson, при десериализации автоматически выбирает именно HashSet, если в моделе указан интерфейс Set от енума. Так что без подсказок наш софт врятли будет работать наиболее эффективно


      1. GospodinKolhoznik
        20.12.2023 07:47

        Может быть, но почему надо самому? Потому что раньше так делали? Ну раньше и структуры реализовывали самостоятельно в машинных кодах.

        Компилятор, как и любая другая программа должна выполнить некоторый контракт. В данном случае контракт это "Я сделаю так, что програмный код будет выполнен не более чем за N шагов, потребляя не более, чем за M памяти". Всё остальное, т.е. каким именно образом компилятор будет исполнять этот контракт, это вообще не дело пользователя. Ну это же основной принцип разделения ПО на слои абстракции - есть слои, между слоями есть контракты, а то, как исполняются эти контракты под капотом каждого слоя, это личное дело каждого слоя и другие слои об это думать не должны. И как я вижу, если программист (пользователь компилятора) вместо того, чтобы ориентироваться на контракт ориентируется на то, как оно там устроено в компиляторе под капотом, по моему это в каком то смысле баг компилятора. Понятно почему - поменялась внутренняя логика работы компилятора, и из-за этого надо переписывать код программы.

        В чем то я понимаю, что программисту хочется обладать максимумом контроля над программой. Но как разработчик промышленного компилятора, я в этом вижу нарушения принципа изолированности слоёв ПО.