Effective Java 75 Consider using a custom serialized form

时间:2023-03-10 04:49:41
Effective Java 75 Consider using a custom serialized form

Principle

  1. Do not accept the default serialized form without first considering whether it is appropriate.  

    The default serialized form is likely to be appropriate if an object's physical representation is identical to its logical content.

    @serial tag tells the Javadoc utility to place this documentation on a special page that documents serialized forms.

    // Good candidate for default serialized form

    public class Name implements Serializable {

    /**

    * Last name. Must be non-null.

    * @serial

    */

    private final String lastName;

    /**

    * First name. Must be non-null.

    * @serial

    */

    private final String firstName;

    /**

    * Middle name, or null if there is none.

    * @serial

    */

    private final String middleName;

    ... // Remainder omitted

    }

  2. Even if you decide that the default serialized form is appropriate, you often must provide a readObject method to ensure invariants and security.  

    Using the default serialized form when an object's physical representation differs substantially from its logical data content has four disadvantages:

    1. It permanently ties the exported API to the current internal representation.
    2. It can consume excessive space.
    3. It can consume excessive time. (Topology graph traversal)
    4. it can cause stack overflow. (Depends on JVM implementation)

    Inconsistent demo for logic data and physical representation

    // Awful candidate for default serialized form

    public final class StringList implements Serializable {

    private int size = 0;

    private Entry head = null;

    private static class Entry implements Serializable {

    String data;

    Entry next;

    Entry previous;

    }

    ... // Remainder omitted

    }

    Revised version of StringList containing writeObject and readObject methods implementing this serialized form.

    public class StringList implements Serializable {

    /**

    * The serial version UID of the object.

    */

    private static final long serialVersionUID = 7533856777949584383L;

    private transient int size = 0;

    private transient Entry head = null;

    private transient Entry tail = null;

    // No longer Serializable!

    private static class Entry {

    String data;

    Entry next;

    @SuppressWarnings("unused")

    Entry previous;

    }

    // Appends the specified string to the list

    public final void add(String s) {

    Entry e = new Entry();

    e.data = s;

    if (null == head) {

    tail = head = e;

    } else {

    tail.next = e;

    tail.next.previous = tail;

    tail = tail.next;

    }

    size++;

    }

    /**

    * Serialize this {@code StringList} instance.

    *

    * @serialData The size of the list (the number of strings it contains) is

    * emitted ({@code int}), followed by all of its elements (each

    * a {@code String}), in the proper sequence.

    */

    private void writeObject(ObjectOutputStream s) throws IOException {

    s.defaultWriteObject();

    s.writeInt(size);

    // Write out all elements in the proper order.

    for (Entry e = head; e != null; e = e.next)

    s.writeObject(e.data);

    }

    private void readObject(ObjectInputStream s) throws IOException,

    ClassNotFoundException {

    s.defaultReadObject();

    int numElements = s.readInt();

    // Read in all elements and insert them in list

    for (int i = 0; i < numElements; i++)

    add((String) s.readObject());

    }

    /*

    * (non-Javadoc)

    *

    * @see java.lang.Object#toString()

    */

    @Override

    public String toString() {

    StringBuilder sb = new StringBuilder();

    for (Entry it = head; it != null; it = it.next)

    sb.append(it.data);

    return sb.toString();

    }

    /**

    * @param args

    */

    public static void main(String[] args) {

    StringList sl = new StringList();

    for (int i = 0; i < 5; i++) {

    sl.add(String.valueOf(i));

    }

    System.out.println(sl);

    try {

    FileOutputStream fos = new FileOutputStream("t.tmp");

    ObjectOutputStream oos = new ObjectOutputStream(fos);

    oos.writeObject(sl);

    oos.close();

    FileInputStream fis = new FileInputStream("t.tmp");

    ObjectInputStream ois = new ObjectInputStream(fis);

    StringList sl2 = (StringList) ois.readObject();

    System.out.println("Desialized obj = " + sl2);

    } catch (FileNotFoundException e) {

    e.printStackTrace();

    } catch (IOException e) {

    e.printStackTrace();

    } catch (ClassNotFoundException e) {

    e.printStackTrace();

    }

    }

    // Remainder omitted

    }

  3. Before deciding to make a field nontransient, convince yourself that its value is part of the logical state of the object.  

    Note

    If all instance fields are transient, it is technically permissible to dispense with invoking defaultWriteObject and defaultReadObject, but it is not recommended. If there are nontransient fields to be added in the class due to failed to invoke defaultReadObject, the deserialization would fail with a StreamCorruptedException.

    Every instance field that can be made transient should be made so since not labeled transient will be serialized when the defaultWriteObject method is invoked.

  4. if you are using the default serialized form.
    1. Transient field will be initialized as their default value (e.g. null, 0, false).
    2. If the default value cannot be acceptable you should provide readObject method and invoke defaultReadObject method and then restores transient fields to acceptable values (Item 76).
    3. Transient field can be lazily initialized the first time they are used (Item 71) .
  5. You must impose any synchronization on object serialization that you would impose on any other method that reads the entire state of the object. You must ensure that it adheres to the same lock-ordering constraints as other activity,  

    // writeObject for synchronized class with default serialized form

    private synchronized void writeObject(ObjectOutputStream s)

    throws IOException {

    s.defaultWriteObject();

    }

  6. Regardless of what serialized form you choose, declare an explicit serial version UID in every serializable class you write.

    private static final long serialVersionUID = randomLongValue ;

    If you modify an existing class that lacks a serial version UID, and you want the new version to accept existing serialized instances, you must use the value that was automatically generated for the old version. Or InvalidClassException will be invoked when there is a serialized object to be deserialized by the new modified the class.

Summary

when you have decided that a class should be serializable (Item 74), think hard about what the serialized form should be. Use the default serialized form only if it is a reasonable description of the logical state of the object; otherwise design a custom serialized form that aptly describes the object. You should allocate as much time to designing the serialized form of a class as you allocate to designing its exported methods (Item 40). Just as you cannot eliminate exported methods from future versions, you cannot eliminate fields from the serialized form; they must be preserved forever to ensure serialization compatibility. Choosing the wrong serialized form can have a permanent, negative impact on the complexity and performance of a class.