Object Serialization
In Section 14.5, we demonstrated how to write the individual fields of an AccountRecord object into a file as text, and how to read those fields from a file and place their values into an AccountRecord object in memory. In the examples, AccountRecord was used to aggregate the information for one record. When the instance variables for an AccountRecord were output to a disk file, certain information was lost, such as the type of each value. For instance, if the value "3" were read from a file, there is no way to tell if the value came from an int, a String or a double. We have only data, not type information, on a disk. If the program that is going to read this data "knows" what object type the data corresponds to, then the data is simply read into objects of that type. For example, in Section 14.5.2, we know that we are inputting an int (the account number), followed by two Strings (the first and last name) and a double (the balance). We also know that these values are separated by spaces, with only one record on each line. Sometimes we will not know exactly how the data is stored in a file. In such cases, we would like to read or write an entire object from a file. Java provides such a mechanism, called object serialization. A so-called serialized object is an object represented as a sequence of bytes that includes the object's data as well as information about the object's type and the types of data stored in the object. After a serialized object has been written into a file, it can be read from the file and deserializedthat is, the type information and bytes that represent the object and its data can be used to recreate the object in memory.
Classes ObjectInputStream and ObjectOutputStream, which respectively implement the ObjectInput and ObjectOutput interfaces, enable entire objects to be read from or written to a stream (possibly a file). To use serialization with files, we initialize ObjectInputStream and ObjectOutputStream objects with stream objects that read from and write to filesobjects of classes FileInputStream and FileOutputStream, respectively. Initializing stream objects with other stream objects in this manner is sometimes called wrappingthe new stream object being created wraps the stream object specified as a constructor argument. To wrap a FileInputStream in an ObjectInputStream, for instance, we pass the FileInputStream object to the ObjectInputStream's constructor.
The ObjectOutput interface contains method writeObject, which takes an Object that implements interface Serializable (discussed shortly) as an argument and writes its information to an OutputStream. Correspondingly, the ObjectInput interface contains method readObject, which reads and returns a reference to an Object from an InputStream. After an object has been read, its reference can be cast to the object's actual type. As you will see in Chapter 24, Networking, applications that communicate via a network, such as the Internet, can also transmit entire objects across the network.
In this section, we create and manipulate sequential-access files using object serialization. Object serialization is performed with byte-based streams, so the sequential files created and manipulated will be binary files. Recall that binary files cannot be viewed in standard text editors. For this reason, we write a separate application that knows how to read and display serialized objects.
14.6.1. Creating a Sequential-Access File Using Object Serialization
We begin by creating and writing serialized objects to a sequential-access file. In this section, we reuse much of the code from Section 14.5, so we focus only on the new features.
Defining the AccountRecordSerializable Class
Let us begin by modifying our AccountRecord class so that objects of this class can be serialized. Class AccountRecordSerializable (Fig. 14.17) implements interface Serializable (line 7), which allows objects of AccountRecordSerializable to be serialized and deserialized with ObjectOutputStreams and ObjectInputStreams. Interface Serializable is a tagging interface. Such an interface contains no methods. A class that implements Serializable is tagged as being a Serializable object. This is important because an ObjectOutputStream will not output an object unless it is a Serializable object, which is the case for any object of a class that implements Serializable.
Figure 14.17. AccountRecordSerializable class for serializable objects.
(This item is displayed on pages 699 - 700 in the print version)
1 // Fig. 14.17: AccountRecordSerializable.java 2 // A class that represents one record of information. 3 package com.deitel.jhtp6.ch14; // packaged for reuse 4 5 import java.io.Serializable; 6 7 public class AccountRecordSerializable implements Serializable 8 { 9 private int account; 10 private String firstName; 11 private String lastName; 12 private double balance; 13 14 // no-argument constructor calls other constructor with default values 15 public AccountRecordSerializable() 16 { 17 this ( 0, "", "", 0.0 ); 18 } // end no-argument AccountRecordSerializable constructor 19 20 // four-argument constructor initializes a record 21 public AccountRecordSerializable( 22 int acct, String first, String last, double bal ) 23 { 24 setAccount( acct ); 25 setFirstName( first ); 26 setLastName( last ); 27 setBalance( bal ); 28 } // end four-argument AccountRecordSerializable constructor 29 30 // set account number 31 public void setAccount( int acct ) 32 { 33 account = acct; 34 } // end method setAccount 35 36 // get account number 37 public int getAccount() 38 { 39 return account; 40 } // end method getAccount 41 42 // set first name 43 public void setFirstName( String first ) 44 { 45 firstName = first; 46 } // end method setFirstName 47 48 // get first name 49 public String getFirstName() 50 { 51 return firstName; 52 } // end method getFirstName 53 54 // set last name 55 public void setLastName( String last ) 56 { 57 lastName = last; 58 } // end method setLastName 59 60 // get last name 61 public String getLastName() 62 { 63 return lastName; 64 } // end method getLastName 65 66 // set balance 67 public void setBalance( double bal ) 68 { 69 balance = bal; 70 } // end method setBalance 71 72 // get balance 73 public double getBalance() 74 { 75 return balance; 76 } // end method getBalance 77 } // end class AccountRecordSerializable |
In a class that implements Serializable, the programmer must ensure that every instance variable of the class is a Serializable type. Any instance variable that is not serializable must be declared transient to indicate that it is not Serializable and should be ignored during the serialization process. By default, all primitive-type variables are serializable. For variables of reference types, you must check the definition of the class (and possibly its superclasses) to ensure that the type is Serializable. By default, array objects are serializable. However, if the array contains references to other objects, those objects may or may not be serializable.
Class AccountRecordSerializable contains private data members account, firstName, lastName and balance. This class also provides public get and set methods for accessing the private fields.
Now let us discuss the code that creates the sequential-access file (Fig. 14.18Fig. 14.19). We concentrate only on new concepts here. As stated in Section 14.3, a program can open a file by creating an object of stream class FileInputStream or FileOutputStream. In this example, the file is to be opened for output, so the program creates a FileOutputStream (line 21 of Fig. 14.18). The string argument that is passed to the FileOutputStream's constructor represents the name and path of the file to be opened. Existing files that are opened for output in this manner are truncated. Note that the .ser file extension is usedwe use this file extension for binary files that contain serialized objects.
Figure 14.18. Sequential file created using ObjectOutputStream.
(This item is displayed on pages 701 - 702 in the print version)
1 // Fig. 14.18: CreateSequentialFile.java 2 // Writing objects sequentially to a file with class ObjectOutputStream. 3 import java.io.FileOutputStream; 4 import java.io.IOException; 5 import java.io.ObjectOutputStream; 6 import java.util.NoSuchElementException; 7 import java.util.Scanner; 8 9 import com.deitel.jhtp6.ch14.AccountRecordSerializable; 10 11 public class CreateSequentialFile 12 { 13 private ObjectOutputStream output; // outputs data to file 14 15 // allow user to specify file name 16 public void openFile() 17 { 18 try // open file 19 { 20 output = new ObjectOutputStream( 21 new FileOutputStream( "clients.ser" ) ); 22 } // end try 23 catch ( IOException ioException ) 24 { 25 System.err.println( "Error opening file." ); 26 } // end catch 27 } // end method openFile 28 29 // add records to file 30 public void addRecords() 31 { 32 AccountRecordSerializable record; // object to be written to file 33 int accountNumber = 0; // account number for record object 34 String firstName; // first name for record object 35 String lastName; // last name for record object 36 double balance; // balance for record object 37 38 Scanner input = new Scanner( System.in ); 39 40 System.out.printf( "%s %s %s %s ", 41 "To terminate input, type the end-of-file indicator ", 42 "when you are prompted to enter input.", 43 "On UNIX/Linux/Mac OS X type d then press Enter", 44 "On Windows type z then press Enter" ); 45 46 System.out.printf( "%s %s", 47 "Enter account number (> 0), first name, last name and balance.", 48 "? " ); 49 50 while ( input.hasNext() ) // loop until end-of-file indicator 51 { 52 try // output values to file 53 { 54 accountNumber = input.nextInt(); // read account number 55 firstName = input.next(); // read first name 56 lastName = input.next(); // read last name 57 balance = input.nextDouble(); // read balance 58 59 if ( accountNumber > 0 ) 60 { 61 // create new record 62 record = new AccountRecordSerializable( accountNumber, 63 firstName, lastName, balance ); 64 output.writeObject( record ); // output record 65 } // end if 66 else 67 { 68 System.out.println( 69 "Account number must be greater than 0." ); 70 } // end else 71 } // end try 72 catch ( IOException ioException ) 73 { 74 System.err.println( "Error writing to file." ); 75 return; 76 } // end catch 77 catch ( NoSuchElementException elementException ) 78 { 79 System.err.println( "Invalid input. Please try again." ); 80 input.nextLine(); // discard input so user can try again 81 } // end catch 82 83 System.out.printf( "%s %s %s", "Enter account number (>0),", 84 "first name, last name and balance.", "? " ); 85 } // end while 86 } // end method addRecords 87 88 // close file and terminate application 89 public void closeFile() 90 { 91 try // close file 92 { 93 if ( output != null ) 94 output.close(); 95 } // end try 96 catch ( IOException ioException ) 97 { 98 System.err.println( "Error closing file."); 99 System.exit( 1 ); 100 } // end catch 101 } // end method closeFile 102 } // end class CreateSequentialFile |
Figure 14.19. Testing class CreateSequentialFile.
(This item is displayed on page 703 in the print version)
1 // Fig. 14.19: CreateSequentialFileTest.java 2 // Testing class CreateSequentialFile. 3 4 public class CreateSequentialFileTest 5 { 6 public static void main( String args[] ) 7 { 8 CreateSequentialFile application = new CreateSequentialFile(); 9 10 application.openFile(); 11 application.addRecords(); 12 application.closeFile(); 13 } // end main 14 } // end class CreateSequentialFileTest
|
Common Programming Error 14.2
It is a logic error to open an existing file for output when, in fact, the user wishes to preserve the file. |
Class FileOutputStream provides methods for writing byte arrays and individual bytes to a file. In this program we wish to write objects to a filea capability not provided by FileOutputStream. For this reason, we wrap a FileOutputStream in an ObjectOutputStream by passing the new FileOutputStream object to the ObjectOutputStream's constructor (lines 2021). The ObjectOutputStream object uses the FileOutputStream object to write objects into the file. Lines 2021 might throw an IOException if a problem occurs while opening the file (e.g., when a file is opened for writing on a drive with insufficient space or when a read-only file is opened for writing). If so, the program displays an error message (lines 2326). If no exception occurs, the file is open and variable output can be used to write objects to the file.
This program assumes that data is input correctly and in the proper record-number order. Method addRecords (lines 3086) performs the write operation. Lines 6263 create an AccountRecordSerializable object from the data entered by the user. Line 64 calls ObjectOutputStream method writeObject to write the record object to the output file. Note that only one statement is required to write the entire object.
Method closeFile (lines 89101) closes the file. Method closeFile calls ObjectOutputStream method close on output to close both the ObjectOutputStream and its underlying FileOutputStream (line 94). Note that the call to method close is contained in a try block. Method close throws an IOException if the file cannot be closed properly. In this case, it is important to notify the user that the information in the file might be corrupted. When using wrapped streams, closing the outermost stream also closes the underlying file.
In the sample execution for the program in Fig. 14.19, we entered information for five accountsthe same information shown in Fig. 14.10. The program does not show how the data records actually appear in the file. Remember that now we are using binary files, which are not humanly readable. To verify that the file has been created successfully, the next section presents a program to read the file's contents.
14.6.2. Reading and Deserializing Data from a Sequential-Access File
As discussed in Section 14.5.2, data is stored in files so that it may be retrieved for processing when needed. The preceding section showed how to create a file for sequential access using object serialization. In this section, we discuss how to read serialized data sequentially from a file.
The program in Fig. 14.20Fig. 14.21 reads records from a file created by the program in Section 14.6.1 and displays the contents. The program opens the file for input by creating a FileInputStream object (line 21). The name of the file to open is specified as an argument to the FileInputStream constructor. In Fig. 14.18, we wrote objects to the file, using an ObjectOutputStream object. Data must be read from the file in the same format in which it was written. Therefore, we use an ObjectInputStream wrapped around a FileInputStream in this program (lines 2021) If no exceptions occur when opening the file, variable input can be used to read objects from the file.
Figure 14.20. Sequential file read using an ObjectInputStream.
(This item is displayed on pages 704 - 705 in the print version)
1 // Fig. 14.20: ReadSequentialFile.java 2 // This program reads a file of objects sequentially 3 // and displays each record. 4 import java.io.EOFException; 5 import java.io.FileInputStream; 6 import java.io.IOException; 7 import java.io.ObjectInputStream; 8 9 import com.deitel.jhtp6.ch14.AccountRecordSerializable; 10 11 public class ReadSequentialFile 12 { 13 private ObjectInputStream input; 14 15 // enable user to select file to open 16 public void openFile() 17 { 18 try // open file 19 { 20 input = new ObjectInputStream( 21 new FileInputStream( "clients.ser" ) ); 22 } // end try 23 catch ( IOException ioException ) 24 { 25 System.err.println( "Error opening file." ); 26 } // end catch 27 } // end method openFile 28 29 // read record from file 30 public void readRecords() 31 { 32 AccountRecordSerializable record; 33 System.out.printf( "%-10s%-12s%-12s%10s ", "Account", 34 "First Name", "Last Name", "Balance" ); 35 36 try // input the values from the file 37 { 38 while ( true ) 39 { 40 record = ( AccountRecordSerializable ) input.readObject(); 41 42 // display record contents 43 System.out.printf( "%-10d%-12s%-12s%10.2f ", 44 record.getAccount(), record.getFirstName(), 45 record.getLastName(), record.getBalance() ); 46 } // end while 47 } // end try 48 catch ( EOFException endOfFileException ) 49 { 50 return; // end of file was reached 51 } // end catch 52 catch ( ClassNotFoundException classNotFoundException ) 53 { 54 System.err.println( "Unable to create object." ); 55 } // end catch 56 catch ( IOException ioException ) 57 { 58 System.err.println( "Error during read from file." ); 59 } // end catch 60 } // end method readRecords 61 62 // close file and terminate application 63 public void closeFile() 64 { 65 try // close file and exit 66 { 67 if ( input != null ) 68 input.close(); 69 } // end try 70 catch ( IOException ioException ) 71 { 72 System.err.println( "Error closing file." ); 73 System.exit( 1 ); 74 } // end catch 75 } // end method closeFile 76 } // end class ReadSequentialFile |
Figure 14.21. Testing class ReadSequentialFile.
1 // Fig. 14.21: ReadSequentialFileTest.java 2 // This program test class ReadSequentialFile. 3 4 public class ReadSequentialFileTest 5 { 6 public static void main( String args[] ) 7 { 8 ReadSequentialFile application = new ReadSequentialFile(); 9 10 application.openFile(); 11 application.readRecords(); 12 application.closeFile(); 13 } // end main 14 } // end class ReadSequentialFileTest
|
The program reads records from the file in method readRecords (lines 3060). Line 40 calls ObjectInputStream method readObject to read an Object from the file. To use AccountRecordSerializable-specific methods, we downcast the returned Object to type AccountRecordSerializable. Method readObject tHRows an EOFException (processed at lines 4851) if an attempt is made to read beyond the end of the file. Method readObject throws a ClassNotFoundException if the class for the object being read cannot be located. This might occur if the file is accessed on a computer that does not have the class. Figure 14.21 contains method main (lines 613), which opens the file, calls method readRecords and closes the file.