Найкраща практика зворотного пошуку Java enum


76

Я бачив, що в блозі припускають, що наступним є розумний спосіб зробити "зворотний пошук", використовуючи 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;
    }
}

Чи є очевидні проблеми з будь-яким із методів, і чи є рекомендований спосіб реалізації такого виду пошуку?


До речі, для вас цикл побудови карт, який ви могли б зробитиfor(Status s : values()) lookup.put(s.code, s);
Пітер Лорі

4
Щось не так із використанням Enum.valueOf()? Ви не можете зберігати рядки?
Джонатан,

1
@Jonathan Досить часто потрібно створювати переліки з двійкового або числового введення. Таким чином , я припускаю , що немає нічого поганого з Enum.valueOf()(пом капіталізацією , хоча) , але досить часто ви тільки що отримав байт або номер , щоб почати с. І будь ласка: якщо рядок не потрібен, залиште його, шукайте "строго набраний кодувальний жах", якщо ви хочете знати, чому. В основному вам слід запитати себе постійно: коли я отримую рядок, чи знаю я, що в ньому? Він містить набагато більше стану, ніж ціле число або, насправді, збільшення кількості та стану є поганим .
Maarten Bodewes

Відповіді:


30

Maps.uniqueIndex від компанії 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);
    }
}

2
Це чудова відповідь, і це позбавляє від staticініціалізатора класу. Хтось знає, чи має він якісь інші переваги перед конкретною Mapз Java (просто позбавлення від статичного ініціалізатора для конкретного польового ініціалізатора не є достатньою причиною для того, щоб я включив бібліотеку до свого шляху до класу, навіть якщо ця бібліотека є Гуавою).
Maarten Bodewes

На мій погляд, це хакерське рішення, яке додає непотрібну залежність від зовнішньої бібліотеки. Приємніше, елегантніше рішення можна знайти тут stackoverflow.com/questions/28762438/how-to-reverse-enum
богема

2
Звичайно з потоками вам не потрібно Guava: LOOKUP = stream(values()).collect(toMap(MyEnum::getStatus, x -> x));.
Джин

20

Незважаючи на те, що вона має більші накладні витрати, статична карта приємна, оскільки вона пропонує постійний пошук по code. Час пошуку вашої реалізації лінійно збільшується із збільшенням кількості елементів у переліку. Для малих переліків це просто не буде суттєво сприяти.

Одна з проблем , з обома реалізаціями (і, можливо, з Java перерахуваннями в цілому) є те , що є на насправді приховане додаткове значення, яке Statusможе взяти на себе : null. Залежно від правил ділової логіки, може мати сенс повернути фактичне значення перерахування або видати Exception, коли пошук "не вдається".


2
@Matt, я вважаю, що обидва способи - це постійний пошук часу, оскільки на Карті є постійна кількість елементів.
jjnguy

3
@jjnguy: це O(n)розмір переліку. Це все педантичність, тому що перелік навряд чи буде великим.
Matt Ball

6
@jinguy, вони обидві можуть бути "постійними" операціями часу, але константа в кожній операції різна. Один - це час пошуку значення в хеш-таблиці, інший - час, необхідний для циклу через змінний (але постійний під час виконання) масив значень. Якби у вас був один мільйон значень у цьому переліченні (не практично, а лише приклад), тоді ви віддали б перевагу варіанту перегляду карти.
matt b

5
@jjnguy: заява про те, що алгоритм є O(n), не означає, що воно nможе змінюватися під час виконання . Що, якби це виконало аналогічний пошук у незмінному (тому фіксованому під час виконання) списку? Це був би абсолютно O(n)алгоритм, де nрозмір списку.
Matt Ball

11
@jjnguy подивіться на rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation O (1) - це "алгоритм, який завжди буде виконуватися в один і той же час (або пробіл) незалежно розміру набору вхідних даних. " тоді як O (N) - це "алгоритм, продуктивність якого буде рости лінійно і прямо пропорційно розміру вхідного набору даних", тому, хоча в цьому випадку розмір набору даних не змінюється від запуску до запуску (чому я думаю ви вважаєте його "постійним"), ефективність алгоритму все ще базується на розмірі набору вхідних даних (в даному випадку кількості записів у
переліку

7

Ось альтернатива, яка може бути навіть трохи швидшою:

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;
    }
}

Звичайно, це насправді не підтримується, якщо ви хочете мати можливість додати більше констант пізніше.


9
Не зовсім те саме, версія комутатора може використовувати таблицю пошуку, щоб перейти безпосередньо до правильного коду, замість того, щоб проводити серію тестів: artima.com/underthehood/flowP.html
Ден Беріндей

12
@jjnguy: Ні, компілятор може оптимізувати цей перемикач для використання двійкового пошуку або таблиці пошуку (залежно від чисел). І вам не потрібно створювати та заповнювати values()масив раніше (що саме зробить цей варіант O(n)). Звичайно, зараз метод довший, тому завантаження займає більше часу.
Paŭlo Ebermann

1
@Alison: case WAITING.codeце гарна ідея, але я боюся, що це не константа часу компіляції.
Paŭlo Ebermann

1
@jinguy Я не думаю, що вони б потрудились створити інструкцію табличного перемикача, якби не хотіли використовувати пошук таблиці. Не знаю, чи є в інструкції пошуку перемикач якісь оптимізації.
Dan Berindei

15
Але це абсолютно неприйнятне рішення з точки зору чистого коду. За допомогою цього рішення ви отримали два різних місця, де ідентифікатор зіставляється зі значенням перерахування, тому можуть виникати помилки, коли відображення відрізняються!
Зордід

6

Очевидно, що карта буде забезпечувати постійний пошук часу, тоді як цикл не буде. У типовому переліченні з кількома значеннями я не бачу проблем із пошуковим пошуком.


Для купки переліків, на мій погляд, це не має значення.
нескінченні

3

Ось альтернатива 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");

    }
}

1
Не могли б ви пояснити свій код замість того, щоб вказати лише дамп коду? Це схоже на реалізацію OK (насправді, я щойно запрограмував подібне рішення), але без перерахування його властивостей людям доведеться оцінювати його на основі коду, і це, швидше за все, не трапиться.
Maarten Bodewes

0

У Java 8 я просто додав би наступний заводський метод до вашого переліку і пропустив карту пошуку.

public static Optional<Status> of(int value) {
    return Arrays.stream(values()).filter(v -> value == v.getCode()).findFirst();
}

Думки про це: Просто петля for під ковдрою. Не надто читається. Може бути цікаво написати багаторазову функцію, щоб зробити це і взяти її (значення, Статус :: getCode) (як це робить Maps.uniqueIndex)
Барет

1
Це повільніше, ніж будь-яка з інших відповідей, згаданих вище. 1. Ви створюєте новий масив щоразу з values()(це саме по собі дуже повільно, тому, якщо робити багато, ви можете досить швидко пришвидшити це, кешуючи масив один раз і використовуючи його повторно), 2. Ви використовуєте потоки (які в тестах продуктивності / тестування зазвичай тестують набагато повільніше, ніж простий цикл for-loop).
Shadow Man

0
@AllArgsConstructor
@Getter
public enum MyEnum {
    A(0),
    B(1),
    C(2);
    private static final Map<Integer, MyEnum> LOOKUP =
            Arrays.stream(MyEnum.values()).collect(Collectors.toMap(MyEnum::getStatus, Function.identity()));
    private final int status;

    @Nullable
    public static MyEnum fromStatus(int status) {
        return LOOKUP.get(status);
    }
}

-2

Обидва способи цілком справедливі. І у них технічно однаковий час роботи Big-Oh.

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


3
Оскільки кількість констант перерахувань є постійною, все є O(1):-)
Paŭlo Ebermann

12
Ні, лінійний пошук виконується за лінійний час O (n) замість O (1) для HashMap. З іншого боку, n дорівнює 4 ...
Том Хоутін - таклін

6
@jjnguy: Річ у тому, що збільшення кількості констант - це лінійне збільшення часу виконання пошуку, що робить пошук O (N). Те, що кількість констант не змінюється під час виконання, не має значення.
ColinD

4
@jjnguy: цей коментар безглуздий. N - кількість предметів. Період.
user207421

2
Вам слід справді виправити відповідь. Отримання n- го елемента в масиві дорівнює O (n).
user1803551
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.