【问题标题】:EnumSet serializationEnumSet 序列化
【发布时间】:2016-03-22 21:18:15
【问题描述】:

我刚刚在调试我的应用程序时浪费了几个小时,而且我相信我偶然发现了一个(另一个 o_O)Java 错误...嗅...我希望不是,因为这会很伤心:(

我正在做以下事情:

  1. 创建带有一些标志的 EnumSet mask
  2. 对其进行序列化(使用ObjectOutputStream.writeObject(mask)
  3. 清除和设置mask 中的一些其他标志
  4. 再次序列化

预期结果:第二个序列化对象与第一个不同(反映实例的变化)

获得的结果:第二个序列化对象是第一个的精确副本

代码:

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 (!!!) 字节!太空经济就这么多:)

简而言之,虽然ObjectOutputStream 对于原始类型来说非常快(正如我在这里的一个小测试中发现的那样:https://stackoverflow.com/a/33753694),但对于(尤其是小)对象来说,它非常缓慢且效率低下......

所以我通过创建我自己的由 int 支持的 EnumSet 并直接序列化/反序列化 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 个?

【问题讨论】:

    标签: java serialization enumset


    【解决方案1】:

    ObjectOutputStream 序列化对对象的引用,并在第一次发送对象时,即实际对象。如果你修改一个对象并再次发送它,ObjectOutputStream 所做的只是再次发送 reference 到那个对象。

    这有一些后果

    • 如果您修改对象,您将看不到这些修改
    • 它必须在两端保留对曾经发送的每个对象的引用。这可能是微妙的内存泄漏。
    • 这样做的原因是您可以序列化对象图而不是树。例如A 指向 B,B 又指向 A。您只想发送 A 一次。

    解决这个问题并取回一些内存的方法是在每个完整对象之后调用reset()。例如在打电话之前flush()

    重置将忽略已写入流的任何对象的状态。状态被重置为与新的 ObjectOutputStream 相同。流中的当前点被标记为重置,因此相应的 ObjectInputStream 将在同一点重置。之前写入流的对象不会被称为已经在流中。它们将再次写入流中。

    另一种方法是使用writeUnshared,但是这会将浅层非共享性应用于顶级对象。在EnumSet 的情况下,它会有所不同,但是它包装的Enum[] 仍然是共享的o_O

    将“非共享”对象写入 ObjectOutputStream。此方法与 writeObject 相同,不同之处在于它始终将给定对象作为流中新的唯一对象写入(与指向先前序列化实例的反向引用相反)。

    简而言之,这不是错误,而是预期的行为。

    【讨论】:

    • 感谢您的解释,现在说得通了!但是,我刚刚尝试了writeUnshared,它似乎不起作用,我仍然得到相同的输出:第一次序列化:[ONE,TWO] 第二次序列化:[]第一次反序列化[ONE,TWO]第二次反序列化[ONE,二]
    • @Denis 我总是在 flush() 之前使用 reset(),我会试试的。
    • @Denis 实际上不调用 reset() 是一个性能问题,因为它会保存您在两端发送的每个对象,除非您这样做。
    • 嗯,听起来像是通过缩短所有其他人来修复一张桌子的断腿 :) 但无论如何,谢谢,我今天学到了很多关于序列化的知识!
    • 那是一个油嘴滑舌的说法。不反序列化同一个对象的目的是支持循环对象图的序列化,这是一项壮举。这是一项功能,而不是错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-06
    • 2016-05-12
    • 2013-03-23
    • 2017-02-13
    • 2013-01-27
    • 2011-01-25
    相关资源
    最近更新 更多