Лучшая практика обратного просмотра Java enum

Я видел, как предлагалось в блоге, что следующее был разумным способом выполнить «обратный поиск» с использованием getCode(int) в перечислении Java:

public enum Status {
    WAITING(0),
    READY(1),
    SKIPPED(-1),
    COMPLETED(5);

    private static final Map<Integer,Status> lookup 
            = new HashMap<Integer,Status>();

    static {
        for(Status s : EnumSet.allOf(Status.class))
            lookup.put(s.getCode(), s);
    }

    private int code;

    private Status(int code) {
        this.code = code;
    }

    public int getCode() { return code; }

    public static Status get(int code) { 
        return lookup.get(code); 
    }
}

Для меня статическая карта и статический инициализатор кажутся плохой идеей, и моей первой мыслью было бы закодировать поиск следующим образом:

public enum Status {
    WAITING(0),
    READY(1),
    SKIPPED(-1),
    COMPLETED(5);

    private int code;

    private Status(int code) {
        this.code = code;
    }

    public int getCode() { return code; }

    public static Status get(int code) { 
        for(Status s : values()) {
            if(s.code == code) return s;
        }
        return null;
    }
}

Есть ли какие-либо очевидные проблемы с любым из этих методов и есть ли рекомендуемый способ реализации такого поиска?


person Armand    schedule 15.03.2011    source источник
comment
Кстати, для вас цикл построения карты вы могли бы сделать for(Status s : values()) lookup.put(s.code, s);   -  person Peter Lawrey    schedule 15.03.2011
comment
Что-то не так с использованием Enum.valueOf()? Вы не можете хранить строки?   -  person Jonathan    schedule 15.03.2011
comment
@Jonathan Довольно часто вам нужно производить перечисления из двоичного или числового ввода. Так что я думаю, что в Enum.valueOf() нет ничего неправильного (хотя и с заглавными буквами), но довольно часто у вас просто есть байт или число, с которого нужно начинать. И пожалуйста: если строка не нужна, оставьте ее, посмотрите ужас строкового кодирования, если хотите знать, почему. По сути, вы должны постоянно спрашивать себя: когда я получаю строку, знаю ли я, что в ней? Он содержит намного больше состояний, чем целое число, или, действительно, перечисление и увеличение состояния - это плохо.   -  person Maarten Bodewes    schedule 22.12.2018
comment
Взгляните на stackoverflow.com/questions/28762438/how-to-reverse-enum   -  person bohemian    schedule 09.01.2019


Ответы (7)


Maps.uniqueIndex из Guava от Google весьма удобен для построения поисковых карт.

Обновление: вот пример использования Maps.uniqueIndex с Java 8:

public enum MyEnum {
    A(0), B(1), C(2);

    private static final Map<Integer, MyEnum> LOOKUP = Maps.uniqueIndex(
                Arrays.asList(MyEnum.values()),
                MyEnum::getStatus
    );    

    private final int status;

    MyEnum(int status) {
        this.status = status;
    }

    public int getStatus() {
        return status;
    }

    @Nullable
    public static MyEnum fromStatus(int status) {
        return LOOKUP.get(status);
    }
}
person Community    schedule 15.03.2011
comment
Это прекрасный ответ, и он избавляет от инициализатора класса static. Кто-нибудь знает, есть ли у него какие-либо другие преимущества перед конкретным Map из Java (просто избавление от статического инициализатора для инициализатора, специфичного для поля, не является достаточной причиной для включения библиотеки в мой путь к классам, даже если эта библиотека - Guava). - person Maarten Bodewes; 22.12.2018
comment
На мой взгляд, это хакерское решение, которое добавляет необходимую зависимость от внешней библиотеки. Более красивое и элегантное решение можно найти здесь stackoverflow.com/questions/28762438/how-to-reverse- enum - person bohemian; 09.01.2019
comment
Конечно, для потоков вам не нужна Guava: LOOKUP = stream(values()).collect(toMap(MyEnum::getStatus, x -> x));. - person Gene; 29.05.2019

Хотя статическая карта имеет более высокие накладные расходы, она хороша тем, что предлагает постоянный поиск по code. Время поиска вашей реализации увеличивается линейно с количеством элементов в перечислении. Для небольших перечислений это просто не принесет существенного вклада.

Одна проблема с обеими реализациями (и, возможно, с перечислениями Java в целом) заключается в том, что действительно существует скрытое дополнительное значение, которое Status может принимать: null. В зависимости от правил бизнес-логики, может иметь смысл возвращать фактическое значение перечисления или выдавать Exception, когда поиск «терпит неудачу».

person Matt Ball    schedule 15.03.2011
comment
@Matt, я считаю, что оба способа - это постоянный поиск по времени, потому что на карте есть постоянное количество элементов. - person jjnguy; 15.03.2011
comment
@jjnguy: нет. Выполнение поиска по массиву значений требует проверки каждого элемента в списке. Если вы увеличиваете количество элементов в перечислении, вы увеличиваете количество элементов, которые могут быть проверены для обратного просмотра. - person Matt Ball; 15.03.2011
comment
@ Мэтт, верно ... но оба подхода O(1). Тот, который выполняет цикл каждый раз, - это O(NUMBER_OF_ENUMS), где NUMBER_OF_ENUMS - постоянное число. Таким образом, это то же самое, что O(1) - Это базовый анализ времени выполнения. - person jjnguy; 15.03.2011
comment
@jjnguy: это O(n) в размере перечисления. Это все педантизм, ведь enum вряд ли будет большим. - person Matt Ball; 15.03.2011
comment
@Matt, O(n) предполагает, что n может измениться. n является постоянным во время выполнения в этом сценарии, поэтому он становится постоянной операцией во время выполнения. - person jjnguy; 15.03.2011
comment
@jinguy, обе они могут быть операциями с постоянным временем, но константа в каждой операции разная. Один - это время, чтобы найти значение в хеш-таблице, другой - время, необходимое для перебора массива значений переменной (но постоянного во время выполнения). Если у вас есть миллион значений в этом перечислении (не практично, а просто пример), вы бы предпочли вариант поиска по карте. - person matt b; 15.03.2011
comment
@jjnguy: утверждение, что алгоритм O(n), не означает, что n может изменяться во время выполнения. Что, если бы это произвело аналогичный поиск в неизменяемом (то есть фиксированном во время выполнения) списке? Абсолютно это будет алгоритм O(n), где n - размер списка. - person Matt Ball; 15.03.2011
comment
@Matt, но это то, что нас беспокоит, когда мы говорим о производительности. См. Ответ Пауло. Это поиск O(1), который эквивалентен циклическому просмотру values(). - person jjnguy; 15.03.2011
comment
@jjnguy взгляните на rob-bell .net / 2009/06 / a-beginners-guide-to-big-o-notation O (1) - это алгоритм, который всегда будет выполняться в одно и то же время (или пространство) независимо от размера входных данных. набор данных. тогда как O (N) - это алгоритм, производительность которого будет расти линейно и прямо пропорционально размеру набора входных данных, поэтому, хотя в этом случае размер набора данных не изменяется от запуска к запуску (почему я думаю, вы рассматриваете это 'константа'), производительность алгоритма по-прежнему зависит от размера набора входных данных (в данном случае количества записей в перечислении) - person digitaljoel; 15.03.2011
comment
Я думаю, что в этом случае мы должны пойти и сравнить абсолютные пространственные и временные характеристики для реальных размеров перечислений, а не для N → ∞. - person Paŭlo Ebermann; 15.03.2011
comment
@ Paŭlo: как я сказал в своем ответе, Map будет иметь более высокие накладные расходы, а для небольших перечислений поиск в линейном времени не будет иметь заметного влияния на производительность. Как я уже говорил ранее в комментариях, это все педантизм. - person Matt Ball; 15.03.2011
comment
Фактически, линейное сканирование может быть немного быстрее для типичных перечислений с менее чем 10 членами. Однако есть вызов values(), который, вероятно, замедляет работу. - person maaartinus; 15.03.2011
comment
@jjinguy - Ах, получение первого элемента в списке по сравнению с последним не является операцией с постоянной скоростью (O (1)), даже если размер набора (n) не меняется. Независимо от того, изменится размер или нет, не имеет значения, нас интересует именно операция поиска, и в неупорядоченном наборе это O (n). Получение его из хеш-карты - это константа. Обозначение не обязательно означает, что один быстрее другого, просто он более предсказуем. - person Robin; 15.03.2011
comment
@Robin: получение первого элемента в списке по сравнению с последним не является операцией с постоянной скоростью - это не обязательно так. ArrayList обращается к i-му элементу списка за постоянное время; LinkedList обращается к i-му элементу за O(i) раз. Дело в том, что необходимо пройти по списку, чтобы найти элемент при использовании List, а не Map. - person Matt Ball; 15.03.2011
comment
@Matt Ball - очевидно, я имел в виду рассматриваемый случай, когда ищется элемент в списке, а не выборка по индексу. Следовательно, я также указал неупорядоченный, поскольку время поиска для упорядоченного списка также было бы лучше, чем O (n) - person Robin; 16.03.2011
comment
Я решил вернуть java.util.Optional<MyEnumType> в свой метод статического поиска. Этого не было, когда был написан этот ответ, но он дает понять пользователю, что поиск может завершиться ошибкой. Может быть, вы могли бы включить это в свой ответ? Я, наверное, тоже отправлю ответ, но это тоже хорошее дополнение к вашему. - person Maarten Bodewes; 22.12.2018

Вот альтернатива, которая может быть даже немного быстрее:

public enum Status {
    WAITING(0),
    READY(1),
    SKIPPED(-1),
    COMPLETED(5);

    private int code;

    private Status(int code) {
        this.code = code;
    }

    public int getCode() { return code; }

    public static Status get(int code) {
        switch(code) {
            case  0: return WAITING;
            case  1: return READY;
            case -1: return SKIPPED;
            case  5: return COMPLETED;
        }
        return null;
    }
}

Конечно, это не очень удобно, если вы хотите добавить больше констант позже.

person Paŭlo Ebermann    schedule 15.03.2011
comment
Это в точности то же самое, что и цикл values() во время выполнения, насколько это возможно. - person jjnguy; 15.03.2011
comment
Не в точности, версия коммутатора могла использовать таблицу поиска для перехода непосредственно к нужному коду вместо выполнения серии тестов: artima.com/underthehood/flowP.html - person Dan Berindei; 15.03.2011
comment
@jjnguy: Нет, компилятор может оптимизировать этот переключатель для использования двоичного поиска или таблицы поиска (в зависимости от чисел). И вам не нужно создавать и заполнять массив values() раньше (что само по себе сделало бы этот вариант O(n)). Конечно, теперь метод длиннее, поэтому загрузка занимает больше времени. - person Paŭlo Ebermann; 15.03.2011
comment
@ Пауло, хорошо. Думаю, в этом есть смысл. - person jjnguy; 15.03.2011
comment
@Paulo, это здорово, если вы действительно заботитесь о производительности. Я бы переписал его с помощью ... case WAITING.code: ... и т. Д. И удостоверился, что есть модульный тест, проверяющий все Status.values() на метод get() и удостоверяющийся, что ни один из них не вернул null, а ожидаемый Status не был возвращен - это должно избавить от некоторых проблем, связанных с ремонтопригодностью. - person Armand; 17.03.2011
comment
@Alison: case WAITING.code - хорошая идея, но я боюсь, что это не постоянная времени компиляции. - person Paŭlo Ebermann; 17.03.2011
comment
@ Пауло, честно. Возможно, было бы интересно определить private static final int WAITING_CODE = 0 и использовать его в switch и в объявлении enum ?! - person Armand; 17.03.2011
comment
@jinguy Я не думаю, что они потрудились бы создать инструкцию tablewitch, если бы они не хотели использовать поиск по таблице. Не знаю, есть ли в инструкции lookupswitch какие-либо оптимизации. - person Dan Berindei; 17.03.2011
comment
Но это абсолютно неприемлемое решение с точки зрения чистого кода. С помощью этого решения у вас есть два разных места, где идентификатор сопоставляется со значением перечисления, поэтому могут быть ошибки, когда сопоставления различаются! - person Zordid; 27.03.2013

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

person digitaljoel    schedule 15.03.2011
comment
На мой взгляд, для горстки перечислений это не имеет значения. - person endless; 10.06.2020

Вот альтернатива Java 8 (с модульным тестом):

// DictionarySupport.java :

import org.apache.commons.collections4.Factory;
import org.apache.commons.collections4.map.LazyMap;

import java.util.HashMap;
import java.util.Map;

public interface DictionarySupport<T extends Enum<T>> {

    @SuppressWarnings("unchecked")
    Map<Class<?>,  Map<String, Object>> byCodeMap = LazyMap.lazyMap(new HashMap(), (Factory) HashMap::new);

    @SuppressWarnings("unchecked")
    Map<Class<?>,  Map<Object, String>> byEnumMap = LazyMap.lazyMap(new HashMap(), (Factory) HashMap::new);


    default void init(String code) {
        byCodeMap.get(this.getClass()).put(code, this);
        byEnumMap.get(this.getClass()).put(this, code) ;
    }

    static <T extends Enum<T>> T getByCode(Class<T> clazz,  String code) {
        clazz.getEnumConstants();
        return (T) byCodeMap.get(clazz).get(code);
    }

    default <T extends Enum<T>> String getCode() {
        return byEnumMap.get(this.getClass()).get(this);
    }
}

// Dictionary 1:
public enum Dictionary1 implements DictionarySupport<Dictionary1> {

    VALUE1("code1"),
    VALUE2("code2");

    private Dictionary1(String code) {
        init(code);
    }
}

// Dictionary 2:
public enum Dictionary2 implements DictionarySupport<Dictionary2> {

    VALUE1("code1"),
    VALUE2("code2");

    private Dictionary2(String code) {
        init(code);
    }
}

// DictionarySupportTest.java:     
import org.testng.annotations.Test;
import static org.fest.assertions.api.Assertions.assertThat;

public class DictionarySupportTest {

    @Test
    public void teetSlownikSupport() {

        assertThat(getByCode(Dictionary1.class, "code1")).isEqualTo(Dictionary1.VALUE1);
        assertThat(Dictionary1.VALUE1.getCode()).isEqualTo("code1");

        assertThat(getByCode(Dictionary1.class, "code2")).isEqualTo(Dictionary1.VALUE2);
        assertThat(Dictionary1.VALUE2.getCode()).isEqualTo("code2");


        assertThat(getByCode(Dictionary2.class, "code1")).isEqualTo(Dictionary2.VALUE1);
        assertThat(Dictionary2.VALUE1.getCode()).isEqualTo("code1");

        assertThat(getByCode(Dictionary2.class, "code2")).isEqualTo(Dictionary2.VALUE2);
        assertThat(Dictionary2.VALUE2.getCode()).isEqualTo("code2");

    }
}
person Vitaliy Oliynyk    schedule 11.05.2015
comment
Не могли бы вы объяснить свой код, а не просто дамп кода? Это похоже на нормальную реализацию (на самом деле, я только что сам запрограммировал подобное решение), но без перечисления его свойств людям пришлось бы оценивать его на основе кода, а это вряд ли произойдет. - person Maarten Bodewes; 22.12.2018

В Java 8 я бы просто добавил в ваше перечисление следующий заводской метод и пропустил карту поиска.

public static Optional<Status> of(int value) {
    return Arrays.stream(values()).filter(v -> value == v.getCode()).findFirst();
}
person ce72    schedule 21.04.2017
comment
Мысли по этому поводу: Просто цикл for под прикрытием. Не очень читаемый. Может быть интересно написать для этого многоразовую функцию, которая принимает (value, Status :: getCode) (как это делает Maps.uniqueIndex) - person Barett; 13.12.2017
comment
Это медленнее, чем любой из других ответов, упомянутых выше. 1. Вы создаете новый массив каждый раз из values() (это само по себе очень медленно, поэтому, если вы делаете много, вы можете немного ускорить это, кэшируя массив один раз и повторно используя его), 2. Вы с использованием потоков (которые в тестах производительности / производительности обычно тестируются намного медленнее, чем простой цикл for). - person Shadow Man; 16.04.2019

Оба пути вполне допустимы. И технически у них одинаковое время работы Big-Oh.

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

person jjnguy    schedule 15.03.2011
comment
Поскольку количество констант перечисления равно и постоянно, все равно O(1) :-) - person Paŭlo Ebermann; 15.03.2011
comment
Нет, линейный поиск выполняется за линейное время O (n) вместо O (1) для HashMap. С другой стороны, n равно 4 ... - person Tom Hawtin - tackline; 15.03.2011
comment
@ Пауло, отлично. Это то, о чем я думал. - person jjnguy; 15.03.2011
comment
@Tom, n на самом деле не n, это какое-то постоянное число, которое фиксируется во время выполнения. - person jjnguy; 15.03.2011
comment
@jjnguy: Дело в том, что увеличение количества констант - это линейное увеличение времени выполнения поиска, что делает поиск O (N). То, что количество констант не меняется во время выполнения, несущественно. - person ColinD; 15.03.2011
comment
@jjnguy: этот комментарий бессмысленный. N - количество предметов. Период. - person user207421; 16.03.2011
comment
Тебе действительно стоит исправить ответ. Получение n -го элемента в массиве - O (n). - person user1803551; 06.09.2018
comment
Немного жаль, что этот ответ все еще здесь, потому что ни один цикл for / next никогда не является O (1), это действительно так просто. Нет большого времени выполнения O - большой O просто показывает, что вам нужно умножить на некоторое значение даже на время выполнения одной итерации. Итерирование всех значений в перечислении совершенно нормально с точки зрения производительности, и это должно быть ответом. Я предпочитаю карту просто потому, что при правильной реализации она более читабельна; карта лучше передает намерение: это поиск по таблице. - person Maarten Bodewes; 22.12.2018