Implementing Serializable

2:36 PM

Do not implement Serializable lightly, since it restricts future flexibility, and publicly exposes class implementation details which are usually private. As well, implementing Serializable correctly is not trivial.

The serialVersionUID is a universal version identifier for a Serializable class. Deserialization uses this number to ensure that a loaded class corresponds exactly to a serialized object. If no match is found, then an InvalidClassException is thrown.

Guidelines for serialVersionUID :

  • always include it as a field, for example: "private static final long serialVersionUID = 7526472295622776147L; " include this field even in the first version of the class, as a reminder of its importance
  • do not change the value of this field in future versions, unless you are knowingly making changes to the class which will render it incompatible with old serialized objects
  • new versions of Serializable classes may or may not be able to read old serialized objects; it depends upon the nature of the change; provide a pointer to Sun's guidelines for what constitutes a compatible change, as a convenience to future maintainers
In Windows, generate serialVersionUID using the JDK's graphical tool like so :
  • use Control Panel | System | Environment to set the classpath to the correct directory
  • run serialver -show from the command line
  • point the tool to the class file including the package, for example, finance.stock.Account - without the .class
  • (here are the serialver docs for both Win and Unix)
readObject and writeObject :
  • readObject implementations always start by calling default methods
  • deserialization must be treated as any constructor : validate the object state at the end of deserializing - this implies that readObject should almost always be implemented in Serializable classes, such that this validation is performed.
  • deserialization must be treated as any constructor : if constructors make defensive copies for mutable object fields, so must readObject
  • when serializing a Collection, store the number of objects in the Collection as well, and use this number to read them back in upon deserialization; avoid tricks using null
Other points :
  • use javadoc's @serial tag to denote Serializable fields
  • the .ser extension is conventionally used for files representing serialized objects
  • no static or transient fields undergo default serialization
  • extendable classes should not be Serializable, unless necessary
  • inner classes should rarely, if ever, implement Serializable
  • container classes should usually follow the style of Hashtable, which implements Serializable by storing keys and values, as opposed to a large hash table data structure
Example
import java.io.Serializable;
import java.text.StringCharacterIterator;
import java.util.*;
import java.io.*;

public final class SavingsAccount implements Serializable {

/**
* This constructor requires all fields to be passed as parameters.
*
* @param aFirstName contains only letters, spaces, and apostrophes.
* @param aLastName contains only letters, spaces, and apostrophes.
* @param aAccountNumber is non-negative.
* @param aDateOpened has a non-negative number of milliseconds.
*/

public SavingsAccount (
String aFirstName, String aLastName, int aAccountNumber, Date aDateOpened
){
super();
setFirstName(aFirstName);
setLastName(aLastName);
setAccountNumber(aAccountNumber);
//make a defensive copy of the mutable Date passed to the constructor
setDateOpened( new Date(aDateOpened.getTime()) );
//there is no need here to call validateState.
}

public SavingsAccount () {
this ("FirstName", "LastName", 0, new Date(System.currentTimeMillis()));
}

public final String getFirstName() {
return fFirstName;
}

public final String getLastName(){
return fLastName;
}

public final int getAccountNumber() {
return fAccountNumber;
}

/**
* Returns a defensive copy of the field.
* The caller may change the state of the returned object in any way,
* without affecting the internals of this class.
*/

public final Date getDateOpened() {
return new Date(fDateOpened.getTime());
}

/**
* Names must contain only letters, spaces, and apostrophes.
* Validate before setting field to new value.
*
* @throws IllegalArgumentException if the new value is not acceptable.
*/

public final void setFirstName( String aNewFirstName ) {
validateName(aNewFirstName);
fFirstName = aNewFirstName;
}

/**
* Names must contain only letters, spaces, and apostrophes.
* Validate before setting field to new value.
*
* @throws IllegalArgumentException if the new value is not acceptable.
*/

public final void setLastName ( String aNewLastName ) {
validateName(aNewLastName);
fLastName = aNewLastName;
}

/**
* Validate before setting field to new value.
*
* @throws IllegalArgumentException if the new value is not acceptable.
*/

public final void setAccountNumber( int aNewAccountNumber ) {
validateAccountNumber(aNewAccountNumber);
fAccountNumber = aNewAccountNumber;
}

public final void setDateOpened( Date aNewDate ){
//make a defensive copy of the mutable date object
Date newDate = new Date( aNewDate.getTime());
validateDateOpened( newDate );
fDateOpened = newDate;
}

// PRIVATE //

/**
* The client's first name.
* @serial
*/

private String fFirstName;

/**
* The client's last name.
* @serial
*/

private String fLastName;

/**
* The client's account number.
* @serial
*/

private int fAccountNumber;

/**
* The date the account was opened.
* @serial
*/

private Date fDateOpened;

/**
* Determines if a de-serialized file is compatible with this class.
*
* Maintainers must change this value if and only if the new version
* of this class is not compatible with old versions. See Sun docs
* for details.
*
* Not necessary to include in first version of the class, but
* included here as a reminder of its importance.
*/

private static final long serialVersionUID = 7526471155622776147L;

/**
* Verify that all fields of this object take permissible values; that is,
* this method defines the class invariant.
*
* In this style of implementation, both the entire state of the object
* and its individual fields can be validated without repeating or
* duplicating code.
* Each condition is defined in one place. Checks on the entire
* object are performed at the end of object construction, and at
* the end of de-serialization. Checks on individual fields are
* performed at the start of the corresponding setXXX method.
* As well, this style replaces the if's and throwing
* of exceptions at the start of a setXXX, with a simple call to validateXXX.
* Validation is separated from the regular path of execution,
* which leads to improved legibility.
*
* @throws IllegalArgumentException if any field takes an unpermitted value.
*/

private void validateState() {
validateAccountNumber(fAccountNumber);
validateName(fFirstName);
validateName(fLastName);
validateDateOpened(fDateOpened);
}

/**
* Ensure names contain only letters, spaces, and apostrophes.
*
* @throws IllegalArgumentException if field takes an unpermitted value.
*/

private void validateName(String aName){
boolean nameHasContent = (aName != null) && (!aName.equals(""));
if (!nameHasContent){
throw new IllegalArgumentException("Names must be non-null and non-empty.");
}

StringCharacterIterator iterator = new StringCharacterIterator(aName);
char character = iterator.current();
while (character != StringCharacterIterator.DONE ){
boolean isValidChar =
(Character.isLetter(character) ||
Character.isSpaceChar(character) ||
character =='\''
);
if ( isValidChar ) {
//do nothing
}
else {
String message = "Names can contain only letters, spaces, and apostrophes.";
throw new IllegalArgumentException(message);
}
character = iterator.next();
}
}

/**
* AccountNumber must be non-negative.
* @throws IllegalArgumentException if field takes an unpermitted value.
*/

private void validateAccountNumber(int aAccountNumber){
if (aAccountNumber < 0) {
String message = "Account Number must be greater than or equal to 0.";
throw new IllegalArgumentException(message);
}
}

/**
* DateOpened must be after 1970.
* @throws IllegalArgumentException if field takes an unpermitted value.
*/

private void validateDateOpened( Date aDateOpened ) {
if( aDateOpened.getTime()<0 ) {
throw new IllegalArgumentException("Date Opened must be after 1970.");
}
}

/**
* Always treat de-serialization as a full-blown constructor, by
* validating the final state of the de-serialized object.
*/

private void readObject(
ObjectInputStream aInputStream
) throws ClassNotFoundException, IOException {
//always perform the default de-serialization first
aInputStream.defaultReadObject();

//make defensive copy of the mutable Date field
fDateOpened = new Date( fDateOpened.getTime() );

//ensure that object state has not been corrupted or tampered with maliciously
validateState();
}

/**
* This is the default implementation of writeObject.
* Customise if necessary.
*/

private void writeObject(
ObjectOutputStream aOutputStream
) throws IOException {
//perform the default serialization for all non-transient, non-static fields
aOutputStream.defaultWriteObject();
}

}

0 comments: