Today a story about a design decision whether or not to implement the Comparable interface. We will start with the following code snippet – a simplified version of a UI widget that holds info about a person and can be selected by the user:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
public static class SelectablePerson {
private final String name;
private boolean selected;
public SelectablePerson(String name) {
this.name = name;
this.selected = false;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
}
|
A quick notice: the code above is just to show my point in this post, not to be taken seriously when implementing similar functionality
As you can see this class is fairly simple: just with a name and ’selected’ variable that can be switched on and off. We use those objects in our application to show a list of persons (maybe employees?) and allow the user to select a subset of them (to give a raise or fire?).
All is working fine, but then we get a new requirement: since it is hard to find an employee on the list we want to have it now sorted alphabetically. No biggy, right? We can sort all our objects with Collections.sort() and display them sorted. There is only one decision to make: either we will have ‘SelectablePerson’ implementing Comparable interface or write a separate comparator and use it in sort. So which one is the best option? Is it even important? Let’s check that!
Assume we go with the first possibility and implement additional interface. This means that we have to add one new method to the class – compareTo()… right? Well, close but not really. If we have compareTo() we also need to implement equals(). If we have this one we also need hashCode(). After all the class will look like this:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
public class SelectablePerson implements Comparable<SelectablePerson> {
// ...
public int compareTo(SelectablePerson other) {
return this.name.compareTo(other.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SelectablePerson)) return false;
SelectablePerson other = (SelectablePerson) obj;
return this.compareTo(other) == 0;
}
}
|
It is a pain that we had to go with equals() and hashCode() – I mean why do you need a hash code to sort out widgets? This is an indication that something is going wrong… but besides that all is fine and we can do the sorting. Mission accomplished!
Next requirement: we need our widget to react on the changes in other widgets of UI. This is done in a standard way with Observer-Observable pattern. What’s awesome is that in Java implementing that is really easy. Let’s be more specific with the new requirement: we need to have a reset button that will unselect all of the SelectablePerson widgets. This is the code of the button:
1: 2: 3: 4: 5: 6: |
public class ResetButton extends Observable {
public void press() {
setChanged();
notifyObservers();
}
}
|
Now a simple change in SelectablePerson – implement Observer and as a reaction on (any) change of an observed object clear the selection. The code is following:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
public class SelectablePerson implements Comparable<SelectablePerson>,
Observer {
// ...
public void update(Observable observable, Object value) {
selected = false;
}
}
|
Now when an reset() method of a reset button is invoked all SelectablePerson objects observing it will be unselected. Nice, huh?
Now we are back to the main question: was the decision of implementing Comparable good? You can use the code above, compile it and test it… all is working, right? Well, no… I really suggest trying to implement it as the bug is not that obvious and its good to see it on your own. One of the use cases making the bug appear is when you will instantiate the widgets with the following names: “Smith”, “Brown”, “Smith”, “Jackson”. You will notice that sorting is working fine, but the reset button does not work correctly – it resets all except one of the “Smith” widgets. Strange, huh? Even more bizarre: why only one of the same widgets is affected??
The explanation is quite simple: the reset button extends Observable (as it should). The Observable class internals handle the keeping and managing of all the objects that observe it. More specifically it keeps a list (Vector) of all observing objects and (when appropriate) notifies them about change in observed object. Every time you add another observer the Observable.addObserver() code checks whether or not the provided object is already in the list. How does it check? By comparing with equals() function, the one that we had to override… So because the name “Smith” appears twice in UI both of its widgets are considered by reset button equal and therefore only one is added to the list of observers. And the one that is not added is the one malfunctioning…
Now back to the main topic: would we encounter the same bug if we wrote a Comparator instead of implementing Comparable? No, we would not. You might argue that there are ways of keeping the Comparable and removing the bug… true, but that would just mask the design error we made. The reason we run into trouble is that the SelectablePerson instances are not suppose to have equals() or compareTo() methods as they are in fact unique and representing UI entities rather than values they carry. This comes to an additional lesson: if you do not really need to implement Comparable, do not do it… especially if you can get by with a Comparator
19 Comments until now
The code broke not because Comparable was bad but because you were not aware that an Observerable would check with equals to determine if an Observer needs to be added.
There are many classes within java that make use of Comparable / equals / hashCode internally. Your post helps highlight a potential gotcha. I would not fully agree that this should be a reason to move away from implementing Comparable though. Thanks for sharing
This has nothing to do with the interface.
Please read Effective Java from Joshua Bloch..
I think the basic problem was deciding to make the Person object Comparable. There is no inherent comparison operation on this object. Now you want to sort by name, but later it may be by city or zip, or account status… Unless there is some significant reason not to, I would stick with the Comparator for classes without natural ordering.
In the code, you actually have an implementation error of equals and hashCode. This is a separate error from the design error. Sometimes you may indeed want equals and hashCode implementations because two separate object instances may represent the same entity. Objects instantiated from a relational database, for example, cometo mind.
@CertPal: the actual bug manifested itself because Observable uses equal, but it was caused by the design decision of implementing Comparable.
@Java: I totally agree with the first part of what you wrote. I do not see a bug in the implementation – could you be more specific?
@Jose: Hehe… I did read the EJ. Are you referring to any specific item or issue?
The problem here is conceptual, more than technical.
The Comparable interface is meant to be implemented if there is an ordering that inherent to the domain object (ie, numbers, days of week, months). For all other cases, you should implement a Comparator.
n this case, ordering by name is not inherent of SelectablePerson, as in the future you may want to order by the selected status too.
There are a couple of issues at play here:
The first problem is that you did not read the javadoc for Observable’s addObserver method. It clearly states that it will only add an observer if an equal observer is not already in the set (note the term they used: ’set’ — not ‘list’).
The second problem is that your SelectablePerson object only uses name in its comparison. The equals method says that all Smith’s are equal. Therefore, only one Smith will be added to an observable (or, for that matter, any Set). If you want more than one Smith to be available in any Set, you must add other attributes to the object’s “business key” or use some sort of surrogate ID (like the primary key in a database). This won’t help you sort by name… that’s why Comparator was introduced.
@Jeff: you are right about the direct causes of the error but I think you are missing the main issue here. The problem was not caused by ‘not reading Javadoc’ or faulty implementation of equals() but by a bad design decision of implementing Comparable instead of using a Comparator. You are also right that adding a field to SelectablePerson class could remove that bug, but it would only complicate the design and mask the real issue.
Interesting post, thanks for sharing.
For me the whole Observable / Observer is a bad implementation of the observer-pattern because you have to use inheritance.
For an imo much better realization of the observer pattern, check out the EventBus:
http://bwinterberg.blogspot.com/2009/05/eventbus-library.html
[...] chronicles an interesting tale on choosing between Comparable vs. Comparator. In short, the design decision to have an object implement Comparable led to problems later when [...]
Wouldn’t all of this be sorted out if equals/hashcode were implemented correctly? Regardless of Comparable/Comparator?
“The problem was not caused by ‘not reading Javadoc’ or faulty implementation of equals() but by a bad design decision of implementing Comparable instead of using a Comparator. You are also right that adding a field to SelectablePerson class could remove that bug, but it would only complicate the design and mask the real issue.”
Really? IMHO, you are the one missing the main point. Regardless of if the design decision was a good one or not, the implementation is poor. And it most definitely is not masking the real issue; it’s exposing it.
Per your argument, you would blame the following on the Set interface, or the “design decision” to use a Set implementation:
final SelectablePerson p1 = new SelectablePerson( “smith” );
final SelectablePerson p2 = new SelectablePerson( “smith” );
final Set people = new HashSet();
people.add( p1 );
people.add( p2 );
// Fails because there’s only one element in the Set
assertEquals( 2, people.size() );
Comparable should be implemented to define natural ordering for Objects of a class, and every class may not have natural ordering.
Here for example if the SelectablePerson had an id field, then that could be used for natural ordering, since we would consider two SelectablePerson to be equal whenever their ids match. In all other cases, one should use a Comparator.
Genial brief and this mail helped me alot in my college assignement. Thank you on your information.
Looking forward to finally finishing my research, great post!
Head Profile Sketches Medicine Stock cheap ambien without prescription Ambien can be prescribed to act as an anti-convulsant or muscle relaxant. http://www.3rdhalo.com/ – ambien online no prescription
kJZXoB bftywaakaljm, [url=http://mierrbhvjrpm.com/]mierrbhvjrpm[/url], [link=http://mafjqwtdshlo.com/]mafjqwtdshlo[/link], http://nkdvrnuhxcdb.com/
, Viagra, 5606, Viagra, >:]]], Personal loan, 8OOO,
, Viagra online, >:PPP, Generic viagra, hwdve, Viagra, 8-D,
hjjsfkbwbcmphhjoh, Generic viagra without prescription, pBCfzxF, [url=http://arrogatemedical.com/GenericViagra.html]Buy viagra online[/url], nENdARF, http://arrogatemedical.com/GenericViagra.html Is maxoderm better than viagra, lgFBxRg.
Add your Comment!