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

发布于 2024-08-08 06:36:13 字数 3218 浏览 1 评论 0原文

我似乎已经多次遇到这个问题,我想问问社区我是否只是找错了树。基本上我的问题可以归结为:如果我有一个枚举(在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:我知道政府增加新月份的可能性......相当不可能,但考虑一下更大的前景 - 枚举有很多用途。

I seem to have faced this problem many times and I wanted to ask the community whether I am just barking up the wrong tree. Basically my question can be distilled down to this: if I have an enum (in Java) for which the values are important, should I be using an enum at all or is there a better way, and if I do use an enum then what is the best way to reverse the lookup?

Here's an example. Suppose I want to create a bean representing a specific month and year. I might create something like the following:

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

Here I'm storing my month as a separate class called Month, so that it is type-safe. If I just put int, then anyone could pass in 13 or 5,643 or -100 as a number, and there would be no way to check for that at compile-time. I'm restricting them to put a month which I'll implement as an enum:

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

Now suppose that I have some backend database I want to write to, which only accepts the integer form. Well the standard way to do this seems to be:

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

Fairly straightforward, but what happens if I want to read these values from the database as well as writing them? I could just implement a static function using a case statement within the enum that takes an int and returns the respective Month object. But this means if I changed anything, then I would have to change this function as well as the constructor arguments - change in two places. So here's what I've been doing. First off I created a reversible map class as follows:

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

Then I implemented this within my enum instead of the constructor method:

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

Now this does everything I want it to, but it still looks wrong. People have suggested to me "if the enumeration has a meaningful internal value, you should be using constants instead". However, I don't know how that approach would give me the type-safety I am looking for. The way I've developed does seem overly complicated though. Is there some standard way to do this kind of thing?

PS: I know that the likelihood of the government adding a new month is...fairly unlikely, but think of the bigger picture - there are plenty of uses for enums.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

や莫失莫忘 2024-08-15 06:36:13

这是一种非常常见的模式,对于枚举来说很好......但它可以更简单地实现。不需要“可逆映射” - 在构造函数中采用月份编号的版本更适合从 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() 的返回值,以防止每次调用时创建新数组。 (正如 cletus 所说,getMonthNum 可能只返回 ordinal() + 1。)

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

需要注意的是,静态初始值设定项是在创建所有枚举值之后执行的。最好只

numberToMonthMap.put(monthNum, this);

在构造函数中编写并为 numberToMonthMap 添加静态变量初始值设定项,但这不起作用 - 您会立即得到一个 NullReferenceException ,因为您会尝试将值放入尚不存在的地图中:(

This is a very common pattern, and it's fine for enums... but it can be implemented more simply. There's no need for a "reversible map" - the version which takes the month number in the constructor is better for going from Month to int. But going the other way isn't too hard either:

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

In the specific case of numbers which you know will go from 1 to N, you could simply use an array - either taking Month.values()[value - 1] or caching the return value of Month.values() to prevent creating a new array on every call. (And as cletus says, getMonthNum could just return ordinal() + 1.)

However, it's worth being aware of the above pattern in the more general case where the values may be out of order, or sparsely distributed.

It's important to note that the static initializer is executed after all the enum values are created. It would be nice to just write

numberToMonthMap.put(monthNum, this);

in the constructor and add a static variable initializer for numberToMonthMap, but that doesn't work - you'd get a NullReferenceException immediately, because you'd be trying to put the value into a map which didn't exist yet :(

陌路黄昏 2024-08-15 06:36:13

有一种更简单的方法可以做到这一点。每个枚举都有一个 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];

There's a way easier way of doing this. Every enum has an ordinal() method return it's number (starting from zero).

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

As for how to store this in a database, it depends on what persistence framework (if any) you're using. JPA/Hibernate have the option of mapping enum values by either number (ordinal) or name. Months are something you can probably take as non-changing so just use the ordinal. To get a specific value:

Month.values()[ordinalNumber];
乙白 2024-08-15 06:36:13

我在这里的答案可能远远落后于其他人,但我倾向于实现得更简单一些。不要忘记“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
}

I'm probably far behind the pack in an answer here but I tend to implement it a little bit simpler. Don't forget that 'Enum' has a values() method.

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
}
绅刃 2024-08-15 06:36:13

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

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

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

给乔恩+1,因为他写的和我刚刚写的一样。

You shouldn't use ordinal() for this kind of thing, for the sample with months it would work (because it will not be extended) but one of the good things with enums in java is that they are designed to be possible to extend without breaking things. If you start relying on ordinal() things will break if you add some value in the middle.

I would do it like Jon Skeet suggests (he wrote it while I was writing this) but for cases where the internal number representation is in a well defined range of say 0 to 20 (or something) I would probably not use a HashMap and introduce autoboxing of the int but rather use an ordinary array (like Month[12]) but both are fine (Jon later changed his post to include this suggestion).

Edit: For the few enums where there is a natural order (like sorted months) ordinal() is probably safe to use. The problems you risk running into if you persist it will appear for things where someone might change the order of the enum. Like if:
the "enum { MALE, FEMALE }" becomes an "enum {UNKNOWN, FEMALE, MALE}" when someone extends the program in the future not knowing that you rely on ordinal.

Giving Jon a +1 for writing the same I was just writing.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文