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