Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why serialization proxy pattern is not compatible with classes whose object graph contains circularities

I read 《Effective Java》and I was told that Serialization Proxy Pattern is not compatible with classes whose object graph contains circularities. Actually I don't get it. So I wrote a sample to validate it. As you see:

public class A implements Serializable{
    private B b;

    public A(){
    }

    public A(B b){
            this.b = b;
    }
}

public class B implements Serializable{
    private A a;

    public B(){
    }

    public B(A a){
            this.a = a;
    }
}

public class C implements Serializable{
private A a;
private B b;

public C(A a, B b){
    this.a = a;
    this.b = b;
}   

private Object writeReplace(){
    return new SerializationProxy(this);
}

private Object readObject(ObjectInputStream in) throws InvalidObjectException{
    throw new InvalidObjectException("Proxy Required!");
}

private static final class SerializationProxy implements Serializable{
    private final A a;
    private final B b;

    SerializationProxy(C c){
        this.a = c.a;
        this.b = c.b;
    }

    private Object readResolve(){
        return new C(a, b);
    }
}

public static void main(String args[]) throws Exception{
    C c = new C(new A(), new B());

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(baos);
    out.writeObject(c);

    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));

    c = (C)in.readObject();
}
}

However, it executed very well. So, can anyone explain it further to me. Thanks a lot.

like image 851
tuan long Avatar asked Mar 11 '26 22:03

tuan long


1 Answers

The problem is not with circular references generally but with specific situation when you have a reference to the root object inside your object graph. In the following example Container has a list of Items but every item in the list has a reference to the Container (root object in the object graph!) as it's parent.

If you execute this code, the output will be ClassCastException's stack trace. But if you comment the line item.setParent(this); from the Container's item setter and runs the code again, output will be fine.

Reason why it happens is the way of implementation of the serialization using writeReplace(). When you replace original object with SerializationProxy then in the SerializationProxy's stream, JVM will replace any reference to the orginal object with a reference to the SerializationProxy object. (I don't know why it is implemented that way but it is). http://docs.oracle.com/javase/6/docs/platform/serialization/spec/input.html#5903

In our case, we put the items in the SerializationProxy object, but items had a reference to the original object (their parent). Then when you want to deserialize a stream, deserialized SerializationProxy's reference is assigned to the orginal object's reference in the Item's parent field and you have a ClassCastException.

public class Main {

    public static void main(String[] args) throws Exception {
        try {
            Container c = new Container("Circular References Test");
            c.addItem(new Item("Item 1"));
            c.addItem(new Item("Item 2"));

            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(baos);
            out.writeObject(c);

            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));

            c = (Container)in.readObject();
            System.out.println("Container c has label '" + c.getLabel() + "' and has " + c.getItems().size() + " items.");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}

class Container implements Serializable {
    private static final long serialVersionUID = 1L;
    private List<Item> items = new ArrayList<>();
    private String label = "";

    public Container() {
    }

    public Container(String label) {
        this.label = label;
    }

    public String getLabel() {
        return this.label;
    }
    public void setLabel(String label) {
        this.label = label;
    }

    public boolean addItem(Item item) {
        if(item != null) {
            item.setParent(this);
            return this.items.add(item);
        } else {
            return false;
        }
    }
    public boolean removeItem(Item item) {
        if(item != null) {
            item.setParent(null);
            return this.items.remove(item);
        } else {
            return false;
        }
    }

    public List<Item> getItems() {
        return new ArrayList<Item>(this.items);
    }

    private static final class SerializationProxy implements Serializable {

        private static final long serialVersionUID = 1L;
        private String containerLabel = "";
        private List<Item> items;
        public SerializationProxy(Container c){
            this.containerLabel = c.getLabel();
            this.items = new ArrayList<>(c.getItems());
        }

        private Object readResolve(){
            Container c = new Container(this.containerLabel);
            for(int i = 0; this.items != null && i < this.items.size(); i++) {
                c.addItem(this.items.get(i));
            }
            return c;
        }
    }

    private Object writeReplace(){
        return new SerializationProxy(this);
    }

    private Object readObject(ObjectInputStream in) throws InvalidObjectException{
        throw new InvalidObjectException("Proxy Required!");
    }
}

class Item implements Serializable {
    private static final long serialVersionUID = 1L;
    private Container parent = null;
    private String name = "";

    public Item() { 
    }
    public Item(String name) {
        this.name = name;
    }

    public Container getParent() {
        return this.parent;
    }
    public void setParent(Container parent) {
        this.parent = parent;
    }

    public String getName() {
        return this.name;
    }
    public void setName(String name) {
        this.name = name;
    }

}
like image 104
Mladen Krstić Avatar answered Mar 14 '26 12:03

Mladen Krstić



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!