EnumSet 序列化
EnumSet serialization
我刚刚花了几个小时调试我的应用程序,我相信我偶然发现了一个(另一个 o_O)Java 错误...嗅探...我希望不是,因为这会很伤心:(
我正在执行以下操作:
- 创建带有一些标志的 EnumSet
mask
- 序列化它(使用
ObjectOutputStream.writeObject(mask)
)
- 正在清除和设置
mask
中的一些其他标志
- 再次序列化
预期结果:第二个序列化对象与第一个不同(反映了实例的变化)
得到的结果:第二个序列化对象是第一个的精确副本
代码:
enum MyEnum {
ONE, TWO
}
@Test
public void testEnumSetSerialize() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(bos);
EnumSet<MyEnum> mask = EnumSet.noneOf(MyEnum.class);
mask.add(MyEnum.ONE);
mask.add(MyEnum.TWO);
System.out.println("First serialization: " + mask);
stream.writeObject(mask);
mask.clear();
System.out.println("Second serialization: " + mask);
stream.writeObject(mask);
stream.close();
ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("First deserialized " + istream.readObject());
System.out.println("Second deserialized " + istream.readObject());
}
它打印:
First serialization: [ONE, TWO]
Second serialization: []
First deserialized [ONE, TWO]
Second deserialized [ONE, TWO] <<<<<< Expecting [] here!!!!
我使用 EnumSet
不正确吗?我是否必须每次都创建一个新实例而不是清除它?
感谢您的意见!
**** 更新 ****
我最初的想法是使用 EnumSet
作为掩码来指示在随后的消息中哪些字段将出现或不出现,因此是一种带宽和 cpu 使用优化。大错特错!!! EnumSet
需要很长时间才能序列化,每个实例需要 30 (!!!) 字节! space 经济就这么多了:)
简而言之,虽然 ObjectOutputStream
对于基本类型来说非常快(正如我已经在此处的一个小测试中发现的那样:),但它非常懒散且效率低下(尤其是小) 对象...
所以我通过使我自己的 EnumSet 由 int 支持来解决这个问题,serializing/deserializing 直接使用 int(而不是对象)。
static class MyEnumSet<T extends Enum<T>> {
private int mask = 0;
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
return mask == ((MyEnumSet<?>) o).mask;
}
@Override
public int hashCode() {
return mask;
}
private MyEnumSet(int mask) {
this.mask = mask;
}
public static <T extends Enum<T>> MyEnumSet<T> noneOf(Class<T> clz) {
return new MyEnumSet<T>(0);
}
public static <T extends Enum<T>> MyEnumSet<T> fromMask(Class<T> clz, int mask) {
return new MyEnumSet<T>(mask);
}
public int mask() {
return mask;
}
public MyEnumSet<T> add(T flag) {
mask = mask | (1 << flag.ordinal());
return this;
}
public void clear() {
mask = 0;
}
}
private final int N = 1000000;
@Test
public void testSerializeMyEnumSet() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream(N * 100);
ObjectOutputStream out = new ObjectOutputStream(bos);
List<MyEnumSet<TestEnum>> masks = Lists.newArrayList();
Random r = new Random(132477584521L);
for (int i = 0; i < N; i++) {
MyEnumSet<TestEnum> mask = MyEnumSet.noneOf(TestEnum.class);
for (TestEnum f : TestEnum.values()) {
if (r.nextBoolean()) {
mask.add(f);
}
}
masks.add(mask);
}
logger.info("Serializing " + N + " myEnumSets");
long tic = TicToc.tic();
for (MyEnumSet<TestEnum> mask : masks) {
out.writeInt(mask.mask());
}
TicToc.toc(tic);
out.close();
logger.info("Size: " + bos.size() + " (" + (bos.size() / N) + "b per object)");
logger.info("Deserializing " + N + " myEnumSets");
MyEnumSet<TestEnum>[] deserialized = new MyEnumSet[masks.size()];
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
tic = TicToc.tic();
for (int i = 0; i < deserialized.length; i++) {
deserialized[i] = MyEnumSet.fromMask(TestEnum.class, in.readInt());
}
TicToc.toc(tic);
Assert.assertArrayEquals(masks.toArray(), deserialized);
}
它在序列化过程中快了大约 130 倍,在反序列化过程中快了 25 倍...
我的枚举集:
17/12/15 11:59:31 INFO - Serializing 1000000 myEnumSets
17/12/15 11:59:31 INFO - Elapsed time is 0.019 s
17/12/15 11:59:31 INFO - Size: 4019539 (4b per object)
17/12/15 11:59:31 INFO - Deserializing 1000000 myEnumSets
17/12/15 11:59:31 INFO - Elapsed time is 0.021 s
常规枚举集:
17/12/15 11:59:48 INFO - Serializing 1000000 enumSets
17/12/15 11:59:51 INFO - Elapsed time is 2.506 s
17/12/15 11:59:51 INFO - Size: 30691553 (30b per object)
17/12/15 11:59:51 INFO - Deserializing 1000000 enumSets
17/12/15 11:59:51 INFO - Elapsed time is 0.489 s
虽然它并不安全。例如,它不适用于超过 32 个条目的枚举。
如何确保枚举在创建 MyEnumSet 时少于 32 个值?
ObjectOutputStream 序列化对对象的引用,第一次发送对象时,实际对象。如果您修改一个对象并再次发送它,ObjectOutputStream 所做的就是再次将 reference 发送到该对象。
这会产生一些后果
- 如果您修改对象,您将看不到这些修改
- 它必须在两端保留对每个发送过的对象的引用。这可能是一个微妙的内存泄漏。
- 这样做的原因是您可以序列化对象图而不是树。例如A 指向 B,B 指向 A。您只想发送一次 A。
解决这个问题并取回一些内存的方法是在每个完整的对象之后调用 reset()。例如在致电 flush()
之前
Reset will disregard the state of any objects already written to the stream. The state is reset to be the same as a new ObjectOutputStream. The current point in the stream is marked as reset so the corresponding ObjectInputStream will be reset at the same point. Objects previously written to the stream will not be referred to as already being in the stream. They will be written to the stream again.
另一种方法是使用 writeUnshared,但是这对顶级对象应用了浅层非共享性。在 EnumSet
的情况下它会有所不同,但是它包装的 Enum[]
仍然是共享的 o_O
Writes an "unshared" object to the ObjectOutputStream. This method is identical to writeObject, except that it always writes the given object as a new, unique object in the stream (as opposed to a back-reference pointing to a previously serialized instance).
简而言之,不,这不是错误,而是预期的行为。
我刚刚花了几个小时调试我的应用程序,我相信我偶然发现了一个(另一个 o_O)Java 错误...嗅探...我希望不是,因为这会很伤心:(
我正在执行以下操作:
- 创建带有一些标志的 EnumSet
mask
- 序列化它(使用
ObjectOutputStream.writeObject(mask)
) - 正在清除和设置
mask
中的一些其他标志
- 再次序列化
预期结果:第二个序列化对象与第一个不同(反映了实例的变化)
得到的结果:第二个序列化对象是第一个的精确副本
代码:
enum MyEnum {
ONE, TWO
}
@Test
public void testEnumSetSerialize() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(bos);
EnumSet<MyEnum> mask = EnumSet.noneOf(MyEnum.class);
mask.add(MyEnum.ONE);
mask.add(MyEnum.TWO);
System.out.println("First serialization: " + mask);
stream.writeObject(mask);
mask.clear();
System.out.println("Second serialization: " + mask);
stream.writeObject(mask);
stream.close();
ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("First deserialized " + istream.readObject());
System.out.println("Second deserialized " + istream.readObject());
}
它打印:
First serialization: [ONE, TWO] Second serialization: [] First deserialized [ONE, TWO] Second deserialized [ONE, TWO] <<<<<< Expecting [] here!!!!
我使用 EnumSet
不正确吗?我是否必须每次都创建一个新实例而不是清除它?
感谢您的意见!
**** 更新 ****
我最初的想法是使用 EnumSet
作为掩码来指示在随后的消息中哪些字段将出现或不出现,因此是一种带宽和 cpu 使用优化。大错特错!!! EnumSet
需要很长时间才能序列化,每个实例需要 30 (!!!) 字节! space 经济就这么多了:)
简而言之,虽然 ObjectOutputStream
对于基本类型来说非常快(正如我已经在此处的一个小测试中发现的那样:),但它非常懒散且效率低下(尤其是小) 对象...
所以我通过使我自己的 EnumSet 由 int 支持来解决这个问题,serializing/deserializing 直接使用 int(而不是对象)。
static class MyEnumSet<T extends Enum<T>> {
private int mask = 0;
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
return mask == ((MyEnumSet<?>) o).mask;
}
@Override
public int hashCode() {
return mask;
}
private MyEnumSet(int mask) {
this.mask = mask;
}
public static <T extends Enum<T>> MyEnumSet<T> noneOf(Class<T> clz) {
return new MyEnumSet<T>(0);
}
public static <T extends Enum<T>> MyEnumSet<T> fromMask(Class<T> clz, int mask) {
return new MyEnumSet<T>(mask);
}
public int mask() {
return mask;
}
public MyEnumSet<T> add(T flag) {
mask = mask | (1 << flag.ordinal());
return this;
}
public void clear() {
mask = 0;
}
}
private final int N = 1000000;
@Test
public void testSerializeMyEnumSet() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream(N * 100);
ObjectOutputStream out = new ObjectOutputStream(bos);
List<MyEnumSet<TestEnum>> masks = Lists.newArrayList();
Random r = new Random(132477584521L);
for (int i = 0; i < N; i++) {
MyEnumSet<TestEnum> mask = MyEnumSet.noneOf(TestEnum.class);
for (TestEnum f : TestEnum.values()) {
if (r.nextBoolean()) {
mask.add(f);
}
}
masks.add(mask);
}
logger.info("Serializing " + N + " myEnumSets");
long tic = TicToc.tic();
for (MyEnumSet<TestEnum> mask : masks) {
out.writeInt(mask.mask());
}
TicToc.toc(tic);
out.close();
logger.info("Size: " + bos.size() + " (" + (bos.size() / N) + "b per object)");
logger.info("Deserializing " + N + " myEnumSets");
MyEnumSet<TestEnum>[] deserialized = new MyEnumSet[masks.size()];
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
tic = TicToc.tic();
for (int i = 0; i < deserialized.length; i++) {
deserialized[i] = MyEnumSet.fromMask(TestEnum.class, in.readInt());
}
TicToc.toc(tic);
Assert.assertArrayEquals(masks.toArray(), deserialized);
}
它在序列化过程中快了大约 130 倍,在反序列化过程中快了 25 倍...
我的枚举集:
17/12/15 11:59:31 INFO - Serializing 1000000 myEnumSets 17/12/15 11:59:31 INFO - Elapsed time is 0.019 s 17/12/15 11:59:31 INFO - Size: 4019539 (4b per object) 17/12/15 11:59:31 INFO - Deserializing 1000000 myEnumSets 17/12/15 11:59:31 INFO - Elapsed time is 0.021 s
常规枚举集:
17/12/15 11:59:48 INFO - Serializing 1000000 enumSets 17/12/15 11:59:51 INFO - Elapsed time is 2.506 s 17/12/15 11:59:51 INFO - Size: 30691553 (30b per object) 17/12/15 11:59:51 INFO - Deserializing 1000000 enumSets 17/12/15 11:59:51 INFO - Elapsed time is 0.489 s
虽然它并不安全。例如,它不适用于超过 32 个条目的枚举。
如何确保枚举在创建 MyEnumSet 时少于 32 个值?
ObjectOutputStream 序列化对对象的引用,第一次发送对象时,实际对象。如果您修改一个对象并再次发送它,ObjectOutputStream 所做的就是再次将 reference 发送到该对象。
这会产生一些后果
- 如果您修改对象,您将看不到这些修改
- 它必须在两端保留对每个发送过的对象的引用。这可能是一个微妙的内存泄漏。
- 这样做的原因是您可以序列化对象图而不是树。例如A 指向 B,B 指向 A。您只想发送一次 A。
解决这个问题并取回一些内存的方法是在每个完整的对象之后调用 reset()。例如在致电 flush()
Reset will disregard the state of any objects already written to the stream. The state is reset to be the same as a new ObjectOutputStream. The current point in the stream is marked as reset so the corresponding ObjectInputStream will be reset at the same point. Objects previously written to the stream will not be referred to as already being in the stream. They will be written to the stream again.
另一种方法是使用 writeUnshared,但是这对顶级对象应用了浅层非共享性。在 EnumSet
的情况下它会有所不同,但是它包装的 Enum[]
仍然是共享的 o_O
Writes an "unshared" object to the ObjectOutputStream. This method is identical to writeObject, except that it always writes the given object as a new, unique object in the stream (as opposed to a back-reference pointing to a previously serialized instance).
简而言之,不,这不是错误,而是预期的行为。