我应该尝试在 Java 中创建可逆枚举还是有更好的方法?
我似乎已经多次遇到这个问题,我想问问社区我是否只是找错了树。基本上我的问题可以归结为:如果我有一个枚举(在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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
这是一种非常常见的模式,对于枚举来说很好......但它可以更简单地实现。不需要“可逆映射” - 在构造函数中采用月份编号的版本更适合从
Month
到int
。但另一种方式也不是太难:在您知道数字将从 1 到 N 的特定情况下,您可以简单地使用一个数组 - 要么采用 Month.values()[value - 1] 或缓存
Month.values()
的返回值,以防止每次调用时创建新数组。 (正如 cletus 所说,getMonthNum
可能只返回ordinal() + 1
。)但是,在更一般的情况下,值得注意上述模式,其中值可能无序或分布稀疏。
需要注意的是,静态初始值设定项是在创建所有枚举值之后执行的。最好只
在构造函数中编写并为
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
toint
. But going the other way isn't too hard either: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 ofMonth.values()
to prevent creating a new array on every call. (And as cletus says,getMonthNum
could just returnordinal() + 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
in the constructor and add a static variable initializer for
numberToMonthMap
, but that doesn't work - you'd get aNullReferenceException
immediately, because you'd be trying to put the value into a map which didn't exist yet :(有一种更简单的方法可以做到这一点。每个枚举都有一个
ordinal()
方法返回它的数字(从零开始)。至于如何将其存储在数据库中,这取决于您使用的持久性框架(如果有)。 JPA/Hibernate 可以选择按数字(序数)或名称映射枚举值。您可能认为月份是不变的,因此只需使用序数即可。要获得特定值:
There's a way easier way of doing this. Every enum has an
ordinal()
method return it's number (starting from zero).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:
我在这里的答案可能远远落后于其他人,但我倾向于实现得更简单一些。不要忘记“Enum”有一个values()方法。
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.
您不应该将 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.