?

Log in

Previous Entry | Next Entry

Очень часто встречается в последнее время проблема: пишут объект в поток, меняют, пишут снова – а на выходе при чтении получают два одинаковых объекта. Первых. Изменения, сделанные перед второй записью, не передаются. Что делать?

На самом деле так оно и должно быть. По следующей причине.

Класс java.io.ObjectOuputStream, который используется для сериализации объектов – он умный. Он умеет сохранять граф объектов, с учетом всех связей. Это означает, что один и тот же объект он не будет сериализовать два раза. Один раз он его сериализует, а во второй раз просто запишет в поток ссылку на тот же самый первый объект. И тогда при десериализации вместо восстановления еще одного объекта он просто вернет ссылку на уже созданный. В результате два класса, имеющие ссылку на один и тот же третий, так и восстановятся.

И вот тут-то как раз и есть подводный камень. ObjectOuputStream контролирует ссылочную целостность графа. Иначе говоря, сравнивает он ссылки. И если мы запишем объект, а потом изменим его – ObjectOuputStream об этом изменении не узнает и сочтет объект тем же самым, который он уже записал.

Иллюстрируется это просто:
package ru.skipy.tests;

import java.io.*;
import java.util.*;

/**
 * ObjectOutputStreamTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 26.09.12
 */
public class ObjectOutputStreamTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        List<String> data = new ArrayList<>();
        data.add("One");
        data.add("Two");
        data.add("Three");
        oos.writeObject(data);
        data.clear();
        // 1
        //data = new ArrayList<>();
        data.add("Ein");
        data.add("Zwei");
        data.add("Drei");
        // 2
        //oos.reset();
        oos.writeObject(data);
        oos.flush();
        oos.close();
        baos.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        List<String> newData = (List<String>)ois.readObject();
        System.out.println(newData);
        newData = (List<String>)ois.readObject();
        System.out.println(newData);
    }
}
В результате выполнения этого кода мы получим:
[One, Two, Three]
[One, Two, Three]
Какие есть варианты действий в такой ситуации?

Первый, самый простой – каждый раз создавать новый объект для записи, вместо модификации существующего. Это, естественно, будет работать. В коде выше это первая закомментированная строка. Убираем комментарий – и тут же получаем:
[One, Two, Three]
[Ein, Zwei, Drei]
Второй способ менее тривиален. Заключается он в использовании метода reset() класса ObjectOuputStream (вторая закомментированная строка в коде). Метод этот, как сказано в документации (http://docs.oracle.com/javase/6/docs/api/java/io/ObjectOutputStream.html#reset()):

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 refered to as already being in the stream. They will be written to the stream again.


Коротко говоря – этот метод отбрасывает информацию обо всех объектах, записанных уже в поток. Ключевое тут – the state of any objects, т.е. обо всех объектах. Это серьезный подводный камень данного способа. Представьте, что у нас есть четыре объекта – A, B, C и D. A и B ссылаются на C. D - изменяемый. Нам надо записать D, потом A, потом изменить D, снова записать его, потом записать B. Что получится?

Если мы НЕ используем reset и не пересоздаем объект D. Записываем D. Записываем A, по ссылке записывается и C. Потом модифицируем D. Записываем его – в поток попадает только ссылка на уже записанный объект D. Записываем B, он ссылается на C, который уже записан по ссылке из А. Таким образом, мы сохраняем ссылочную целостность между A, B и C, но теряем изменения в D.

Что получится при использовании метода reset()? Записываем D. Записываем A, по ссылке записывается и C. Потом модифицируем D. Вызываем reset(). Записываем D, ObjectOuputStream расценивает его как новый объект, тут всё в порядке. Однако теперь мы записываем B, который ссылается на C, который уже записан по ссылке из А. Но поскольку мы информацию об этом благополучно забыли при вызове reset() – объект C опять будет записан в поток. Второй раз. И при десериализации мы получим объекты A и B, ссылающиеся на разные объекты C. Т.е. мы получаем нарушение ссылочной целостности.

Это иллюстрируется следующим примером:
package ru.skipy.tests;

import java.io.*;

/**
 * ObjectOutputStreamTest2
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 26.09.12
 */
public class ObjectOutputStreamTest2 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        D d = new D();
        d.msg = "Message 1";
        C c = new C();
        A a = new A();
        a.c = c;
        B b = new B();
        b.c = c;
        oos.writeObject(d);
        oos.writeObject(a);
        d.msg = "Message 2";
        //oos.reset();
        oos.writeObject(d);
        oos.writeObject(b);
        oos.flush();
        oos.close();
        baos.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        D nd1 = (D)ois.readObject();
        A na = (A)ois.readObject();
        D nd2 = (D)ois.readObject();
        B nb = (B)ois.readObject();
        System.out.println("\"Message 1\" expected, found: " + nd1.msg);
        System.out.println("\"Message 2\" expected, found: " + nd2.msg);
        System.out.println("Same link on object C expected, found? " + (na.c == nb.c));
    }

    private static class A implements Serializable {
        private C c;
    }

    private static class B implements Serializable {
        private C c;
    }

    private static class C implements Serializable {
    }

    private static class D implements Serializable {
        private String msg;
    }
}
При закомментированном вызове ObjectOutputStream.reset() мы получаем:
"Message 1" expected, found: Message 1
"Message 2" expected, found: Message 1
Same link on object C expected, found? true
. Это та проблема, с которой всё и началось – в поток не записалось новое значение в объекте D. А вот если раскомментировать вызов reset(), мы получим:
"Message 1" expected, found: Message 1
"Message 2" expected, found: Message 2
Same link on object C expected, found? false
Исходная проблема решилась – новое значение записалось в поток. Но ссылочная целостность графа объектов A, B и C нарушена – A и B теперь ссылаются на разные экземпляры объекта C.

Резюме. При необходимости повторной записи объекта в ObjectOutputStream самым простым и надежным решением является создание нового экземпляра этого объекта. Использовать метод ObjectOutputStream.reset() для переинициализации потока можно только тогда, когда вы хорошо понимаете, что делаете и зачем.

Comments

Den Zurin
May. 29th, 2015 06:04 pm (UTC)
А зачем нужен этот класс, везде же сериализуют в XML или JSON.