views:

1283

answers:

11

This question is specifically related to overriding the equals() method for objects with a large number of fields. First off, let me say that this large object cannot be broken down into multiple components without violating OO principles, so telling me "no class should have more than x fields" won't help.

Moving on, the problem came to fruition when I forgot to check one of the fields for equality. Therefore, my equals method was incorrect. Then I thought to use reflection:

--code removed because it was too distracting--

The purpose of this post isn't necessarily to refactor the code (this isn't even the code I am using), but instead to get input on whether or not this is a good idea.

Pros: If a new field is added, it is automatically included
The method is much more terse than 30 if statements

Cons: If a new field is added, it is automatically included, sometimes this is undesirable
Performance: This has to be slower, I don't feel the need to break out a profiler
Whitelisting certain fields to ignore in the comparison is a little ugly

Any thoughts?

+8  A: 

If you did want to whitelist for performance reasons, consider using an annotation to indicate which fields to compare. Also, this implementation won't work if your fields don't have good implementations for equals().

P.S. If you go this route for equals(), don't forget to do something similar for hashCode().

P.P.S. I trust you already considered HashCodeBuilder and EqualsBuilder.

Hank Gay
All of the fields are strings or [email protected]. Already did that, but it doesn't hurt to remind [email protected]. I knew there had to be something out there. I had been using the ToStringBuilder, I have no idea how I overlooked those. The real question is to reflect or not to reflect.
oreoshake
It depends on what you think is most likely: adding a field and forgetting to add it to equals() or adding a field you don't want and/or can't use in the automated comparison, though using an annotation could help. You might consider something like flexjson and comparing the output of each object.
Hank Gay
Those builders are pretty cool.
Thomas Owens
+1  A: 

You can always annotate the fields you do/do not want in your equals method, that should be a straightforward and simple change to it.

Performance is obviously related to how often the object is actually compared, but a lot of frameworks use hash maps, so your equals may be being used more than you think.

Also, speaking of hash maps, you have the same issue with the hashCode method.

Finally, do you really need to compare all of the fields for equality?

Will Hartung
I actually misspoke, we have a hashcode/equals method that just uses the ID of the record (from a db) and since this is always going to be unique the hashCode/equals method just compares IDs. But we need to check whether or not the objects are equivelent, having every field be the same.
oreoshake
A: 

If you have access to the names of the fields, why don't you make it a standard that fields you don't want to include always start with "local" or "nochk" or something like that.

Then you blacklist all fields that begin with this (code is not so ugly then).

I don't doubt it's a little slower. You need to decide whether you want to swap ease-of-updates against execution speed.

paxdiablo
I think adding conventions for something like this reduces the readability unnecessarily.
oreoshake
A: 

I actually misspoke, we have a hashcode/equals method that just uses the ID of the record (from a db) and since this is always going to be unique the hashCode/equals method just compares IDs. But we need to check whether or not the objects are equivelent, having every field be the same. It is for a sync app if you haven't guessed by now.

oreoshake
+1  A: 

You have a few bugs in your code.

  1. You cannot assume that this and obj are the same class. Indeed, it's explicitly allowed for obj to be any other class. You could start with if ( ! obj instanceof myClass ) return false; however this is still not correct because obj could be a subclass of this with additional fields that might matter.
  2. You have to support null values for obj with a simple if ( obj == null ) return false;
  3. You can't treat null and empty string as equal. Instead treat null specially. Simplest way here is to start by comparing Field.get(obj) == Field.get(this). If they are both equal or both happen to point to the same object, this is fast. (Note: This is also an optimization, which you need since this is a slow routine.) If this fails, you can use the fast if ( Field.get(obj) == null || Field.get(this) == null ) return false; to handle cases where exactly one is null. Finally you can use the usual equals().
  4. You're not using foundMismatch

I agree with Hank that [HashCodeBuilder][1] and [EqualsBuilder][2] is a better way to go. It's easy to maintain, not a lot of boilerplate code, and you avoid all these issues.

Jason Cohen
A: 

I appreciate the responses, but I'm afraid by posting my code I may have mislead everyone. I wasn't looking to have the code refactored, in fact that wasn't even the code I used. I was just looking to get input on whether or not to use reflection (whether homegrown or using apache-commons) over explicitly writing the checks. I can see advantages both ways, but so far I'm leaning towards reflection, the only drawback left is performance, which is not very important in my case (the equals method accounts for less than 0.1 of my execution time).

oreoshake
+2  A: 

Here's a thought if you're worried about:

1/ Forgetting to update your big series of if-statements for checking equality when you add/remove a field.

2/ The performance of doing this in the equals() method.

Try the following:

a/ Revert back to using the long sequence of if-statements in your equals() method.

b/ Have a single function which contains a list of the fields (in a String array) and which will check that list against reality (i.e., the reflected fields). It will throw an exception if they don't match.

c/ In your constructor for this object, have a synchronized run-once call to this function (similar to a singleton pattern). In other words, if this is the first object constructed by this class, call the checking function described in (b) above.

The exception will make it immediately obvious when you run your program if you haven't updated your if-statements to match the reflected fields; then you fix the if-statements and update the field list from (b) above.

Subsequent construction of objects will not do this check and your equals() method will run at it's maximum possible speed.

Try as I might, I haven't been able to find any real problems with this approach (greater minds may exist on StackOverflow) - there's an extra condition check on each object construction for the run-once behaviour but that seems fairly minor.

If you try hard enough, you could still get your if-statements out of step with your field-list and reflected fields but the exception will ensure your field list matches the reflected fields and you just make sure you update the if-statements and field list at the same time.

paxdiablo
+2  A: 

If you do go the reflection approach, EqualsBuilder is still your friend:

 public boolean equals(Object obj) {
    return EqualsBuilder.reflectionEquals(this, obj);
 }
daveb
A: 

You could use Annotations to exclude fields from the check

e.g.

@IgnoreEquals
String fieldThatShouldNotBeCompared;

And then of course you check the presence of the annotation in your generic equals method.

Wouter Lievens
+5  A: 

Use Eclipse, FFS!

Delete the hashCode and equals methods you have.

Right click on the file.

Select Source->Generate hashcode and equals...

Done! No more worries about reflection.

Repeat for each field added, you just use the outline view to delete your two methods, and then let Eclipse autogenerate them.

MetroidFan2002