2008/04/02

TypeSafe Enum and equals()

At first it seems a No Brainer Problem

If you assign the same pre-initialized instance of the same class to two variables, do they always equal to each other? Note that this is the typical pre-jdk1.5 typesafe enum pattern.

public class ReagentType implements Serializable{
private static final String PROTEIN_TYPE = "PROTEIN";
// more types here

public ReagentType PROTEIN = new ReagentType(PROTEIN_TYPE);
// more pre-initialized instance here

private String type;

public String getType(){
return this.type;
}

private ReagentType(String type){
this.type = type;
}

public void checkEquals(ReagentType a, ReagentType b){
System.out.println(a.equals(b));
}

public static void main(String[] args){
ReagentType a = ReagentType.PROTEIN;
ReagentType b = ReagentType.PROTEIN;

checkEquals(a,b);
}

}
Sounds like a no brainer. The answer obviously should be yes. checkEquals() method will print out "true". Always. That is, in an ideal world.

However...

In real world, assuming a & b are both assigned to ReagentType.PROTEIN, in many cases we will see checkEquals() method prints out "false". The standard java equals() compares object identity, which is nothing but the memory location of the object instance, which will most certainly fail in a real world deployment environment.
  1. Consider Distributed Computing. Either a or b has travelled the wire before checkEquals() method gets it, a.equals(b) is false. They resides in two different memory location.
  2. Even in a single server, after serializable/deserialization, it's guranteed that an object will have a different memory location. a equals() the serialize/deserialized copy of itself, is false.
  3. Consider an EJB invoking a method in another EJB, if they're packaged in different deployment JARs and both contains the ReagentType class, depends how the class loading mechanism in app-server works, there might exists many pre-initialized instance of the REAGENT object and they have different memory location.
Solutions.

Oh yeah, there's always solutions. For one, implement the equals() method properly.
public boolean equals(Object obj){
if (obj!=null && obj instanceof ReagentType){
return getType().equals( ((ReagentType)obj).getType());
} else {
return false;
}
}

// hashcode() methods omitted here
For two, for those who're not big fan of the typesafe enum pattern, they can simply fall back to the good old set-of-static-primitive-values enumeration.

A note is that while implementation of the RMI readResolve() method might seem to solve this problem, it actually will fail in scenario 3, since the container may decide not to perform serialize/deserialize if the EJBs resides in the same server. However the readResolve() solution should be safe in non-EJB situation where the class loading situation is simpler.

Well well... why another J2EE blog? I benefited from other people's technical blogs, and guess what, it's a good idea to contribute some of my works too. Hope it's helpful and useful, to all of your folks.