Comparing Deserialized Objects 

COMP 310    Java Resources  Eclipse Resources

(Back to the RMI Overview)

In a networked application, we are often serializing objects and sending around the globe.   Sometimes we need to compare those objects against each other.  But the problem is that the equals() method, inherited from Object, by default, uses a direct comparison of the hashcode of the object, which in turn is derived from its memory location.  This means that two deserialized instances of the same original object will return false if compared using equals()!

What we must do is to override the equals() method and the hashCode() method of the original object so they will return true if deserialized instances of the same original object are compared.  (The Java standard is that if two objects are equal, then they must return the same hash code.)

Technique #1:  Objects are equal if all their fields return values that are equal

Here is some example code for an IPerson implementation where there are two available fields, the name and IP address:

  /**
   * IPersons holding the same values will be equal
   * regardless of implementation
   */
  public boolean equals(Object o) {
    if(null != o) {
      if(o instanceof IPerson) {
        return ((IPerson)o).getName().equals(getName()) 
          && ((IPerson)o).getIPAddress().equals(getIPAddress());
      }
    }
    return false;
  }
  
  /**
   * IPersons holding the same values will have the
   * same hashcode, regardless of implementation
   */
  public int hashCode() {
    return (getName()+getIPAddress()).hashCode();
  }
  

NOTE: The above code utilizes the fact that the String class in java overrides the default hashCode method inherited from Object to return a hash code that is determined purely and completely by the letters in the String and the order in which they are in the String .    Therefore different instances of the same string value will always be equal and have the same hash code.

Pros:

Cons:

 

Technique #2: Include a Universally Unique Identifier (UUID)

A Universally Unique Identifier (also called a Globally Unique Identifier or GUID in other systems) is a 128-bit value (4x size of an int) that is guaranteed (to within a very small probability) of being unique no matter who creates it or when.   This enables us to give an instance a unique identifying value that will be the same even after it has been transmitted across the net.   Most large enterprise systems use this technique because it scales well.

  /**
   * Universally Unique IDentifier, used to create
   * an ID value that is unique across different machines.
   */
  private java.util.UUID uuid = java.util.UUID.randomUUID();
  
  /**
   * Insures that unmarshalled copies of this object will 
   * still be equal to the original.
   * Two rooms with the same values may still not be equal.
   */
  public boolean equals(Object o){
    if(null != o) {
      if( o instanceof Room) {
        return uuid.equals(((Room)o).uuid);
      }
    }  
    return false;
  }
     
  /**
   * Insures that unmarshalled copies still have the 
   * same hashcode as the original
   * Two rooms with the same values may still 
   * not have the same hashcode.
   */
  public int hashCode() {
    return uuid.hashCode();
  }

 

Pros:

Cons:

 


Comparing Remote Objects

UPDATE:   In Java 8 (as of 12/2015), RMI stub objects support .equals() and .hashCode() such that different instances of stubs that refer back to the same RMI Server objects will properly be comparable as equal and will generate the same hash code.   Thus, RMI Remote stubs can be used directly as keys in hash tables, etc. without any need for proxying or other techniques as described below.    

Refs:  

 Note that the documentaion does not imply that if the RMI Server object has a custom override for .equals() and .hashCode() that the stub will make a network call back to the server to run the corresponding method on the RMI Server object.    All the official documentation says is that if two stubs refer back to the same RMI Server object, then stub1.equals(stub2) == true and stub1.hashCode().equals(stub2.hashCode()) == true

This is NOT true in general for serialized objects unless their implementation code explicitly overrides .equals() and .hashCode() so that equality comparison and hash code generation are only dependent on the field values inside the object!

 

THE FOLLOWING DISCUSSION ONLY PERTAINS TO ADVANCED EQUALITY TESTING SITUATIONS WHERE EQUALITY CANNOT BE SIMPLY DETERMINED BY STUBS REFERRING TO THE SAME RMI SERVER OBJECT.

The problem with Remote objects, where a stub gets sent from the server to the client, is that unlike serialized objects, an entire, fully functional copy of the original instance does NOT get deserialized and instantiated on the client.  

The deserialized stub object forwards all methods that throw RemoteException to the skeleton object on the server, which then forwards the method call to the original instantiation of the object.   Since the stub is an Object instance, it does locally retain all those abilities inherited from Object.

In particular, the equals() and hashCode() methods, which are inherited from Object are problematic.   They cannot be overridden to be remote calls because that would entail adding the ability to throw the RemoteException exception, but the rules of Java forbid overriding a method to throw an exception.  By default, as inherited from Object, equals() compares the hashCode of the instances being compared.  But by default, the hashCode() method simply returns a number based upon the memory location of that object.

Thus if one were to use equals() to compare any two remote objects, the result would always be false because two stubs are fundamentally two different instances stored at different memory locations, even if they were originally generated from the same instance on the server. 

The problem here is that our stub, our "proxy" in design pattern-speak, is too dumb.  We need to send a more intelligent proxy.   The solution is to use the Decorator Design Pattern, which wraps the original dumb stub and provides more functionality, i.e. in the equals() and hashCode() methods.

There are a couple of ways to do this, depending on whether or not the problem is on the sending or the receiving end.

Receiving End Work-Around

In this case, we only need to make a proxy for our own private use.  This makes life easier.   The solution depends on whether or not you have access to the actual comparison process:

All we have to do is to wrap every incoming RMI stub in a proxy that overrides the equals and hashCode methods.

	/**
	 * Private class to decorate an IPerson to override the equals() and hashCode() 
	 * methods so that a dictionary, e.g. Hashtable, can properly compare IUsers.
	 */
	private class ProxyPerson implements IPerson {
		
		/**
		 * The decoree
		 */
		private IPerson stub;

		/**
		 * Constructor for the class
		 * @param stub The decoree
		 */
		public ProxyPerson(IPerson stub){
			this.stub = stub;
		}
		
		/**
		 * No-op decoration of the getName method.   Just pass the request to the decoree.
		 * @return The name of the person.
		 * @throws RemoteException
		 */
		@Override
		public String getName() throws RemoteException {
			return stub.getName();
		}
		
		/**
		 * No-op decoration of the getIPAdress method.   Just pass the request to the decoree.
		 * @return The IP address of the person
		 * @throws RemoteException
		 */
		@Override
		public String getIPAddress() throws RemoteException {
			return stub.getIPAddress();
		}

		/**
		 * Get the decoree
		 * @return the decoree
		 */
		public IUser getStub() {
			return stub;
		}


		/**
		 * Overriden equals() method to simply compare hashCodes.
		 * @return  Equal if the hashCodes are the same.  False otherwise.
		 */
		@Override
		public boolean equals(Object other){
			return hashCode() == other.hashCode();
		}
		

		/**
		 * Overriden hashCode() method to create a hashcode from all the accessible values in IPerson.
		 * @return a hashCode tied to the values of this IPerson.	
		 */
		@Override
		public int hashCode(){
			try {
			    // generate a unique hashCode.
				return stub.getName().hashCode()+stub.getIPAddress().hashCode();
			} catch (RemoteException e) {
				// Deal with the exception without throwing a RemoteException.
				System.err.println("ProxyPerson.hashCode(): Error calling remote method on IPerson stub: "+e);
				e.printStackTrace();
				return super.hashCode();
			}
		}
		
	}

Simply wrap every IPerson object in a ProxyPerson and use them as if they weren't Remote objects, except  that they will properly compare their values.

It is recommended that the proxy wrapper be removed before an IPerson object is sent to an outside entity by retrieving the stub from inside the ProxyPerson instance.   This keeps the modified comparison process private and encapsulated. 

 

 

Sending End Work-Around

In this situation, one cannot assume that the receiving end realizes that there is a problem, so we must fundamentally send over something that is properly comparable.

The trick is to separate the Remote interface from the interface that defines the remote behavior of the stub/proxy (IChat below).  That is, when sending the stub, don't send the stub made by exporting the Remote object, but rather, send a non-Remote instance of our own, custom proxy class.   We don't want the proxy to be Remote because Java will try to create and send a stub of it when we try to transmit it and what we want is to send the entire proxy object.

UML diagram of smart proxy class

In the above example, we have a class Chat, that would normally implement the IChat interface, and via that interface, inherit from the Remote interface.   Instead, above, Chat implements the IChat_Impl interface, with combines the IChat and Remote interfaces.   And yes, IChat_Impl adds no new functionality.   It's only purpose is to create the direct, in-line hierarchy from Remote to Chat that includes IChat as required for the proper functioning of the stub exporting mechanism of UnicastRemoteObject.exportObject()

 

Below is the code for the smart stub itself.   All the calls that throw RemoteException are simply extensions of those found on the dumb stub, so hence, they simply forward the method call to the original dumb stub.   The equals() and hashCode() methods use the UUID method from above.   In theory those methods could use any method they believed was right, including dispatching the call across to be processed back on the server.  Here, all equality processing takes place locally on the client, to maximize speed and to reduce the overall network traffic.

import java.rmi.*;
import chat.*;

/**
 * A "smart proxy" that wraps the normal "dumb" stub of a Chat 
 * instance.   All remote operations are simply forwarded to the 
 * composed stub.  The equals() and hashCode() methods are overridden
 * to make this instance of the smart proxy unique, no matter 
 * how many times it has been serialized and sent over to another 
 * machine.  Any deserialization of this stub will compare true
 * with any other deserialization of this stub.
 */
class Chat_Proxy implements IChat {
  /**
   * Needed for cross-version compatibility of deserialization
   */
  private static final long serialVersionUID = 200710262L;
  
  /**
   * The composed Chat stub object
   */
  private IChat c_impl_stub;
  
  /**
   * Constructor for the class
   * @param c_impl_stub The regular, "dumb" stub of a Chat object.
   */
  Chat_Proxy(IChat c_impl_stub) {
    this.c_impl_stub = c_impl_stub;
  }
  
  /**
   * Simply delegates to the c_impl_stub object
   */
  public void append(String s) throws RemoteException {
    System.out.println("Chat_Proxy.append: "+s);
    c_impl_stub.append(s);
  }

  /**
   * Simply delegates to the c_impl_stub object
   */
  public String joinRoom(IChat me) throws RemoteException {
    return c_impl_stub.joinRoom(me);
  }

  /**
   * Simply delegates to the c_impl_stub object
   */
  public IPerson getPerson() throws RemoteException {
    return c_impl_stub.getPerson();
  }
  
  /**
   * Unique id value that identifies this instance of the proxy
   * object
   */
  private java.util.UUID uuid = java.util.UUID.randomUUID();
  
  /**
   * Compares the UUID of this instance with the UUID of the other
   * instance.
   * @param o the other object to compare with
   * @return true if both objects are IChat_Proxy instances 
   * and both have the same uuid value.
   */
  public boolean equals(Object o){
    if( o instanceof Chat_Proxy) {
      return uuid.equals(((Chat_Proxy)o).uuid);
    }
    else {   
      return false;
    }
  }
  
  /**
   * Returns the hashCode of the UUID
   */
  public int hashCode() {
    return uuid.hashCode();
  }
}

 

Here is an example of a factory method that is employed to encapsualate the creation of the smart proxy.  Since the return value of the factory method is an IChat-compliant stub instance, the return value can be used wherever the dumb stub was used originally.   One only needs to replace the creation of the original Chat instance and its dumb stub wherever they is found with a call to this factory method.

  /**
   * Create a "smart stub" of a Chat object that overrides 
   * equals() and hashCode() such that desrialized versions 
   * of the same stub will always compare to be equal.
   */
  public IChat makeChatStub(Chat c_impl) {
    try {
      // Make the dumb stub 
      IChat c_impl_stub = (IChat) UnicastRemoteObject.exportObject(c_impl, getNextPort());      
      // Make the smart stub
      IChat c = new Chat_Proxy(c_impl_stub);  
      
      IChat c = new Chat_Proxy(c_impl_stub);  // Make the smart stub
      view.append("ChatModel.makeChatStub: c = "+c+"\n");
      return c; // return the smart stub
    }
    catch(RemoteException e) {
      view.append("Exception creating chat stub: "+e+"\n");
      // uses another similar routine that 
      // makes a null object instance of IChat
      return makeNullChatStub(); 
    }
  }

 

The above code assumes that there is a null object instance of IChat that gives some well-defined behavior for use when a valid instance of IChat is required but unattainable.

More information on using smart proxies can be found at http://www.javaworld.com/javaworld/jw-11-2000/jw-1110-smartproxy.html?page=3

 

 

© 2020 by Stephen Wong