- Special Edition Using Java, 2nd Edition -

Chapter 40

Object Serialization and Remote Method Invocation


by Joe Weber

Normally when you open a stream to and from a client program (such as an applet), the odds are fairly good that you are sending/receiving a byte. You're probably then adding that byte to a String. Or, you may instead be opening a stream, reading in a bunch of data, and then using it to fill out the contents of a new object (by passing the read-in elements to the constructor). Wouldn't it be great if, instead, you could grab an object a whole class at a time?

That's exactly what object serialization is all about. Do you have a class structure that holds all of the information about a house for a real estate program? No problem—simply open the stream and send or receive the whole house. Do you want to save the state of a game Applet? Again, no problem. Just send the Applet object down the stream.

Object Serialization

The ability to store and retrieve whole objects is essential to the construction of all but the most ephemeral of programs. While a full-blown database might be what you need if you're storing large amounts of data, frequently that's overkill. Even if you want to implement a database, it would be easier to store objects as BLOB types (byte streams of data) than to break out an int here, a char there, and a byte there.

The key to object serialization is to store enough data about the object to be able to reconstruct it fully. Furthermore, to protect the user (and programmer), the object must have a "fingerprint" that correctly associates it with the legitimate object from which it was made. This is critical so that when an object is read in from a stream, each of its fields will be placed back into the correct class in the correct location.

If you are a C or C++ programmer, you're probably used to taking the pointer to a class or struct, doing a sizeOf(), and writing out the entire class. Java does not support pointers or direct-memory access, so object serialization is required.
 

It's not necessary, however, to store the methods or the transient fields of a class. The class code is assumed to be available any time these elements are required.

Objects frequently refer to other objects by using them as class variables (fields). In order to save a class, it is also necessary to save the contents of these reference objects. Of course, the reference objects may also refer to yet even more objects. So, as a rule, to serialize an object completely, you must store all of the information for that object, as well as every object that is reachable by the object.

Object serialization and Remote Method Invocation are not available under Netscape Navigator 3.0 Microsoft Internet Explorer 3.0 or the JDK 1.0. The first version of the JDK that supports object serialization and RMI is JDK 1.02.
 

Obtaining the RMI/Object Serialization Classes

In order to use RMI or object serialization, you must add several classes to your class library. These classes can be downloaded from the following URL:

Object Serialization Example

As a simple example, let's store and retrieve a Date class to and from a file. To do this without object serialization, you would probably do something on the order of getTime() and write the resulting long integer to the file. However, with object serialization, the process is much, much easier.

An Application to Write a Date Class

First, declare an Application that will write the Date to a file (as shown in listing 40.1).

Listing 40.1 DateWrite.java—an application that writes a Date object to a file.

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
public class DateWrite {
public static void main (String args[]){
try{
// Serialize today's date to a file.
FileOutputStream outputFile = new FileOutputStream("dateFile");
ObjectOutputStream serializeStream = new ObjectOutputStream(outputFile);
serializeStream.writeObject("Hi!");
serializeStream.writeObject(new Date());
serializeStream.flush();
} catch (Exception e) {
System.out.println("Error during serialization");
}
}
}//end class DateWrite

Let's take a look at the code in listing 40.1. First, notice that the program creates a FileOutputStream. It is necessary to first declare an outputStream of some sort to which you will attach the ObjectOutputStream. (As you see later in listing 40.3, you can also use the OutputStream generated from any other object, including a URL.)

Once you have established a stream, it is necessary to create an ObjectOutputStream with it. The ObjectOutputStream contains all of the necessary information to serialize any object and to write it to the stream.

In the example of the previous short code fragment, you see two objects being written to the stream. The first object that is written is a String object; the second is the Date object.

Compiling DateWrite

To compile listing 40.1, you need to add some extra commands that you're probably not used to. Before you do this, though, first verify that you have downloaded the RMI/object serialization classes and unzipped the file into your Java directory. Now, type the command:

The previous compiler command assumes that you are using a Windows machine and that the directory in which your Java files exist is C:\JAVA. If you have placed it in a different location or are using a system other than Windows, you need to substitute C:\JAVA\LIB with the path that is appropriate for your Java installation. As always, it's a good idea to take a look at the README file included with your instillation, and to read the release notes to learn about any known bugs or problems.
 
This should compile DateWrite cleanly. If you receive an error, though, make sure that you have a OBJIO.ZIP file in your JAVA\LIB directory. Also, make sure that you have included both the CLASSES.ZIP and the OBJIO.ZIP files in your class path.
 

Running DateWrite

Once the file is compiled, you can run it. However, just as you had to include the OBJIO.ZIP file in the classpath when you compiled the DateWrite class, you must also include it in order to run the class.

java -classpath c:\java\lib\classes.zip;c:\java\lib\objio.zip;. DateWrite

If you fail to include the OBJIO.ZIP file in your class path, you will likely get an error such as:
 
java.lang.NoClassDefFoundError: java/io/ObjectOutputStream
at DateWrite.main (DateWrite.java: 9)
 
This is the result of the virtual machine being unable to locate the class files that are required for object serialization.
 

No real output is generated by the DateWrite class, so when you run this program, you should be returned to the command prompt fairly quickly. However, if you now look in your directory structure, you should see a file called dateFile. This is the file you just created. If you attempt to type out the file, you will see something that looks mostly like gobbledy-gook.

This file contains several things. The stuff that looks like gobbledy-gook is actually what the serialization uses to store information about the class, such as the value of the fields and the class signature that was discussed earlier.

A Simple Application to Read In the Date

The next step, of course, is to read the Date and String back in from the file. Let's see how complicated this could be. Listing 40.2 shows an example program that reads in the String and Date.

Listing 40.2 DateRead.java—An application that reads the String and Date back in.

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Date;
public class DateRead {
public static void main (String args[]){
Date wasThen;
String theString;
try{
// Serialize today's date to a file.
FileInputStream inputFile = new FileInputStream("dateFile");
ObjectInputStream serializeStream = new ObjectInputStream(inputFile);
theString = (String) serializeStream.readObject();
wasThen = (Date)serializeStream.readObject();
} catch (Exception e) {
System.out.println("Error during serialization");
return;
}
System.out.println("The string is:"+theString);
System.out.println("The old date was:"+wasThen);
}
}

Listings 40.1 and 40.2 differ primarily in the ways that you would expect. Listing 40.1 is writing, and Listing 40.2 is reading. In DateRead, you first declare two variables to store the objects in. You need to remember to do this, because if you were to create the variables inside the try-catch block, they would go out of scope before reaching the System.out line. Next, a FileInputStream and ObjectInputStream are created, just as the FileOutputStream and ObjectOutputStreams were created for DateWrite.

The next two lines of the code are also probably fairly obvious, but pay special attention to the casting operator. readObject() returns an Object class. By default, Java does not polymorph-cast any object, so you must implicitly direct it to do so. The rest of the code should be fairly obvious to you by now.

To compile the code, this time let's set a classpath variable so that you don't always have to use the -classpath option with javac. To do this on a Windows machine, type:

On other platforms, the syntax is slightly different. For instance, under UNIX you might type:

In either case, don't forget to add the current director (.) to the end of the classpath statement. javac will run without the current directory being listed, but the java command won't work.

Once you have set the classpath variable, you can compile the code by typing the familiar javac command:

You can also run it just by using the familiar java command:

Here's an example of the resulting output from this code:

The String is:Hi!
The old date was:Wed Jul 31 23:36:26 edt 1996

Notice that the String and Date are read in just as they were when you wrote them out. Now you can write out and read entire objects from the stream without needing to try to push each element into the stream.

As you may have already guessed, it is imperative that you read in objects in exactly the same order as you wrote them out. If you fail to do this, a runtime error will occur that will say something such as the following:
 
Error during serialization
 

 

Reading In the Date with an Applet

Object serialization is not limited to applications. Listing 40.3 shows DateRead changed so that it can also be run as an Applet.

Listing 40.3 DataReadApp.java—An applet that reads a Date object to a file.

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.Date;
import java.awt.Graphics;
public class DateReadApp extends java.applet.Applet {
public void paint (Graphics g){
Date wasThen;
String theString;
try{
// Serialize today's date to a file.
FileInputStream inputFile = new FileInputStream("dateFile");
ObjectInputStream serializeStream = new ObjectInputStream(inputFile);
theString = (String) serializeStream.readObject();
wasThen = (Date)serializeStream.readObject();
} catch (Exception e) {
System.out.println("Error during serialization");
return;
}
g.drawString(("The string is:"+theString),5,100);
g.drawString(("The old date was:"+wasThen),5,150);
}
}

Once you have compiled listing 40.3, the resulting output should look like figure 40.1. Remember that you will have to use Appletviewer to run this applet, because other browsers don't yet support object serialization.


FIG. 40.1

The Date and String have been read in using serialization.

While you can run ReadDate with Appletviewer, you cannot run it using Netscape because some changes needed to be made to the virtual machine in order to make object serialization possible, and these changes have not yet been adopted by Netscape.
 

Writing and Reading Your Own Objects

By default, you have the ability to write and read most of your own objects, just as you did with the Date class. There are certain restrictions right now (such as if the Object refers to a native peer), but for the most part, any class that you create can be serialized.

Listings 40.4 through 40.6 show the source code for serializing an example class called SerializeObject.

Listing 40.4 SerializeObject—A simple class with a couple of fields.

public class SerializeObject{
public int first;
public char second;
public String third;
public SerializeObject (int first, char second, String third){
this.first= first;
this.second = second;
this.third = third;
}
}

Listing 40.5 ObjWrite—Write out a sample SeralizeObejct to a file.

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import SerializeObject;
public class ObjWrite {
public static void main (String args[]){
try{
// Serialize today's date to a file.
FileOutputStream outputFile = new FileOutputStream("objFile");
ObjectOutputStream serializeStream = new ObjectOutputStream(outputFile);
SerializeObject obj = new SerializeObject (1,'c',new String ("Hi!"));
serializeStream.writeObject(obj);
serializeStream.flush();
} catch (Exception e) {
System.out.println("Error during serialization");
}
}
}

Listing 40.6 ObjRead—Read in the same object from the file.

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import SerializeObject;
public class ObjRead extends java.applet.Applet {
public void init(){
main(null);
}
public static void main (String args[]){
SerializeObject obj;
try{
// Serialize today's date to a file.
FileInputStream inputFile = new FileInputStream("objFile");
ObjectInputStream serializeStream = new ObjectInputStream(inputFile);
obj = (SerializeObject)serializeStream.readObject();
} catch (Exception e) {
System.out.println("Error during serialization");
return;
}
System.out.println("first is:"+obj.first);
System.out.println("second is:"+obj.second);
System.out.println("third is:"+obj.third);
}
}

In the previous example classes, notice that the SerializeObject class refers to a number of things, including another class—String. As you might already suspect, once you have compiled and run each of these classes, the resulting output is:

First is:1
Second is:c
Third is:Hi!

What's most amazing about all of this code is how easy it is to transfer the object.

Remote Method Invocation

First, let's define what Remote Method Invocation is. With object serialization, you learned that you could take an entire object and pass it along a stream. Remote Method Invocation is a sister to object serialization that allows you to invoke methods on other systems as well.

In other words, RMI allows you to create Java objects whose methods can be invoked by the virtual machine on a different computer. The system is very similar to the Remote Procedure Call (RPC) mechanisms used frequently in other systems.

Creating a Remote Object

In order for an object's methods to be invoked remotely, the object must implement the Remote interface. Each of these objects are referred to as remote objects. There are four simple steps to implementing a remote object:

Define an interface that extends the Remote interface. Each method of this new interface must declare that it will throw a RemoteExecption.

See "Extending Interfaces," Chapter 13 for more information.
 

Define a class that implements the interface. Because the new interface extends Remote, this fulfills the requirements for making the new class a remote object. The class must provide a means to marshal references to the instances of the class. Currently, the only class available to do this is the UnicastRemoteServer.

Generate the stubs and skeletons that are needed for the remote implementations by using the rmic program.

Create a client program that will make RMI calls to the server.

Start the registry and run your remote server and client.

When parameters are required by an RMI method, the objects are passed using object serialization, as discussed in the first half of this chapter.
 

A Sample RMI Application

The first step to creating an RMI application is to create an interface, which extends the Remote interface. Each of the methods in this interface will be able to be called remotely. Listing 40.7 shows a simple remote interface.

Listing 40.7 RemoteInterface—A sample interface that extends Remote.

public interface RemoteInterface extends java.rmi.Remote {
String message (String message) throws java.rmi.RemoteException;
}

Creating an RMI Server

The second step is to define a class that implements your new RemoteInterface interface. This call is defined in listing 40.8.

Listing 40.8 RemoteServer—A sample server that receives and sends a string.

import java.rmi.Naming;
import java.rmi.server.UnicastRemoteServer;
import java.rmi.RemoteException;
import java.rmi.server.StubSecurityManager;
public class RemoteServer extends UnicastRemoteServer implements RemoteInterface{
String name;
public RemoteServer(String name) throws RemoteException{
super();
this.name = name;
}
public String message(String message) throws RemoteException{
return "My Name is:"+name+",thanks for your message:"+message;
}
public static void main (String args[]){
System.setSecurityManager (new StubSecurityManager());
try{
String myName = "Server Test";
RemoteServer theServer = new RemoteServer (myName);
Naming.rebind(myName,theServer);
} catch (Exception e){
System.out.println("An Exception occured while creating server");
}
}
}

Several key things need to be noticed about the RemoteServer class. First, the RemoteServer extends the UnicastRemoteServer. For the scope of this chapter, you can think of the UnicastRemoteServer as the java.applet.Applet for RMI servers. Next, the server implements the RemoteInterface that you defined in listing 40.7.

Each method in the RemoteServer that can be called via RMI must declare that it will throw a RemoteException. Notice that even the constructor method must be defined to throw a RemoteException.

RemoteServer must define the message method of RemoteInterface, because it implemented it. You are most concerned with this method because this is the method you try to call using RMI. To make things simple, the message method simply returns a String, which includes the message that is received. If our client program receives the String back, we can be sure that the server received our original String.

The main method of RemoteServer simply creates an instance of the server, so we can attach to it.

Compiling the RemoteSever

As with Object Serializaiton, it is once again necessary to include additional classes when compiling RemoteServer. Therefore, set your classpath as follows:

set classpath=c:\java\lib\classes.zip;c:\java\lib\rmi.zip;c:\objio.zip;.

It's not technically necessary to include the OBJIO.ZIP file at this point, but it's not a bad idea to keep it there for good measure.

You can now compile the RemoteServer by typing the following:

The next step to creating an RMI server is to create the stubs and skeletons for the RemoteServer. You can do this using the rmic compiler by typing the following:

As you can see, the syntax for the rmic compiler is nearly identical to that for the java command. The rmic compiler produces two files for you:

RemoteServer_Skel.class
RemoteServer_Stub.class

Creating a Client

The next step to creating an RMI program is to create the client that will actually invoke the remote methods. Listing 40.9 shows an example class.

Listing 40.9 RemoteClient—An example client that interfaces to the RemoteServer class.

import java.rmi.server.StubSecurityManager;
import java.rmi.Naming;
public class RemoteClient {
public static void main(String args[]){
System.setSecurityManager(new StubSecurityManager());
try{
RemoteInterface server = (RemoteInterface) Naming.lookup("Server Test");
String serverString = server.message("Hello There");
System.out.println("The server says :\n"+serverString);
} catch (Exception e){
System.out.println("Error while performing RMI");
}
}
}

The most important portions of the RemoteClient class are the two lines in the middle of the try-catch block:

RemoteInterface server = (RemoteInterface) Naming.lookup("Server Test");
String serverString = server.message("Hello There");

The first line of code looks to the registry to locate the stub called "Server Test" (if you look back to the RemoteServer program, you will see that you bound it using this name). Once the program has created an instance of the RemoteInterface, it then calls the message method with the string "Hello There." Notice that this is actually a method call! You are invoking a method on a completely different system. The method then returns a string that is stored in serverString and later printed out.

You can now compile the client program just as you did for RemoteServer:

javac RemoteClient.java

This, of course, assumes that you have already set your classpath for the RemoteServer class.

Starting the Registry and Running the Code

Before you can actually run the RemoteServer and RemoteClient classes, you must first start the RMI Registry program on the computer that will be hosting the RemoteServer. In this case, the RemoteServer and RemoteClient will be two different processes on the same computer, so it does not really matter. To start the Registry program, type:

This command will cause the RegistryImpl program to start. On UNIX machines, you can push this into the background by instead typing:

On a Windows machine, you now need to start up another MS-DOS prompt to run the server in. Once you have opened the new prompt or returned to the command line (on a UNIX machine), you can start the server by typing:

As with the Registry program, you can pass this into the background on a UNIX machine by running instead as:

Finally, you want to open one more DOS prompt and run the RemoteClient program by typing:

You should see the following output:

The Server Says:
My Name is:Server Test, thanks for your message:Hello There


Previous Page TOC Next Page

| Previous Chapter | Next Chapter |

|Table of Contents | Book Home Page |

| Que Home Page | Digital Bookshelf | Disclaimer |


To order books from QUE, call us at 800-716-0044 or 317-361-5400.

For comments or technical support for our books and software, select Talk to Us.

© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company