方便地在枚举和整数/字符串之间进行映射

122

当处理只能取有限值的变量/参数时,我尝试始终使用Java的enum,例如:

public enum BonusType {
  MONTHLY, YEARLY, ONE_OFF
}
只要我呆在自己的代码中,一切都很好。但是,我经常需要与使用普通int(或String)值具有相同目的的其他代码进行接口交互,或者我需要从/写入存储为数字或字符串的数据的数据库中读取/写入。 在这种情况下,我想要一种方便的方法将每个枚举值与整数相关联,以便我可以双向转换(换句话说,我需要一个“可逆的枚举”)。 从枚举到int很容易:
public enum BonusType {
  public final int id;

  BonusType(int id) {
    this.id = id;
  }
  MONTHLY(1), YEARLY(2), ONE_OFF(3);
}

那么我可以通过 BonusType x = MONTHLY; int id = x.id; 访问整数值。

然而,对于相反的操作,即从整数转换为枚举,我没有找到特别好的方法。理想情况下,希望能够实现类似以下的操作:

BonusType bt = BonusType.getById(2); 
我能想到的唯一解决方案是:
  • 在枚举类中编写一个查找方法,利用 BonusType.values() 填充一个“int -> 枚举”类型的映射表,然后将其缓存起来以供查找使用。虽然可行,但我需要在每个使用该枚举类型的地方都完全复制这个方法 :-(。
  • 将查找方法放入一个静态的工具类中。这样我只需要一个“查找”方法,但要让它可以在任意枚举类型中使用,需要试图使用反射技术进行操作。
对于这么简单的问题,这两种方法都似乎非常笨拙。

2
我喜欢Java枚举,但恰恰因为这个原因我讨厌它们!除了一个非常丑陋的缺陷之外,它们似乎总是完美的... - Chris Thompson
9
对于枚举类型转整型,你可以直接使用 ordinal() 方法。 - davin
1
你的id值是由你决定的(也就是说,你不能只使用.ordinal()),还是由外部力量决定的? - Paŭlo Ebermann
2
@davin:是的,如果有人重新排列枚举声明或删除中间的值,你的代码会立即崩溃。恐怕这不是一个健壮的解决方案 :-/. - sleske
1
@davin 尽可能避免使用"ordinal()",它在语言规范中。 - DPM
18个回答

345

枚举 → 整数

yourEnum.ordinal()

将 int 类型转为枚举类型

EnumType.values()[someInt]

将字符串转换为枚举

EnumType.valueOf(yourString)
枚举 → 字符串
yourEnum.name()
一个副注:
正如你所正确指出的,ordinal()可能在不同版本中是“不稳定”的。这正是为什么我总是将常量作为字符串存储在我的数据库中的确切原因。(实际上,当使用 MySql 时,我将它们存储为MySql枚举!)

2
+1 这是显然正确的答案。但请注意,valueOf方法还有一个单参数版本,只接受字符串类型的输入,并且只存在于您使用具体枚举类型时(例如 BonusType.valueOf("MONTHLY"))。 - Tim Bender
20
使用ordinal()方法似乎是一个有问题的解决方案,因为当枚举值的列表被重新排列或者某个值被删除时,它会失效。此外,仅当int值为0...n(我经常发现这种情况并不总是成立)时,这种方法才实用。 - sleske
4
@sleske,如果你开始删除常量,那么你将会与现有的持久化数据遇到麻烦。(已更新我的回答)。 - aioobe
4
如果您的值都是以0为索引,并按顺序声明,那么使用values()数组才有效。(我进行了测试并验证了这一点:如果您声明 FOO(0), BAR(2), BAZ(1);,那么values[1] == BAR and values[2] == BAZ,尽管传入的id不同。) - corsiKa
2
@glowcoder,当然,整数参数只是枚举对象中的一个字段。它与枚举对象关联的序数常量无关(它也可以是“double”)。 - aioobe
显示剩余7条评论

39

http://www.javaspecialists.co.za/archive/Issue113.html

该解决方案与您的类似,使用int值作为枚举定义的一部分。然后作者继续创建了一个基于泛型的查找实用程序:

public class ReverseEnumMap<V extends Enum<V> & EnumConverter> {
    private Map<Byte, V> map = new HashMap<Byte, V>();
    public ReverseEnumMap(Class<V> valueType) {
        for (V v : valueType.getEnumConstants()) {
            map.put(v.convert(), v);
        }
    }

    public V get(byte num) {
        return map.get(num);
    }
}

这个解决方案很不错,而且不需要“玩弄反射”,因为它是基于所有枚举类型隐式继承 Enum 接口的事实。


这不是使用序数吗?Sleske使用ID仅因为当枚举值被重新排序时,序数会发生变化。 - extraneon
不,它不使用序数。它依赖于明确定义的int值。该int值被用作映射键(由v.convert()返回)。 - Jeff
2
我真的很喜欢这个解决方案;看起来这是你可以得到的最普遍的。 - sleske
我的唯一建议是使用Number而不是Byte,因为我的后端值可能更大。 - Ivaylo Slavov
真正地看一下另一个回答,它有183个赞(截至发表此评论)。有时候Java似乎足够复杂,需要像这样的东西,但这不是那种情况! - James
3
真正地阅读问题。如果您正在处理一个遗留数据库或外部系统,其中定义了您不想在自己的代码中传播的整数,则这正是这些情况之一。序数是一种极其脆弱的方式来保存枚举值,并且在问题中提到的特定情况下毫无用处。 - Jeff

33

我在网上找到了这个非常有帮助且易于实现的内容。

这个解决方案并不是由我创建的。

http://www.ajaxonomy.com/2007/java/making-the-most-of-java-50-enum-tricks

public enum Status {
 WAITING(0),
 READY(1),
 SKIPPED(-1),
 COMPLETED(5);

 private static final Map<Integer,Status> lookup 
      = new HashMap<Integer,Status>();

 static {
      for(Status s : EnumSet.allOf(Status.class))
           lookup.put(s.getCode(), s);
 }

 private int code;

 private Status(int code) {
      this.code = code;
 }

 public int getCode() { return code; }

 public static Status get(int code) { 
      return lookup.get(code); 
 }

}


1
s/EnumSet.allOf(Status.class)/Status.values() - jelinson

12

看起来这个问题的答案已经过时,因为Java 8已经发布。

  1. 不要使用序数,因为如果将其持久化到JVM之外(如数据库),则序数是不稳定的。
  2. 使用键值对创建静态映射相对容易。

public enum AccessLevel {
  PRIVATE("private", 0),
  PUBLIC("public", 1),
  DEFAULT("default", 2);

  AccessLevel(final String name, final int value) {
    this.name = name;
    this.value = value;
  }

  private final String name;
  private final int value;

  public String getName() {
    return name;
  }

  public int getValue() {
    return value;
  }

  static final Map<String, AccessLevel> names = Arrays.stream(AccessLevel.values())
      .collect(Collectors.toMap(AccessLevel::getName, Function.identity()));
  static final Map<Integer, AccessLevel> values = Arrays.stream(AccessLevel.values())
      .collect(Collectors.toMap(AccessLevel::getValue, Function.identity()));

  public static AccessLevel fromName(final String name) {
    return names.get(name);
  }

  public static AccessLevel fromValue(final int value) {
    return values.get(value);
  }
}

Collectors.toMap() 的第二个参数应该是 Functions.identity() 而不是 null,对吗? - Adam Michalik
是的,我采用了一个帮助类,它与Guava一起使用,将null转换为identity。 - John Meyer
这是对Java 8新特性的巧妙运用。然而,这仍意味着代码必须在每个枚举中重复 - 而我的问题是如何避免这种(结构上)重复的样板文件。 - sleske

5

org.apache.commons.lang.enums.ValuedEnum;

为了避免编写大量样板代码或为每个枚举类型重复编写代码,我使用了Apache Commons Lang的ValuedEnum

定义:

public class NRPEPacketType extends ValuedEnum {    
    public static final NRPEPacketType TYPE_QUERY = new NRPEPacketType( "TYPE_QUERY", 1);
    public static final NRPEPacketType TYPE_RESPONSE = new NRPEPacketType( "TYPE_RESPONSE", 2);

    protected NRPEPacketType(String name, int value) {
        super(name, value);
    }
}

使用方法:

整数 -> 带值枚举:

NRPEPacketType packetType = 
 (NRPEPacketType) EnumUtils.getEnum(NRPEPacketType.class, 1);

好主意,我没有意识到这个存在。谢谢分享! - Keith P

3

您可以尝试使用类似以下的方式:

interface EnumWithId {
    public int getId();

}


enum Foo implements EnumWithId {

   ...
}

那将减少您的实用程序类中的反射需求。

你能举个例子说明如何使用这段代码片段吗? - IgorGanapolsky

3
在这段代码中,为了进行持久和强烈的搜索,需要使用内存或进程,并且我选择了内存,将转换器数组作为索引。希望这对你有所帮助。
public enum Test{ 
VALUE_ONE(101, "Im value one"),
VALUE_TWO(215, "Im value two");
private final int number;
private final byte[] desc;

private final static int[] converter = new int[216];
static{
    Test[] st = values();
    for(int i=0;i<st.length;i++){
        cv[st[i].number]=i;
    }
}

Test(int value, byte[] description) {
    this.number = value;
    this.desc = description;
}   
public int value() {
    return this.number;
}
public byte[] description(){
    return this.desc;
}

public static String description(int value) {
    return values()[converter[rps]].desc;
}

public static Test fromValue(int value){
return values()[converter[rps]];
}
}

3

一个使用反向枚举的非常清晰的示例

步骤1 定义一个 interface 枚举转换器

public interface EnumConverter <E extends Enum<E> & EnumConverter<E>> {
    public String convert();
    E convert(String pKey);
}

Step 2

Create a class name ReverseEnumMap

import java.util.HashMap;
import java.util.Map;

public class ReverseEnumMap<V extends Enum<V> & EnumConverter<V>> {
    private Map<String, V> map = new HashMap<String, V>();

    public ReverseEnumMap(Class<V> valueType) {
        for (V v : valueType.getEnumConstants()) {
            map.put(v.convert(), v);
        }
    }

    public V get(String pKey) {
        return map.get(pKey);
    }
}

步骤三

进入你的Enum类,并使用EnumConverter<ContentType>实现它,当然还要覆盖接口方法。你还需要初始化一个静态的ReverseEnumMap

public enum ContentType implements EnumConverter<ContentType> {
    VIDEO("Video"), GAME("Game"), TEST("Test"), IMAGE("Image");

    private static ReverseEnumMap<ContentType> map = new ReverseEnumMap<ContentType>(ContentType.class);

    private final String mName;

    ContentType(String pName) {
        this.mName = pName;
    }

    String value() {
        return this.mName;
    }

    @Override
    public String convert() {
        return this.mName;
    }

    @Override
    public ContentType convert(String pKey) {
        return map.get(pKey);
    }
}

第四步

现在创建一个名为 Communication 的类文件,并调用它的新方法将 Enum 转换为 StringString 转换为 Enum。我只是为了解释而放置了主要方法。

public class Communication<E extends Enum<E> & EnumConverter<E>> {
    private final E enumSample;

    public Communication(E enumSample) {
        this.enumSample = enumSample;
    }

    public String resolveEnumToStringValue(E e) {
        return e.convert();
    }

    public E resolveStringEnumConstant(String pName) {
        return enumSample.convert(pName);
    }

//Should not put main method here... just for explanation purpose. 
    public static void main(String... are) {
        Communication<ContentType> comm = new Communication<ContentType>(ContentType.GAME);
        comm.resolveEnumToStringValue(ContentType.GAME); //return Game
        comm.resolveStringEnumConstant("Game"); //return GAME (Enum)
    }
}

Click for for complete explanation


1
这个我真的非常喜欢 - 我已经寻找一个稳定的解决方案来解决这个问题有一段时间了。我唯一做出的改变是将 ContentType convert(String pKey) 设为静态方法,这样就不需要 Communication 类了,而且更符合我的口味。+1 - Chris Mantle

2
使用接口来掌控它。
public interface SleskeEnum {
    int id();

    SleskeEnum[] getValues();

}

public enum BonusType implements SleskeEnum {


  MONTHLY(1), YEARLY(2), ONE_OFF(3);

  public final int id;

  BonusType(int id) {
    this.id = id;
  }

  public SleskeEnum[] getValues() {
    return values();
  }

  public int id() { return id; }


}

public class Utils {

  public static SleskeEnum getById(SleskeEnum type, int id) {
      for(SleskeEnum t : type.getValues())
          if(t.id() == id) return t;
      throw new IllegalArgumentException("BonusType does not accept id " + id);
  }

  public static void main(String[] args) {

      BonusType shouldBeMonthly = (BonusType)getById(BonusType.MONTHLY,1);
      System.out.println(shouldBeMonthly == BonusType.MONTHLY);

      BonusType shouldBeMonthly2 = (BonusType)getById(BonusType.MONTHLY,1);
      System.out.println(shouldBeMonthly2 == BonusType.YEARLY);

      BonusType shouldBeYearly = (BonusType)getById(BonusType.MONTHLY,2);
      System.out.println(shouldBeYearly  == BonusType.YEARLY);

      BonusType shouldBeOneOff = (BonusType)getById(BonusType.MONTHLY,3);
      System.out.println(shouldBeOneOff == BonusType.ONE_OFF);

      BonusType shouldException = (BonusType)getById(BonusType.MONTHLY,4);
  }
}

结果如下:

C:\Documents and Settings\user\My Documents>java Utils
true
false
true
true
Exception in thread "main" java.lang.IllegalArgumentException: BonusType does not accept id 4
        at Utils.getById(Utils.java:6)
        at Utils.main(Utils.java:23)

C:\Documents and Settings\user\My Documents>

1
就像Turd Ferguson的回答一样,那是我想要避免/改进的不优雅的解决方案... - sleske
我通常在 static{ } 块中创建反向映射以避免每次按 ID 请求值时都循环遍历 values()。我还通常调用方法 valueOf(int) 使其看起来有些类似于已经存在于字符串中的 valueOf(String) 方法(也是 OP 问题的一部分)。有点像《Effective Java》中的第33项:http://tinyurl.com/4ffvc38 - Fredrik
@Sleske 更新了更精细的解决方案。@Fredrik 很有趣,尽管我怀疑迭代不会成为一个重要问题。 - corsiKa
@glowcoder 嗯,不需要迭代超过一次意味着无论你每秒执行一千次还是只调用两次,都不会有太大的问题。 - Fredrik
@glowcoder 我同意应该避免过早的优化,但这并不意味着你不应该将正常的良好工程实践应用于你所做的事情,对吧?这并不像这样的优化会使事情变得混乱,而且它确实更多地是一种被广泛接受的最佳实践,而不是可能不需要的优化。 - Fredrik
显示剩余2条评论

2

.ordinal()values()[i]都不稳定,因为它们依赖于枚举的顺序。因此,如果您更改枚举的顺序或添加/删除枚举,则程序会崩溃。

这里有一种简单而有效的方法来映射枚举和整数之间的关系。

public enum Action {
    ROTATE_RIGHT(0), ROTATE_LEFT(1), RIGHT(2), LEFT(3), UP(4), DOWN(5);

    public final int id;
    Action(int id) {
        this.id = id;
    }

    public static Action get(int id){
        for (Action a: Action.values()) {
            if (a.id == id)
                return a;
        }
        throw new IllegalArgumentException("Invalid id");
    }
}

将其应用于字符串上不应该很难。


是的,我意识到我可以这样做 - 或者更好的是,使用映射进行反向查找,而不是遍历所有值。我在我的问题中提到了这一点,并且我还提到我正在寻找更好的解决方案,以避免在每个枚举中都有样板代码。 - sleske

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接