我有两个类,一个具有int值和String值的enum,以及一个常规类。
枚举类:public enum LettersEnum{
A(0, “Letter A”),
B(1, “Letter B”),
C(2, “Letter C”);
private final id;
private final letter;
LettersEnum(int id, String letter) {
this.id = id;
this.letter = letter;
}
public int getId() {
return this.id;
}
public String getLetter() {
return this.letter;
}
}
常规类:
public class LettersClass {
private LettersEnum letter = LettersEnum.A;
public void setLetter(LettersEnum letter) {
this.letter = letter;
}
public LettersEnum getLetter() {
return this.letter;
}
}
我有一个视频游戏,它有客户端和服务器端。两端的A
为默认字母。如果我在客户端将A
更改为B
(反之亦然),它将不会改变服务器端的字母,除非我运行一个数据包,将整个枚举编码为单个数据类型,并在服务器端解码回枚举。
问题是,我该怎么做?我猜toString()
,但是我如何将每个值转换回正确的数据类型?
这被称为序列化:将任何随机的信息单元(在本例中是枚举值之一)转换为可序列化的数据(通常是字节,但取决于底层传输/存储机制,有时必须使用字符)。
做这项工作有很多方法。特别是对于枚举,一种特别明显的方法是只序列化序数或名称。或者,您可以处理您自己的序列化'key'。序数
'序数'只是枚举def中的索引位置。在您的例子中,A
的序数是0,仅仅因为A
是您列出的第一个。(不是因为id
字段包含0,这只是一个巧合)。
通过序数序列化的一个主要问题是,如果重新排序枚举值或在除末尾以外的任何位置插入新值,则一切都会中断。如果你这样做,你应该在你的代码中添加一个注释,它永远不会打乱现有的值,你只能在列表的末尾添加值。
序数的优点是它们的发送和接收都很简单,而且效率很高:它只是一个int;32位。
将枚举值转换为序数
LettersEnum letter = LettersEnum.A;
int o = letter.ordinal();
assert o == 0;
将序号转换回enum
int o = 0;
LettersEnum letter = LettersEnum.values()[o];
assert letter == LettersEnum.A;
values()
方法是'魔术',所有的枚举都有这些,你不需要写它。
名称还可以选择通过枚举的名称进行序列化;本例中,以A
为例。这确实允许您在代码中重新组织枚举,但如果您这样做,则无法重命名任何枚举。
第二个问题是序列化字符串稍微复杂一些;为了"安全"地做到这一点,你需要首先将字符串转换为字节(因为绝大多数传输/存储机制以字节为单位工作),并且你需要确保在这样做时使用UTF-8编码,否则你会遇到问题,例如开始添加字母ß,这是一个有效的java标识符(enum Foo { A, B, ß; }
是有效的java!)。
将枚举值转换为名称,然后将名称转换为字节
LettersEnum letter = LettersEnum.A;
String name = letter.name();
assert name.equals("A");
byte[] data = name.getBytes(StandardCharsets.UTF_8);
assert data.length == 1 && data[0] == 'A';
将字节转换为字符串,然后转换为enum
byte[] data = new byte[] {'A'};
String name = new String(data, StandardCharsets.UTF_8);
LettersEnum v = LettersEnum.valueOf(name);
assert v == LettersEnum.A;
name()
和valueOf()
方法都是存在的,你不需要写它们。
你自己的ID
这两个以上策略做默默的"链接"方面,java程序员通常不认为有任何影响程序的运行时行为(你写的顺序为ordinal-based序列化字段,和基于名称的名称值),所以你或者开发团队的其他成员可能重命名事物和没有意识到这意味着什么"保存"或"发送"旧的代码现在可以不再被检索/收到的新代码。一个简单的方法是使用自己的身份证。这也可以是明智的,如果你只是建模一个现有的概念(比如,一些第三方API已经定义了一堆命令,有一个整数值,但没有人喜欢.sendCmd(182, ..)
,.sendCmd(Command.LIST_USERS)
是更好的。)
你已经有这个了——一个id
字段。
在这两个方向上都没有现成的支持;你得自己写。但这并不是特别复杂:
public enum LettersEnum{
A(0, “A”),
B(1, “B”),
C(2, “C”);
private final id;
private final letter;
LettersEnum(int id, String letter) {
this.id = id;
this.letter = letter;
}
public int getId() {
return this.id;
}
public String getLetter() {
return this.letter;
}
private static final Map<Integer, LettersEnum> ID_TO_LETTER;
static {
var map = new HashMap<Integer, LettersEnum>();
for (LettersEnum letter : values()) map.put(letter.getId(), letter);
ID_TO_LETTER = Collections.unmodifiableMap(map);
}
public static LettersEnum ofId(int id) {
LettersEnum v = ID_TO_LETTER.get(id);
if (v != null) return v;
throw new IllegalArgumentException(id + " is not a known LettersEnum id");
}
}
和使用:
LettersEnum letter = LettersEnum.A;
int i = letter.getId();
// store i someplace...
// retrieve o someplace...
int o = 0;
LettersEnum letter = LettersEnum.ofId(o);
assert letter == LettersEnum.A;
从Letter中获取枚举需要类似于enum的方法:
public static LettersEnum getLettersEnum(String s){
for (LettersEnum l:values()){
if (l.letter.equals(s)){
return l;
}
}
throw new IllegalArgumentException ();
};
如果该字母始终与枚举的名称匹配,则可以去掉它,并始终返回名称而不是方法getLetter
。这样循环就没有必要了,因为你可以直接调用valueOf(String name)
而不是getLettersEnum(String s)
顺便说一句。您的id和字母字段缺少类型。