EnumSet сериализация

Я только что потерял пару часов, отлаживая свое приложение, и я думаю, что наткнулся на (еще одну 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 в качестве маски для указания того, какие поля будут присутствовать или отсутствовать в последующем сообщении, поэтому это своего рода оптимизация использования полосы пропускания и процессора. Это было очень неправильно!!! EnumSet сериализация занимает много времени, и каждый экземпляр занимает 30 (!!!) байтов! Так много для космической экономики:)

В двух словах, пока ObjectOutputStream очень быстро для примитивных типов (как я выяснил уже в небольшом тесте здесь: /questions/21071490/v-chem-raznitsa-mezhdu-dataoutputstream-i-objectoutputstream/21071506#21071506), он мучительно запутан и неэффективен с (особенно маленькими) объектами...

Поэтому я решил эту проблему, создав собственный EnumSet с поддержкой int и сериализовав / десериализовав 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 записями.

Как я могу убедиться, что перечисление имеет менее 32 значений при создании MyEnumSet?

1 ответ

Решение

ObjectOutputStream сериализует ссылки на объекты и при первой отправке объекта, фактический объект. Если вы изменяете объект и отправляете его снова, все, что делает ObjectOutputStream, - это снова отправляет ссылку на этот объект.

Это имеет несколько последствий

  • если вы измените объект, вы не увидите эти изменения
  • он должен сохранять ссылку на каждый когда-либо отправленный объект на обоих концах. Это может быть небольшая утечка памяти.
  • Это сделано для того, чтобы вы могли сериализовать графики объектов вместо деревьев. например, A указывает на B, что указывает на A. Вы хотите отправить A только один раз.

Чтобы решить эту проблему и вернуть немного памяти, нужно вызывать метод reset() после каждого завершенного объекта. например, перед звонком flush()

Сброс будет игнорировать состояние любых объектов, уже записанных в поток. Состояние сбрасывается, чтобы быть таким же, как новый ObjectOutputStream. Текущая точка в потоке помечена как сброшенная, поэтому соответствующий ObjectInputStream будет сброшен в той же точке. Объекты, ранее записанные в поток, не будут упоминаться как уже находящиеся в потоке. Они будут снова записаны в поток.

Другой подход заключается в использовании writeUnshared, однако при этом применяется неглубокий неразделенный доступ к объекту верхнего уровня. В случае EnumSet все будет иначе, однако Enum[] это обертки по-прежнему передается o_O

Записывает "неразделенный" объект в ObjectOutputStream. Этот метод идентичен writeObject, за исключением того, что он всегда записывает данный объект как новый, уникальный объект в потоке (в отличие от обратной ссылки, указывающей на ранее сериализованный экземпляр).

Короче, нет, это не ошибка, а ожидаемое поведение.

Другие вопросы по тегам