Должен ли я пытаться создать обратимое перечисление на Java или есть способ лучше?

Кажется, я сталкивался с этой проблемой много раз, и я хотел спросить сообщество, не лаяю ли я просто не на то дерево. В основном мой вопрос можно свести к следующему: если у меня есть перечисление (в Java), для которого важны значения, должен ли я вообще использовать перечисление или есть ли лучший способ, и если я использую перечисление, то что лучший способ отменить поиск?

Вот пример. Предположим, я хочу создать bean-компонент, представляющий определенный месяц и год. Я мог бы создать что-то вроде следующего:

public interface MonthAndYear {
    Month getMonth();
    void setMonth(Month month);
    int getYear();
    void setYear(int year);
}

Здесь я храню свой месяц как отдельный класс под названием Month, чтобы он был безопасным по типу. Если бы я просто поставил int, то любой мог бы передать в качестве числа 13, 5643 или -100, и не было бы возможности проверить это во время компиляции. Я ограничиваю их, чтобы поставить месяц, который я реализую как перечисление:

public enum Month {
    JANUARY,
    FEBRUARY,
    MARCH,
    APRIL,
    MAY,
    JUNE,
    JULY,
    AUGUST,
    SEPTEMBER,
    OCTOBER,
    NOVEMBER,
    DECEMBER;
}

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

public enum Month {
    JANUARY(1),
    FEBRUARY(2),
    MARCH(3),
    APRIL(4),
    MAY(5),
    JUNE(6),
    JULY(7),
    AUGUST(8),
    SEPTEMBER(9),
    OCTOBER(10),
    NOVEMBER(11),
    DECEMBER(12);

    private int monthNum;
    public Month(int monthNum) {
        this.monthNum = monthNum;
    }

    public getMonthNum() {
        return monthNum;
    }
}

Довольно просто, но что произойдет, если я захочу прочитать эти значения из базы данных, а также записать их? Я мог бы просто реализовать статическую функцию, используя оператор case в перечислении, который принимает int и возвращает соответствующий объект Month. Но это означает, что если бы я что-то изменил, мне пришлось бы изменить эту функцию, а также аргументы конструктора - изменить в двух местах. Итак, вот что я делал. Во-первых, я создал обратимый класс карты следующим образом:

public class ReversibleHashMap<K,V> extends java.util.HashMap<K,V> {
    private java.util.HashMap<V,K> reverseMap;

    public ReversibleHashMap() {
        super();
        reverseMap = new java.util.HashMap<V,K>();
    }

    @Override
    public V put(K k, V v) {
        reverseMap.put(v, k);
        return super.put(k,v);
    }

    public K reverseGet(V v) {
        return reverseMap.get(v);
    }
}

Затем я реализовал это в своем перечислении вместо метода конструктора:

public enum Month {
    JANUARY,
    FEBRUARY,
    MARCH,
    APRIL,
    MAY,
    JUNE,
    JULY,
    AUGUST,
    SEPTEMBER,
    OCTOBER,
    NOVEMBER,
    DECEMBER;

    private static ReversibleHashMap<java.lang.Integer,Month> monthNumMap;

    static {
        monthNumMap = new ReversibleHashMap<java.lang.Integer,Month>();
        monthNumMap.put(new java.lang.Integer(1),JANUARY);
        monthNumMap.put(new java.lang.Integer(2),FEBRUARY);
        monthNumMap.put(new java.lang.Integer(3),MARCH);
        monthNumMap.put(new java.lang.Integer(4),APRIL);
        monthNumMap.put(new java.lang.Integer(5),MAY);
        monthNumMap.put(new java.lang.Integer(6),JUNE);
        monthNumMap.put(new java.lang.Integer(7),JULY);
        monthNumMap.put(new java.lang.Integer(8),AUGUST);
        monthNumMap.put(new java.lang.Integer(9),SEPTEMBER);
        monthNumMap.put(new java.lang.Integer(10),OCTOBER);
        monthNumMap.put(new java.lang.Integer(11),NOVEMBER);
        monthNumMap.put(new java.lang.Integer(12),DECEMBER);
    }

    public int getMonthNum() {
        return monthNumMap.reverseGet(this);
    }

    public static Month fromInt(int monthNum) {
        return monthNumMap.get(new java.lang.Integer(monthNum));
    }
}

Теперь он делает все, что я хочу, но все равно выглядит неправильно. Люди предложили мне: «Если перечисление имеет значимое внутреннее значение, вы должны вместо этого использовать константы». Однако я не знаю, как этот подход обеспечит мне необходимую безопасность типов. Однако способ, которым я разработал, кажется слишком сложным. Есть ли какой-нибудь стандартный способ сделать это?

PS: Я знаю, что вероятность того, что правительство добавит новый месяц ... довольно маловероятна, но подумайте о более широкой картине - существует множество применений для перечислений.


person Adam Burley    schedule 17.10.2009    source источник
comment
Этот вопрос очень похож на stackoverflow .com / questions / 5021246 /, в котором также рассказывается, как связать каждое значение перечисления со значением int и как преобразовать в обоих направлениях.   -  person sleske    schedule 23.02.2011


Ответы (4)


Это очень распространенный шаблон, и он подходит для перечислений ... но его можно реализовать проще. Нет необходимости в «обратимой карте» - версия, которая принимает номер месяца в конструкторе, лучше подходит для перехода с Month на int. Но пойти другим путем тоже не так уж сложно:

public enum Month {
    JANUARY(1),
    FEBRUARY(2),
    MARCH(3),
    APRIL(4),
    MAY(5),
    JUNE(6),
    JULY(7),
    AUGUST(8),
    SEPTEMBER(9),
    OCTOBER(10),
    NOVEMBER(11),
    DECEMBER(12);

    private static final Map<Integer, Month> numberToMonthMap;

    private final int monthNum;

    static {
        numberToMonthMap = new HashMap<Integer, Month>();
        for (Month month : EnumSet.allOf(Month.class)) {
            numberToMonthMap.put(month.getMonthNum(), month);
        }
    }

    private Month(int monthNum) {
        this.monthNum = monthNum;
    }

    public int getMonthNum() {
        return monthNum;
    }

    public static Month fromMonthNum(int value) {
        Month ret = numberToMonthMap.get(value);
        if (ret == null) {
            throw new IllegalArgumentException(); // Or just return null
        }
        return ret;
    }
}

В конкретном случае чисел, которые, как вы знаете, будут идти от 1 до N, вы можете просто использовать массив - либо взяв Month.values()[value - 1], либо кэшируя возвращаемое значение Month.values(), чтобы предотвратить создание нового массива при каждом вызове. (И, как говорит Клетус, getMonthNum может просто вернуть ordinal() + 1.)

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

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

numberToMonthMap.put(monthNum, this);

в конструкторе и добавьте инициализатор статической переменной для numberToMonthMap, но это не сработает - вы сразу получите NullReferenceException, потому что вы попытаетесь поместить значение в карту, которой еще не было :(

person Jon Skeet    schedule 17.10.2009
comment
В данном конкретном случае это не добавляет особой ценности, но я использовал этот шаблон снова и снова. (Это боль, которую трудно извлечь во вспомогательный класс.) - person Jon Skeet; 17.10.2009
comment
Спасибо всем за комментарии. Джон - Мне больше всего понравилось ваше решение, и я не думал об использовании универсального цикла для заполнения карты. Приведенный мною пример довольно специфичен в том смысле, что внутренние представления являются целыми числами и расположены в указанном точном порядке. Однако, если я использовал символы или числа с пробелами между ними и т. Д., То я не могу использовать решение ordinal (), поэтому мне нужно использовать что-то похожее на то, что предлагает Джон. И я согласен с ним в том, что сложно создать базовый класс со встроенной функциональностью. Но я создам шаблон в своей IDE :) - person Adam Burley; 19.10.2009
comment
См. Также: deepjava.wordpress.com/2006/ 08.12 / - person finnw; 05.11.2009
comment
Пару раз я был с этим немного. Одним из примечательных моментов было то, что у меня был протокол связи, и коды сообщений были сопоставлены с порядковым номером. Он не сломался, но когда у меня были коды типа adduser в позиции 6 и remuser рядом с ним в позиции 7, а затем мне пришлось добавить новый, например moduser в позиции 20. Они действительно должны были быть сгруппированы логически, но я не мог этого сделать, потому что это привело бы к смещению многих других кодовых чисел. Я знаю, что месяцы не часто меняются, но многое из того, что происходит в реальном мире, меняется (независимо от того, сколько раз бизнес-пользователи говорят, что они этого не сделают !!) - person corsiKa; 17.09.2013

Есть способ сделать это проще. Каждое перечисление имеет ordinal() < / a> возвращает его номер (начиная с нуля).

public enum Month {
  JANUARY,
  FEBRUARY,
  MARCH,
  APRIL,
  MAY,
  JUNE,
  JULY,
  AUGUST,
  SEPTEMBER,
  OCTOBER,
  NOVEMBER,
  DECEMBER;

  public Month previous() {
    int prev = ordinal() - 1;
    if (prev < 0) {
      prev += values().length;
    }
    return values()[prev];
  }

  public Month next() {
    int next = ordinal() + 1;
    if (next >= values().length) {
      next = 0;
    }
    return values()[next];
  }
}

Что касается того, как сохранить это в базе данных, это зависит от того, какую структуру персистентности (если таковая имеется) вы используете. JPA / Hibernate имеют возможность отображать значения перечисления либо по номеру (порядковому номеру), либо по имени. Месяцы - это то, что вы, вероятно, можете считать неизменным, поэтому просто используйте порядковый номер. Чтобы получить конкретное значение:

Month.values()[ordinalNumber];
person cletus    schedule 17.10.2009
comment
Одним из недостатков вызова Month.values()[ordinalNumber] является то, что ему каждый раз нужно создавать копию массива. Было бы неплохо, если бы Java предоставляла неизменяемое свойство списка, такое как values(), которое могло бы просто возвращать ссылку на тот же список при каждом вызове. - person Jon Skeet; 17.10.2009
comment
Может быть, но разработка решения этой проблемы - если вы не делаете этого миллионы раз - бессмысленная микрооптимизация. - person cletus; 17.10.2009

Я, вероятно, далеко отстаю в ответе здесь, но я стараюсь реализовать его немного проще. Не забывайте, что у Enum есть метод values ​​().

public static Month parse(int num)
{
  for(Month value : values())
  {
    if (value.monthNum == num)
    {
      return value;
    }
  }
  return null; //or throw exception if you're of that mindset
}
person Beirti    schedule 27.09.2010

Вы не должны использовать ordinal () для такого рода вещей, для образца с месяцами он будет работать (потому что он не будет расширен), но одна из хороших вещей с перечислениями в java заключается в том, что они предназначены для возможности расширения не ломая вещи. Если вы начнете полагаться на ordinal (), все сломается, если вы добавите какое-то значение в середине.

Я бы сделал это, как предлагает Джон Скит (он написал это, когда я писал это), но для случаев, когда представление внутреннего числа находится в четко определенном диапазоне, скажем, от 0 до 20 (или что-то в этом роде), я бы, вероятно, не использовал HashMap и представил autoboxing int, а лучше использовать обычный массив (например, Month [12]), но оба подходят (позже Джон изменил свое сообщение, включив это предложение).

Изменить: для нескольких перечислений, где есть естественный порядок (например, отсортированные по месяцам), вероятно, безопасно использовать ordinal (). Проблемы, с которыми вы рискуете столкнуться, если будете упорствовать, появятся там, где кто-то может изменить порядок перечисления. Например, если: "enum {MALE, FEMALE}" становится "enum {UNKNOWN, FEMALE, MALE}", когда кто-то расширяет программу в будущем, не зная, что вы полагаетесь на порядковый номер.

Давал Джону +1 за то, что писал то же самое, что я только что писал.

person Fredrik    schedule 17.10.2009
comment
@cletus: это субъективно, и я не согласен. Это было причиной отрицательного голоса? Существует довольно полное объяснение того, почему вам никогда не следует полагаться на порядковый номер, если вы берете копию Effective Java (или прочтите пункт 31 здесь: books.google.se/) - person Fredrik; 17.10.2009
comment
@Fredrik: Я думаю, что кто-то проголосовал против всех трех ответов, хотя я не знаю почему. - person Jon Skeet; 17.10.2009