程式扎記: [ Data Structures with Java ] Section 23.4 : Serialization

標籤

2011年4月25日 星期一

[ Data Structures with Java ] Section 23.4 : Serialization

Preface : 
An object is persistent is it can exist apart from the executing program that loaded it into memory. There are good reasons why persistent objects are useful. They can be saved to an external file and then later retrieved. In Internet applications, they can be input and output between a client and a server. We will now illustrate examples that use an external file. 
The process of storing and retrieving objects in an external file is called serialization. Writing an object to a file is called serializing the object, and reading the object back from a file is called deserializing an object. In Java, serialization occurs, for the most part, automatically. You can read and write objects of almost any class type, even when the classes are part of an inheritance hierarchy that shares common data. The process will involve saving not only variables of primitive type such as int and double but also saving objects, arrays, arrays of objects, and other more complex data. 
In the Java.io package, the classes ObjectOutputStream and ObjectInputStream are used for serialization. The class ObjectOutputStream implements the same interface as DataOutputStream and so can process basic types of data. The same is true of ObjectInputStream, which implements the same interface as DataInputStream. 

Serializing an Object : 
The ObjectOutputStream class is used to write an object to a file. The stream is created with a FileOutputStream argument that becomes the underlying byte stream linked to a file that will store the object. For instance : 
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("storeFile")); 

The ObjectOutputStream class defines the method writeObject() with an Object parameter. You can use the operation to write any object to file so long as its class type implements the Serializable interface. We will look at this criterion in a moment. If anObject is an instance of a class that implements Serializable, then write it to file storeFile using the stream oos and writeObject() with anObject as the argument. 
oos.writeObject(anObject); 

The operation writes to the file all the necessary information about the object so that it can be reconstructed later. Reconstructing the file will involve a read operation that deserializes the object. The information that is output does not include just the data in the Object. There is information on the class and any superclasses as well as the name and data type of each variable. If the object has reference variables which may themselves point to the reference data, writeObject() is called recursively for each variable. If the class type for each data object is Serializable, the full state of the object is copied to the file. The method writeObject() throws a NotSerializableException if the object's class or the class of a variable does not implement the Serializable interface. 

Making a Class Serializable : 
Like Cloneable, the Serializable interface defines no methods. It simply signals to the Java compiler that objects of the class may be serialized. Making a class Serializable is often very simple. Just include in the class header the directive implements java.io.Serializable. No additional code is necessary. For instance : 

- Serializable Time24 class :
  1. public class Time24 implements Cloneable, java.io.Serializable{  
  2. for the class>  
  3. }  

In the case of collection classes, the serialization process must be customized. We will discuss customizing serialization later in this section. In some cases, a class has variables which are not Serializable or variables which should not be copied to the file. For instance, the object may maintain the current time and date, which becomes irrelevant when stored in the file. This is data that can be updated after the object is deserialized. To specify that a variable should not be saved, declare it to be transient. The method writeObject() will not write data declared transient to the stream. 

Deserializing an Object : 
Reading an object back from a file reverses the serializing process. Start with an ObjectInputStream that uses a FileInputStream to create an underlying byte stream connected to the file. Use the method readObject(), which completely restores the object and returns reference type Object. Finish the process by casting the return object back to its original class type : 

  1. ObjectInputStream ois = new ObjectInputStream(new FileInputStream("storeFile"));  
  2. ClassName recallObj;  
  3. recallObj = (ClassName)ois.readObject(); // retrieve object  
Method readObject() throws a ClassNotFoundException if the definition of the class for the object read from the stream is not in the current program. 

Application: Serializing Objects : 
An application illustrates default serialization of objects. The class SerializableClass simply implements the Serializable interface. Its instance variable includes a primitive integer type and an array of Integer objects. These variables are Serializable by default. The objects in the array are copied to a file because their class type, Integer, is Serializable. Two other instance variables, str and t, have class type String and Time24, which implement Serializable. The class has a transient reference variable, currentTime, of type Time24. For demonstration purpose, we make all of the variables public and include a constructor with initial values for the integer, the string and the two time objects. The array is created with four Integer elements having values 1 to 4 : 

- Class SerializableClass :
  1. package DSwJ.S23;  
  2.   
  3. import DSwJ.S1.Time24;  
  4.   
  5. public class SerializableClass implements java.io.Serializable{  
  6.     public int n;  
  7.     public String str;  
  8.     public Time24 t;  
  9.     public Integer[] list = new Integer[4];  
  10.     transient public Time24 currentTime;  
  11.       
  12.     public SerializableClass(int n, String str, Time24 t, Time24 currentTime) {  
  13.         this.n = n;  
  14.         this.str = str;  
  15.         this.t = t;  
  16.         for(int i=0; inew Integer(i+1);  
  17.         this.currentTime = currentTime;  
  18.     }  
  19. }  

- Program 23.3 
This program creates a Serializable class object obj with initial integer and string value 45 and "Shooting start" respectively. The instance variable t is set at 9:30 and the transient variable at 7:10. A runtime change advance time t by 45 minutes to 10:15, which point a call to writeObject() uses the ObjectOutputStream oos to copy the object to file storeFile.dat. 
The method readObject() uses ObjectInputStream ios to retrieve the object from the file and assign it to recallObj. The value of recallObj.currentTime is null. The program allocates a new Time24 object that simulates currentTime for recallObj. Output displays all of the fields of the object before it is serialized and after it is deserialized : 

- Demonstration code :
  1. public static void main(String args[]) {  
  2.     SerializableClass obj, recallObj;  
  3.     try{  
  4.         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("storeFile.dat"));  
  5.         obj = new SerializableClass(45"Shooting star"new Time24(9,30), new Time24(7,10));  
  6.         obj.t.addTime(45);  
  7.         System.out.println("Serialized object:");  
  8.         System.out.printf("\tInteger: %d String: %s Time: %s Current Time: %s\n", obj.n, obj.str, obj.t, obj.currentTime);  
  9.         System.out.println("\t"+Arrays.toString(obj.list));  
  10.         oos.writeObject(obj);  
  11.         oos.flush();  
  12.         oos.close();  
  13.           
  14.         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("storeFile.dat"));  
  15.         recallObj = (SerializableClass)ois.readObject();  
  16.         recallObj.currentTime = new Time24(15,45);  
  17.         System.out.println("Deserialized Object:");  
  18.         System.out.printf("\tInteger: %d String: %s Time: %s Current Time: %s\n", recallObj.n, recallObj.str, recallObj.t, recallObj.currentTime);  
  19.         System.out.println("\t"+Arrays.toString(recallObj.list));  
  20.     }catch(IOException ioe) {ioe.printStackTrace();  
  21.     }catch(Exception e){e.printStackTrace();}  
  22.       
  23. }  

Output : 

Serialized object:
Integer: 45 String: Shooting star Time: 10:15 Current Time: 7:10
[1, 2, 3, 4]
Deserialized Object:
Integer: 45 String: Shooting star Time: 10:15 Current Time: 15:45
[1, 2, 3, 4]

Custom Serialization : 
For many classes, the default serialization of its objects work fine. In some cases, however, a programmer wants to customize the write/read process. This is true when the object's data does not effectively move back and forth from a file. With collection objects, elements are generated dynamically and stored with some kind of ordering. The deserialization process must retrieve the elements and then rebuild the underlying storage structure for the collection. The ArrayList class is a good example. 
In an ArrayList collection, elements are stored in the array listArr of type T, which has capacity listArr.length. The actual elements in the collection, with countlistSize, are stored in the front of the array. The tail represents unused capacity. 
Default serializtion of an ArrayList object would copy all of the array elements to a file, even those in the unused capacity. This is inefficient in terms of both time and disk storage. A better serialization algorithm copies only the current collection elements to a file. When the object is deserialized, memory for the array is allocated and the elements are copied back. The process rebuilds the collection dynamically. 
To customize serialization for the ArrayList class, we must implement two special methods with these exact signatures : 

- Serializble ArrayList class :
  1. public class ArrayList implements List, Cloneable, Serializable {  
  2. ...  
  3.     private void writeObject(java.io.ObjectOutputStream out)throws IOException{...}  
  4.     private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException{...}  
  5. ...  
  6. }  

The first step in the implementation of writeObject() is to call out.defaultWriteObject() which writes the instance variables to the stream. In particular, it will write listSize and reference listArr to the ObjectOutputStream. the output of the reference listArr is not used when we deserialize an ArrayList object. We next use the method writeInt() to output the size of the array (the capacity) and follow this by writing the first listSize number of elements from listArr to the stream : 

- Method writeObject() :
  1. private void writeObject(java.io.ObjectOutputStream out)throws IOException{  
  2.     out.defaultWriteObject();  
  3.     out.writeInt(listArr.length);  
  4.     for(int i=0; i
  5. }  

The implementation of readObject() simply reverses the effects of writeObject(). First, call in.defaultReadObject() to restore instance variables. Then read the capacity from the stream using readInt() and allocate an array listArr of type T with that number of elements. Finally use readObject() to read back listSizenumber of objects from the stream. The objects are copied in order into the newly allocated array : 

- Method readObject() :
  1. private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException{  
  2.     in.defaultReadObject();  
  3.     int listCapacity = in.readInt();  
  4.     listArr = (T[]) new Object[listCapacity];  
  5.     for(int i=0; i
  6. }  

Supplement : 
* [ Gossip in Java(2) ] 輸入輸出 : 位元串流 (ObjectInputStream、ObjectOutputStream)

沒有留言:

張貼留言

網誌存檔