Object Streams


Java provides the capability to directly read and write objects from and to streams. Java can write an object to an object output stream by virtue of serializing it. Java serializes an object by converting it into a sequence of bytes. The ability to serialize objects is the basis for Java's RMI (Remote Method Invocation) technology. RMI allows objects to communicate with other objects on remote systems as if they were local. RMI in turn provides the basis for Java's EJB (Enterprise Java Bean) technology for component-based computing.

You write and read objects using the classes ObjectOutputStream and ObjectInputStream. As a quick demonstration, the following code is a rewrite of the the methods store and load in the CourseCatalog class. The modified code uses object streams instead of data streams.

 public void store(String filename) throws IOException {    ObjectOutputStream output = null;    try {       output =          new ObjectOutputStream(new FileOutputStream(filename));       output.writeObject(sessions);    }    finally {       output.close();    } } public void load(String filename)       throws IOException, ClassNotFoundException { ObjectInputStream input = null;    try { input = new ObjectInputStream(new FileInputStream(filename)); sessions = (List<Session>)input.readObject();    }    finally { input.close();    } } 

The test remains largely unchanged.

 public void testStoreAndLoad() throws Exception {    final String filename = "CourseCatalogTest.testAdd.txt";    catalog.store(filename);    catalog.clearAll();    assertEquals(0, catalog.getSessions().size());    catalog.load(filename);    List<Session> sessions = catalog.getSessions();    assertEquals(2, sessions.size());    assertSession(session1, sessions.get(0));    assertSession(session2, sessions.get(1)); } 

The throws clause on the test method signature must change, since the load method now throws a ClassNotFoundException. Within the context of this example, it doesn't seem possible for a ClassNotFoundException to be generated. You store a List of Session objects and immediately read it back, and both java.util.List and Session are known to your code. An exception could be thrown, however, if another application with no access to your Session class were to read the objects from the file.

When you run the test, you should receive an exception:

 java.io.NotSerializableException: studentinfo.CourseSession 

In order to write an object to an object stream, its class must be serializable. You mark a class as serializable by having it implement the interface java.io.Serializable. Most of the classes in the Java system class library that you would expect to be serializable are already marked as such. This includes the String and Date classes as well as all collection classes (HashMap, ArrayList, and so on). But you will need to mark your own application classes:

 abstract public class Session       implements Comparable<Session>,                  Iterable<Student>,                  java.io.Serializable {... 

Don't forget the Course class, since Session encapsulates it:

 public class Course implements java.io.Serializable {... 

When you mark the abstract superclass as serializable, all its subclasses will also be serializable. The Serializable interface contains no method definitions, so you need not do anything else to the Session class.

An interface that declares no methods is known as a marker interface. You create marker interfaces to allow a developer to explicitly mark a class for a specific use. You must positively designate a class as capable of being serialized. The Serializable marker is intended as a safety mechanismyou may want to prevent certain objects from being serialized for security reasons.

Transient

Course sessions allow for enrollment of students. However, you don't want the course catalog to be cluttered with student objects. Suppose, though, that a student has enrolled early, before the catalog was created. The setUp method in CourseCatalogTest enrolls a student as an example:

 protected void setUp() {    catalog = new CourseCatalog();    course1 = new Course("a", "1");    course2 = new Course("a", "1");    session1 =       CourseSession.create(          course1, DateUtil.createDate(1, 15, 2005));    session1.setNumberOfCredits(3);    session2 =       CourseSession.create(          course2, DateUtil.createDate(1, 17, 2005));    session2.setNumberOfCredits(5);    session2.enroll(new Student("a"));    catalog.add(session1);    catalog.add(session2); } 

If you run your tests, you again receive a NotSerializableException. The course session now refers to a Student object that must be serialized. But the Student class does not implement java.io.Serializable.

Instead of changing Student, you can indicate that the list of students in Session is to be skipped during serialization by marking them with the TRansient modifier.

 abstract public class Session       implements Comparable<Session>,                  Iterable<Student>,                  java.io.Serializable {    ...    private transient List<Student> students = new ArrayList<Student>(); 

The list of students will not be serialized in this example. Your tests will now pass.

Serialization and Change

Serialization makes it easy to persist objects. Too easy, perhaps. There are many implications to declaring a class as Serializable. The most significant issue is that when you persist a serialized object, you export with it a definition of the class as it currently exists. If you subsequently change the class definition, then attempt to read the serialized object, you will get an exception.

To demonstrate, add a name field to Session.java. This creates a new version of the Session class. Don't worry about a test; this is a temporary "spike," or experiment. You will delete this line of code in short time. Also, do not run any testsdoing so will ruin the experiment.

 abstract public class Session       implements Comparable<Session>,                  Iterable<Student>,                  java.io.Serializable {    private String name;    ... 

Your last execution of tests persisted an object stream to the file named CourseCatalogTest.testAdd.txt. The object stream stored in this file contains Session objects created using the older definition of Session without the name field.

Then create an entirely new test class, studentinfo.SerializationTest:

 package sis.studentinfo; import junit.framework.*; public class SerializationTest extends TestCase {    public void testLoadToNewVersion() throws Exception {       CourseCatalog catalog = new CourseCatalog();       catalog.load("CourseCatalogTest.testAdd.txt");       assertEquals(2, catalog.getSessions().size());    } } 

The test tries to load the persisted object stream. Execute only this test. Do not execute your AllTests suite. You should receive an exception that looks something like:

 testLoadToNewVersion(studentinfo.SerializationTest)    java.io.InvalidClassException: studentinfo.Session;    local class incompatible:       stream classdesc serialVersionUID = 5771972560035839399,       local class serialVersionUID = 156127782215802147 

Java determines compatibility between the objects stored in the output stream and the existing (local) class definition. It considers the class name, the interfaces implemented by the class, its fields, and its methods. Changing any of these will result in incompatibility.

Transient fields are ignored, however. If you change the declaration of the name field in Session to transient:

 private transient String name; 

SerializationTest will then pass.

Serial Version UID

The InvalidClassException you received referred to a serialVersionUID for both the stream's class definition and the local (current Java) class definition. In order to determine whether the definition of a class has changed, Java generates the serialVersionUID based on the class name, interfaces implemented, fields, and methods. The serialVersionUID, a 64-bit long value, is known as a stream unique identifier.

You can choose to define your own serialVersionUID instead of using the one Java generates. This may give you some ability to better control version management. You can obtain an initial serialVersionUID by using the command-line utility serialver or you can assign an arbitrary value to it. An example execution of serialver:

 serialver -classpath classes studentinfo.Session 

You optionally specify the classpath, followed by the list of classes for which you wish to generate a serialVersionUID.

Remove the name field from Session. Rebuild and rerun your entire test suite. Add a serialVersionUID definition to Session. At the same time, add back the name field.

 abstract public class Session       implements Comparable<Session>,                  Iterable<Student>,                  java.io.Serializable {    public static final long serialVersionUID = 1L;    private String name;    ... 

Then run only SerializationTest. Even though you've added a new field, the version ID is the same. Java will initialize the name field to its default value of null. If you change the serialVersionUID to 2L and rerun the test, you will cause the stream version (1) to be out of synch with the local class version (2).

Creating a Custom Serialized Form

Your class may contain information that can be reconstructed based on other data in the class. When you model a class, you define its attributes to represent the logical state of every object of that class. In addition to those attributes, you may have data structures or other fields that cache dynamically computed information. Persisting this dynamically calculated data may be slow and/or a grossly inefficient use of space.

Suppose you need to persist not only the course sessions but also the students enrolled in each session. Students carry a large amount of additional data, and they are already being persisted elsewhere. You can traverse the collection of course sessions and persist only the unique identifier for each student to the object stream.[1] When you load this compacted collection, you can execute a lookup to retrieve the complete student object and store it in the course session.

[1] We have a small school. We don't admit anyone with the same last name as another student, so you can use that as your unique identifier.


To accomplish this, you will define two methods in Session, writeObject and readObject. These methods are hooks that the serialization mechanism calls when reading and writing each object to the object stream. If you don't supply anything for these hooks, default serialization and deserialization takes place.

First, change the test in CourseCatalogTest to ensure that the enrolled student was properly persisted and restored.

 public void testStoreAndLoad() throws Exception {    final String filename = "CourseCatalogTest.testAdd.txt";    catalog.store(filename);    catalog.clearAll();    assertEquals(0, catalog.getSessions().size());    catalog.load(filename);    List<Session> sessions = catalog.getSessions();    assertEquals(2, sessions.size());    assertSession(session1, sessions.get(0));    assertSession(session2, sessions.get(1));    Session session = sessions.get(1);    assertSession(session2, session);    Student student = session.getAllStudents().get(0);    assertEquals("a", student.getLastName()); } 

Make sure that the students field in Session is marked as TRansient. Then code the writeObject definition for Session:

 private void writeObject(ObjectOutputStream output)       throws IOException {    output.defaultWriteObject();    output.writeInt(students.size());    for (Student student: students)       output.writeObject(student.getLastName()); } 

The first line of writeObject calls the method defaultWriteObject on the stream. This will write every nontransient field to the stream normally. Subsequently, the code in writeObject first writes the number of students to the stream, then loops through the list of students, writing each student's last name to the stream.

 private void readObject(ObjectInputStream input)       throws Exception {    input.defaultReadObject();    students = new ArrayList<Student>();    int size = input.readInt();    for (int i = 0; i < size; i++) {       String lastName = (String)input.readObject();       students.add(Student.findByLastName(lastName));    } } 

On the opposite end, readObject first calls defaultReadObject to load all nontransient fields from the stream. It initializes the transient field students to a new ArrayList of students. It reads the number of students into size and iterates size times. Each iteration extracts a student's last name from the stream. The code looks up and retrieves a Student object using this last name and stores the Student in the students collection.

In real life, the findByLastName method might involve sending a message to a student directory object, which in turn retrieves the appropriate student from a database or another serialization file. For demonstration purposes, you can provide a simple implementation that will pass the test:

 public static Student findByLastName(String lastName) {    return new Student(lastName); } 

Serialization Approaches

For classes whose definitions are likely to change, dealing with serialization version incompatibility issues can be a major headache. While it is possible to load serialized objects from an older version, it is difficult. Your best tactics include:

  • minimizing use of serialization

  • maximizing the number of transient fields

  • identifying versions with serialVersionUID

  • defining a custom serialization version

When you serialize an object, you are exporting its interface. Just as you should keep interfaces as abstract and unlikely to change as possible, you should do the same with serializable classes.



Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net