我应该尝试在Java中创建可逆枚举还是有更好的方法?

时间:2009-10-17 18:58:25

标签: java enums idiomatic

我好像多次遇到过这个问题,我想问一下社区是否只是在咆哮错误的树。基本上我的问题可以归结为:如果我有一个值很重要的枚举(在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 个答案:

答案 0 :(得分:11)

这是一种非常常见的模式,它对于枚举很好......但它可以更简单地实现。不需要“可逆映射” - 从构造函数中获取月份数的版本从Monthint更好。但是走另一条路也不是太难:

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,因为你试图将值放入尚不存在的地图:(

答案 1 :(得分: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];

答案 2 :(得分: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
}

答案 3 :(得分:1)

你不应该对这类东西使用ordinal(),因为它可以使用几个月的样本(因为它不会被扩展)但是java中枚举的一个好处是它们被设计为可以扩展而不会破坏事物。如果你开始依赖ordinal(),如果你在中间添加一些值,事情就会破裂。

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

编辑:对于有自然顺序的少数枚举(如已排序的月份),ordinal()可能是安全的。如果你坚持它可能会遇到的问题会出现在有人可能会改变枚举顺序的事情上。如果: “enum {MALE,FEMALE}”变成了一个“enum {UNKNOWN,FEMALE,MALE}”,当有人在未来扩展该程序而不知道你依赖序数时。

给Jon一个+1来写同样的东西我只是写作。