9

我似乎多次遇到过这个问题,我想问问社区我是否只是在找错树。基本上我的问题可以归结为:如果我有一个值很重要的枚举(在 Java 中),我应该使用枚举还是有更好的方法,如果我确实使用枚举,那么是反转查找的最佳方法吗?

这是一个例子。假设我想创建一个代表特定月份和年份的 bean。我可能会创建如下内容:

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

在这里,我将我的月份存储为一个名为 Month 的单独类,以便它是类型安全的。如果我只是输入 int,那么任何人都可以将 13 或 5,643 或 -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:我知道政府增加新月份的可能性......不太可能,但请考虑更大的图景 - 枚举有很多用途。

4

4 回答 4

11

这是一种非常常见的模式,对于枚举来说很好……但它可以更简单地实现。不需要“可逆映射” - 在构造函数中采用月份编号的版本更适合 fromMonthint. 但走另一条路也不是太难:

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()以防止在每次调用时创建一个新数组。(正如 cletus 所说,getMonthNum可以返回ordinal() + 1。)

但是,在值可能无序或稀疏分布的更一般情况下,值得注意上述模式。

重要的是要注意静态初始化程序是在创建所有枚举值之后执行的。写就好了

numberToMonthMap.put(monthNum, this);

在构造函数中并添加一个静态变量初始化器numberToMonthMap,但这不起作用 - 你会NullReferenceException立即得到一个,因为你会尝试将值放入一个还不存在的映射中:(

于 2009-10-17T19:10:05.723 回答
4

有一种更简单的方法可以做到这一点。每个枚举都有一个ordinal()方法返回它的数字(从零开始)。

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];
于 2009-10-17T19:06:34.560 回答
3

我在这里的答案可能远远落后于包装,但我倾向于实现它更简单一些。不要忘记 '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
}
于 2010-09-27T14:51:19.507 回答
1

您不应该将 ordinal() 用于这种事情,因为它可以工作几个月的样本(因为它不会被扩展)但是 java 中枚举的好处之一是它们被设计为可以扩展不破坏东西。如果您开始依赖 ordinal() 如果您在中间添加一些值,事情将会中断。

我会像 Jon Skeet 建议的那样做(他是在我写这篇文章时写的),但是对于内部数字表示在明确定义的范围内的情况,比如 0 到 20(或其他),我可能不会使用 HashMap 并引入int 的自动装箱,而是使用普通数组(如 Month[12]),但两者都很好(Jon 后来更改了他的帖子以包含此建议)。

编辑:对于有自然顺序(如排序月份)的少数枚举,ordinal() 可能可以安全使用。如果您坚持下去,您可能会遇到的问题将出现在有人可能会更改枚举顺序的事情上。就像:当将来有人扩展程序而不知道您依赖序数时,“enum { MALE, FEMALE }”变成了“enum {UNKNOWN, FEMALE, MALE}”。

给 Jon 一个 +1 写我刚刚写的一样的东西。

于 2009-10-17T19:14:44.580 回答