EnumSet序列化

时间:2015-12-16 16:22:49

标签: java serialization enumset

我已经失去了几个小时调试我的应用程序,我相信我偶然发现了一个(另一个o_O)Java bug ...嗅...我希望它不是,因为这会很难过:(

我正在做以下事情:

  1. 使用一些标志创建EnumSet mask
  2. 序列化(使用ObjectOutputStream.writeObject(mask)
  3. 清除并设置mask
  4. 中的其他一些标记
  5. 再次序列化
  6. 预期结果:第二个序列化对象与第一个序列化对象不同(反映实例中的更改)

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

    代码:

    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倍......

    MyEnumSets:

    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
    

    常规EnumSets:

    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个?

1 个答案:

答案 0 :(得分:12)

ObjectOutputStream序列化对象的引用以及第一次发送对象时的实际对象。如果您修改对象并再次发送它,则所有ObjectOutputStream都会再次将引用发送给该对象。

这会产生一些后果

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

解决此问题并获取内存的方法是在每个完整对象之后调用reset()。例如在致电flush()

之前
  

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

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

  

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

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