When objects are serialized using the writeObject() method, each object is written to the output stream only once. Invoking the writeObject() method on the same object a second time places a back-reference to the previously serialized instance in the stream. Correspondingly, the readObject() method produces at most one instance for each object present in the input stream that was previously written by writeObject().

According to the Java API [API 2013], the writeUnshared() method

writes an "unshared" object to the ObjectOutputStream. This method is identical to writeObject, except that it always writes the given object as a new, unique object in the stream (as opposed to a back-reference pointing to a previously serialized instance).

Correspondingly, the readUnshared() method

reads an "unshared" object from the ObjectInputStream. This method is identical to readObject, except that it prevents subsequent calls to readObject and readUnshared from returning additional references to the deserialized instance obtained via this call.

Consequently, the writeUnshared() and readUnshared() methods are unsuitable for round-trip serialization of data structures that contain reference cycles. 

Consider the following code example:

public class Person {
  private String name;
    
  Person() {
    // Do nothing - needed for serialization
  }
    
  Person(String theName) {
    name = theName;
  }
 
  // Other details not relevant to this example
}


public class Student extends Person implements Serializable {
  private Professor tutor;
     
  Student() {
    // Do nothing - needed for serialization
  }
     
  Student(String theName, Professor theTutor) {
    super(theName);
    tutor = theTutor;
  }
     
  public Professor getTutor() {
    return tutor;
  }
}
 
public class Professor extends Person implements Serializable {
  private List<Student> tutees = new ArrayList<Student>();
     
  Professor() {
    // Do nothing - needed for serialization
  }
     
  Professor(String theName) {
    super(theName);
  }
     
  public List<Student> getTutees () {
    return tutees;
  }
     
  /**
   * checkTutees checks that all the tutees
   * have this Professor as their tutor
   */
  public boolean checkTutees () {
    boolean result = true;
    for (Student stu: tutees) {
      if (stu.getTutor() != this) {
         result = false;
         break;
      }
    }
    return result;
  }
}

// ...
 
Professor jane = new Professor("Jane");
Student able = new Student("Able", jane);
Student baker = new Student("Baker", jane);
Student charlie = new Student("Charlie", jane);
jane.getTutees().add(able);
jane.getTutees().add(baker);
jane.getTutees().add(charlie);
System.out.println("checkTutees returns: " + jane.checkTutees());
// Prints "checkTutees returns: true"

Professor and Students are types that extend the basic type Person. A student (that is, an object of type Student) has a tutor of type Professor. A professor (that is, an object of type Professor) has a list (actually, an ArrayList) of tutees (of type Student). The method checkTutees() checks whether all of the tutees of this professor have this professor as their tutor, returning true if that is the case and false otherwise. 

Suppose that Professor Jane has three students, Able, Baker, and Charlie, all of whom have Professor Jane as their tutor. Issues can arise if the writeUnshared() and readUnshared() methods are used with these classes, as demonstrated in the following noncompliant code example.

Noncompliant Code Example

This noncompliant code example attempts to serialize the data from the previous example using writeUnshared()

String filename = "serial";
try(ObjectOutputStream oos = new ObjectOutputStream(new 
      FileOutputStream(filename))) {
  // Serializing using writeUnshared
  oos.writeUnshared(jane);
} catch (Throwable e) {
  // Handle error
}

// Deserializing using readUnshared
try(ObjectInputStream ois = new ObjectInputStream(new 
      FileInputStream(filename))){
  Professor jane2 = (Professor)ois.readUnshared();
  System.out.println("checkTutees returns: " +
                     jane2.checkTutees());
} catch (Throwable e) {
  // Handle error
}

However, when the data is deserialized using readUnshared(), the checkTutees() method no longer returns true because the tutor objects of the three students are different from the original Professor object.

Compliant Solution

This compliant solution uses the writeObject() and readObject() methods to ensure that the tutor object referred to by the three students has a one-to-one mapping with the original Professor object. The checkTutees() method correctly returns true.

String filename = "serial";
try(ObjectOutputStream oos = new ObjectOutputStream(new 
      FileOutputStream(filename))) {
	// Serializing using writeUnshared
	oos.writeObject(jane);
} catch (Throwable e) {
    // Handle error
} 

// Deserializing using readUnshared
try(ObjectInputStream ois = new ObjectInputStream(new 
      FileInputStream(filename))) {
Professor jane2 = (Professor)ois.readObject();
System.out.println("checkTutees returns: " +
                   jane2.checkTutees());
} catch (Throwable e) {
    // Handle error
} 

Applicability

Using the writeUnshared() and readUnshared() methods may produce unexpected results when used for the round-trip serialization of the data structures containing reference cycles.

Bibliography

 


11 Comments

  1. The title of this recommendation is a bit strong.  It seems to indicate you should never use the methods, but the actual text clarifies that you should never use the methods if you want to preserve object identities during serialization.  However, there are legitimate use cases for not wanting to preserve object identity, so the title saying that the methods should never be used is a bit misleading.

    In fact, there is more subtlety to using writeUnshared() in that only the top level object is unshared, nested references will still be shared (for extra confusion).

    You should probably point out that ObjectOutputStream.reset() has a similar affect on the stream.  (again, though, there are legitimate uses for this method as well).

    1. I've weakened the title and updated the text to be clear that it applies to cases where you want a one-to-one mapping between pre-serialization and post-deserialization objects.  This is, of course, almost but not quite the same thing as preserving object identity during serialization (which appears to be outright impossible, as far as I can tell – you cannot guarantee that you'll get the same default hash code, for example).

       

      Struck-out text is what I replaced whole-sale; there are a few other minor changes to the text.

    2. New text now addresses James' second point above.  Still need to say something about .reset().

    • For the NCE, where is Jane3 delcared? In the CS that is jane2
    • closing streams can be deferred to a finally block

    Made some significant edits to make the text easier to understand.

    1. jane3 was clearly a typo.  I've corrected it.

      1. I've addressed Dhruv's second bullet point...the close commands are now in finally blocks. We could shrink the code to use try-with-resources if anyone feels like doing more work (smile)

        1. We should use the new "try with resources" statement, then we don't need the close method calls at all.

          1. Someone has made this change.

  2. Can we reword the following? Very difficult sentence to understand.

    It is also important to note that calls to the writeUnshared() and readUnshared() methods prevent sharing only of the object referred to by (or returned by) the methods; sharing (or not) of objects reached from references in the unshared object remains unchanged. 

     Also the guideline must pin point the exact problem and its root cause. What is the one to one mapping? Why should the method return true?

    Can we make the guideline clearer?

    1. I made that paragraph shorter, hopefully that also made it clearer (:

      1. Yes thanks. Marked as reviewed.